@eventcatalog/core 5.5.1 → 5.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-GZLVDQPM.js → chunk-5HSYALWK.js} +1 -1
  6. package/dist/{chunk-K5QKWVI3.js → chunk-7ZEKF2LA.js} +1 -1
  7. package/dist/{chunk-6KR5ZLEQ.js → chunk-CNN747RO.js} +1 -1
  8. package/dist/{chunk-EBZQJP7F.js → chunk-MC4F3VVM.js} +1 -1
  9. package/dist/{chunk-GPIKYBQ5.js → chunk-WG6ZK6L3.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/eventcatalog.cjs +1 -1
  13. package/dist/eventcatalog.js +11 -11
  14. package/dist/federation/federate.js +3 -3
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.js +3 -3
  17. package/dist/utils/cli-logger.cjs +1 -1
  18. package/dist/utils/cli-logger.js +2 -2
  19. package/eventcatalog/src/features/mcp/apps/architecture-diagram/view.tsx +8 -2
  20. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +2 -2
  21. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +67 -67
  22. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +72 -72
  23. package/eventcatalog/src/features/mcp/apps/schema-viewer/view.tsx +2 -1
  24. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +24 -5
  25. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -1
  26. package/eventcatalog/src/features/mcp/mcp-server.ts +2 -0
  27. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +2 -0
  28. package/eventcatalog/src/features/studio/tool-sync.ts +67 -3
  29. package/package.json +3 -3
  30. package/dist/{chunk-NCEZDQSW.js → chunk-QJ7KKN2D.js} +7 -7
@@ -12,6 +12,7 @@ import AvroSchemaViewer from '../../../../components/SchemaExplorer/AvroSchemaVi
12
12
  import ProtobufSchemaViewer from '../../../../components/SchemaExplorer/ProtobufSchemaViewer';
13
13
  import { SchemaCode } from './schema-code';
14
14
  import { RESOURCE_TYPE_LABELS } from '../shared/resource-types';
15
+ import { fullscreenStyle } from '../shared/app-view';
15
16
  import type { SchemaViewerPayload, SchemaViewerSchema } from './shared';
16
17
 
17
18
  const INLINE_MAX_HEIGHT = '560px';
@@ -75,7 +76,7 @@ export function SchemaViewerView({ app, payload, hostContext }: SchemaViewerView
75
76
  'inline-flex h-7 items-center gap-1.5 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-card-bg))] px-2.5 text-xs font-medium text-[rgb(var(--ec-page-text))] transition-colors hover:border-[rgb(var(--ec-accent))] hover:text-[rgb(var(--ec-accent))]';
76
77
 
77
78
  return (
78
- <div className={`flex flex-col ${isFullscreen ? 'h-screen' : ''}`}>
79
+ <div className="flex flex-col" style={isFullscreen ? fullscreenStyle(hostContext) : undefined}>
79
80
  <div className="flex items-center justify-between gap-3 border-b border-[rgb(var(--ec-page-border))] px-3 py-2">
80
81
  <div className="flex min-w-0 items-center gap-2 text-[13px]">
81
82
  <span className="shrink-0 rounded bg-[rgb(var(--ec-accent-subtle))] px-1.5 py-px text-[10px] font-semibold uppercase tracking-wide text-[rgb(var(--ec-accent))]">
@@ -2,7 +2,7 @@
2
2
  * Connecting the EventCatalog viewer MCP App to the host, loading the tool result's payload, following the
3
3
  * host's theme and display mode, and mounting the view.
4
4
  */
5
- import { useEffect, useState, type ComponentType } from 'react';
5
+ import { useEffect, useState, type ComponentType, type CSSProperties } from 'react';
6
6
  import { config as configureZod } from 'zod';
7
7
  import { createRoot } from 'react-dom/client';
8
8
  import type { App, McpUiHostContext } from '@modelcontextprotocol/ext-apps';
@@ -20,6 +20,24 @@ const applyTheme = (context?: McpUiHostContext) => {
20
20
  if (context?.theme) document.documentElement.setAttribute('data-theme', context.theme);
21
21
  };
22
22
 
23
+ /**
24
+ * The view full screen: the height of the frame, kept clear of what the host draws over it on phones (the notch,
25
+ * the home indicator, the message box), which it reports as safe area insets
26
+ */
27
+ export const fullscreenStyle = (context?: McpUiHostContext): CSSProperties => {
28
+ const insets = context?.safeAreaInsets;
29
+ return {
30
+ height: '100vh',
31
+ boxSizing: 'border-box',
32
+ ...(insets && {
33
+ paddingTop: insets.top,
34
+ paddingRight: insets.right,
35
+ paddingBottom: insets.bottom,
36
+ paddingLeft: insets.left,
37
+ }),
38
+ };
39
+ };
40
+
23
41
  type McpAppViewOptions<Payload> = {
24
42
  name: string;
25
43
  /** Key of the payload in the model-facing tool result's `_meta` */
@@ -61,8 +79,9 @@ export function useMcpAppView<Payload>(options: McpAppViewOptions<Payload>) {
61
79
  appInfo: { name: options.name, version: '1.0.0' },
62
80
  // The views fit inline and have a full screen button (the server asks hosts to open them full screen)
63
81
  capabilities: { availableDisplayModes: ['inline', 'fullscreen'] },
82
+ // Listeners go on before the view connects: hosts send the tool's result as soon as it has
64
83
  onAppCreated: (createdApp) => {
65
- createdApp.ontoolresult = (result) => {
84
+ createdApp.addEventListener('toolresult', (result) => {
66
85
  if (result.isError) {
67
86
  setError(result.content?.find((item) => item.type === 'text')?.text ?? options.loadErrorMessage);
68
87
  return;
@@ -70,11 +89,11 @@ export function useMcpAppView<Payload>(options: McpAppViewOptions<Payload>) {
70
89
  loadPayload(createdApp, result, options)
71
90
  .then(setPayload)
72
91
  .catch((loadError) => setError(String(loadError?.message ?? loadError)));
73
- };
74
- createdApp.onhostcontextchanged = (context) => {
92
+ });
93
+ createdApp.addEventListener('hostcontextchanged', (context) => {
75
94
  applyTheme(context);
76
95
  setHostContext((previous) => ({ ...previous, ...context }));
77
- };
96
+ });
78
97
  },
79
98
  });
80
99
 
@@ -54,6 +54,8 @@ export type McpAppDefinition<Shape extends z.ZodRawShape, ViewShape extends z.Zo
54
54
  csp?: { resourceDomains?: string[]; connectDomains?: string[] };
55
55
  /** How hosts open the view: ChatGPT reads these before loading it, so it can open straight in that mode */
56
56
  displayModes?: { preferred: McpUiDisplayMode; available: McpUiDisplayMode[] };
57
+ /** Whether the host frames the view with a border and background (hosts' defaults differ, e.g. Claude's web and mobile apps) */
58
+ prefersBorder?: boolean;
57
59
  };
58
60
  /**
59
61
  * Ties every result to one view (ChatGPT's `openai/widgetSessionId`): the host updates the view it already
@@ -151,7 +153,10 @@ export function registerMcpApp<Shape extends z.ZodRawShape, ViewShape extends z.
151
153
  mimeType: RESOURCE_MIME_TYPE,
152
154
  text: await loadView(),
153
155
  _meta: {
154
- ...(app.resource.csp && { ui: { csp: app.resource.csp } }),
156
+ ui: {
157
+ ...(app.resource.csp && { csp: app.resource.csp }),
158
+ ...(app.resource.prefersBorder !== undefined && { prefersBorder: app.resource.prefersBorder }),
159
+ },
155
160
  ...(app.resource.displayModes && {
156
161
  'openai/ui': {
157
162
  preferredDisplayMode: app.resource.displayModes.preferred,
@@ -512,6 +512,8 @@ function createMcpServer(
512
512
  csp: { resourceDomains: [catalogUrl] },
513
513
  // Diagrams and schemas need the room, so hosts that can open the viewer full screen straight away
514
514
  displayModes: { preferred: 'fullscreen', available: ['inline', 'fullscreen'] },
515
+ // The viewer has its own background: no frame around it, on the web and on phones alike
516
+ prefersBorder: false,
515
517
  },
516
518
  // One viewer per server: each result updates the viewer the host already shows
517
519
  sessionId: scope ? `eventcatalog:${scope.ref.kind}:${scope.ref.id}` : 'eventcatalog',
@@ -124,6 +124,8 @@ export function registerCanvasTools(server: McpServer, { catalogUrl, userAgent }
124
124
  // The view joins the canvas over the collaboration WebSocket, and shows icons served by this EventCatalog
125
125
  csp: { connectDomains: [new URL(socketUrl).origin, catalogUrl], resourceDomains: [catalogUrl] },
126
126
  displayModes: { preferred: 'fullscreen', available: ['inline', 'fullscreen'] },
127
+ // The canvas has its own background: no frame around it, on the web and on phones alike
128
+ prefersBorder: false,
127
129
  },
128
130
  metaKey: CANVAS_META_KEY,
129
131
  tool: {
@@ -6,7 +6,8 @@ import type { Status } from './hooks/use-studio-flow';
6
6
  * Syncing a canvas through MCP tool calls, for when a view can't open the collaboration WebSocket
7
7
  * (e.g. an MCP App inside a chat whose sandbox blocks it). Each call sends the server what it doesn't have
8
8
  * yet (by its state vector) and our presence, and gets back what we don't have and everyone's presence.
9
- * Slower than the WebSocket (it polls), but works wherever the chat can call the MCP server's tools.
9
+ * Slower than the WebSocket (it polls), but works wherever the chat can call the MCP server's tools. It polls less
10
+ * while nothing changes, and rarely while the view is out of sight (chats keep every view they showed running).
10
11
  */
11
12
 
12
13
  export type SyncRequest = {
@@ -33,6 +34,11 @@ export const fromBase64 = (text: string) => Uint8Array.from(atob(text), (char) =
33
34
  /** Applied with this origin, so undo (which tracks local changes) leaves other people's changes alone */
34
35
  const REMOTE = Symbol('tool-sync');
35
36
 
37
+ /** Waiting longest between syncs while nothing changes on either side */
38
+ const IDLE_INTERVAL_MS = 5000;
39
+ /** Between syncs while the view is out of sight (slowed, not stopped: a host that wrongly reports it hidden still syncs) */
40
+ const OUT_OF_SIGHT_INTERVAL_MS = 15_000;
41
+
36
42
  export function startToolSync(
37
43
  doc: Y.Doc,
38
44
  awareness: Awareness,
@@ -44,32 +50,90 @@ export function startToolSync(
44
50
  }: { canvasId: string; sync: ToolSync; onStatus: (status: Status) => void; intervalMs?: number }
45
51
  ) {
46
52
  let serverVector: Uint8Array | undefined;
53
+ let lastServerVector: string | undefined;
54
+ let lastAwareness: string | undefined;
47
55
  let stopped = false;
56
+ let syncing = false;
48
57
  let timer: ReturnType<typeof setTimeout> | undefined;
58
+ let dueAt = 0;
59
+ // Slower while nothing changes (a chat keeps every view it showed running, each polling), back to quick on a change
60
+ let delay = intervalMs;
61
+ // Hosts keep a chat's earlier views running: one scrolled out of sight, or in a hidden tab, syncs rarely
62
+ let inView = true;
63
+ const isShown = () => inView && document.visibilityState !== 'hidden';
64
+
65
+ const schedule = (ms: number) => {
66
+ clearTimeout(timer);
67
+ dueAt = Date.now() + ms;
68
+ timer = setTimeout(tick, ms);
69
+ };
49
70
 
50
71
  const tick = async () => {
72
+ timer = undefined;
73
+ if (stopped || syncing) return;
74
+ syncing = true;
51
75
  try {
76
+ const update = Y.encodeStateAsUpdate(doc, serverVector);
52
77
  const response = await sync({
53
78
  canvasId,
54
79
  clientId: doc.clientID,
55
80
  stateVector: toBase64(Y.encodeStateVector(doc)),
56
- update: toBase64(Y.encodeStateAsUpdate(doc, serverVector)),
81
+ update: toBase64(update),
57
82
  awareness: toBase64(encodeAwarenessUpdate(awareness, [doc.clientID])),
58
83
  });
59
84
  if (stopped) return;
60
85
  Y.applyUpdate(doc, fromBase64(response.update), REMOTE);
61
86
  serverVector = fromBase64(response.stateVector);
62
87
  if (response.awareness) applyAwarenessUpdate(awareness, fromBase64(response.awareness), REMOTE);
88
+ // Anything new either way (the canvas, or anyone's presence) keeps it quick
89
+ const changed = response.stateVector !== lastServerVector || response.awareness !== lastAwareness;
90
+ lastServerVector = response.stateVector;
91
+ lastAwareness = response.awareness;
92
+ delay = changed ? intervalMs : Math.min(delay * 1.5, IDLE_INTERVAL_MS);
63
93
  onStatus('connected');
64
94
  } catch {
65
95
  if (!stopped) onStatus('disconnected');
96
+ } finally {
97
+ syncing = false;
66
98
  }
67
- if (!stopped) timer = setTimeout(tick, intervalMs);
99
+ // Out of sight: rarely, and straight away again once it's shown (see show)
100
+ if (!stopped) schedule(isShown() ? delay : Math.max(delay, OUT_OF_SIGHT_INTERVAL_MS));
101
+ };
102
+
103
+ // Our own changes and presence go out within the quick interval, however long it's been waiting
104
+ const wake = (_update: unknown, origin: unknown) => {
105
+ if (origin === REMOTE || stopped) return;
106
+ delay = intervalMs;
107
+ if (timer && dueAt > Date.now() + intervalMs) schedule(intervalMs);
68
108
  };
109
+ doc.on('update', wake);
110
+ awareness.on('update', wake);
111
+
112
+ const show = () => {
113
+ if (stopped || !isShown() || syncing || dueAt <= Date.now() + intervalMs) return;
114
+ clearTimeout(timer);
115
+ delay = intervalMs;
116
+ void tick();
117
+ };
118
+ document.addEventListener('visibilitychange', show);
119
+ // Seen from inside the chat's frame: whether the view is on the screen at all
120
+ const observer =
121
+ typeof IntersectionObserver === 'undefined'
122
+ ? undefined
123
+ : new IntersectionObserver(([entry]) => {
124
+ inView = entry?.isIntersecting ?? true;
125
+ show();
126
+ });
127
+ observer?.observe(document.documentElement);
128
+
69
129
  void tick();
70
130
 
71
131
  return () => {
72
132
  stopped = true;
73
133
  clearTimeout(timer);
134
+ doc.off('update', wake);
135
+ awareness.off('update', wake);
136
+ document.removeEventListener('visibilitychange', show);
137
+ observer?.disconnect();
74
138
  };
75
139
  }
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "BUSL-1.1",
9
9
  "type": "module",
10
- "version": "5.5.1",
10
+ "version": "5.5.2",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -141,11 +141,11 @@
141
141
  "y-protocols": "^1.0.7",
142
142
  "yjs": "^13.6.33",
143
143
  "zod": "^4.6.6",
144
+ "@eventcatalog/exporters": "0.1.2",
144
145
  "@eventcatalog/fitness": "0.1.0",
145
146
  "@eventcatalog/linter": "1.2.4",
146
147
  "@eventcatalog/sdk": "2.31.2",
147
- "@eventcatalog/exporters": "0.1.2",
148
- "@eventcatalog/visualiser": "^5.1.0"
148
+ "@eventcatalog/visualiser": "^5.1.1"
149
149
  },
150
150
  "devDependencies": {
151
151
  "@astrojs/check": "^0.9.10",
@@ -1,10 +1,3 @@
1
- import {
2
- createFederationContentCache
3
- } from "./chunk-R7Z5ALYI.js";
4
- import {
5
- getFederationDiagnostics,
6
- resolveFederationRules
7
- } from "./chunk-JJPB6EOZ.js";
8
1
  import {
9
2
  withFederationOutputTransaction
10
3
  } from "./chunk-WRNH5C3U.js";
@@ -14,6 +7,13 @@ import {
14
7
  import {
15
8
  createFederationSourceProvider
16
9
  } from "./chunk-A2RZR3U4.js";
10
+ import {
11
+ createFederationContentCache
12
+ } from "./chunk-R7Z5ALYI.js";
13
+ import {
14
+ getFederationDiagnostics,
15
+ resolveFederationRules
16
+ } from "./chunk-JJPB6EOZ.js";
17
17
  import {
18
18
  getEventCatalogConfigFile
19
19
  } from "./chunk-4G7FSRPN.js";