@eventcatalog/core 5.0.0-beta.2 → 5.0.0-beta.20

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 (204) hide show
  1. package/LICENSE +234 -134
  2. package/README.md +5 -5
  3. package/dist/analytics/analytics.cjs +18 -3
  4. package/dist/analytics/analytics.d.cts +1 -1
  5. package/dist/analytics/analytics.d.ts +1 -1
  6. package/dist/analytics/analytics.js +3 -2
  7. package/dist/analytics/log-build.cjs +71 -35
  8. package/dist/analytics/log-build.js +5 -4
  9. package/dist/analytics/telemetry.cjs +42 -0
  10. package/dist/analytics/telemetry.d.cts +20 -0
  11. package/dist/analytics/telemetry.d.ts +20 -0
  12. package/dist/analytics/telemetry.js +8 -0
  13. package/dist/catalog-runtime.cjs +1 -1
  14. package/dist/{chunk-HGRFXTFC.js → chunk-3VQSVQTU.js} +1 -1
  15. package/dist/{chunk-KIDISU6X.js → chunk-5ZUN2TXB.js} +56 -33
  16. package/dist/{chunk-TQLRG35T.js → chunk-AWOO63ME.js} +71 -45
  17. package/dist/chunk-CPW6G62V.js +17 -0
  18. package/dist/{chunk-4J2ZM2HX.js → chunk-F236OJZD.js} +1 -1
  19. package/dist/{chunk-XARPIXWF.js → chunk-FWH5T6U4.js} +9 -3
  20. package/dist/{chunk-62EV4IJA.js → chunk-R2X4ZEF6.js} +1 -1
  21. package/dist/constants.cjs +1 -1
  22. package/dist/constants.js +1 -1
  23. package/dist/eventcatalog.cjs +214 -129
  24. package/dist/eventcatalog.config.d.cts +59 -1
  25. package/dist/eventcatalog.config.d.ts +59 -1
  26. package/dist/eventcatalog.js +39 -15
  27. package/dist/federation/federate.js +2 -2
  28. package/dist/generate.cjs +1 -1
  29. package/dist/generate.js +3 -3
  30. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio-fonts.css.mjs +3 -0
  31. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.css.mjs +3 -0
  32. package/dist/runtime-dependencies/@eventcatalog__visualiser__studio.mjs +3 -0
  33. package/dist/runtime-dependencies/@hocuspocus__provider.mjs +3 -0
  34. package/dist/runtime-dependencies/@hocuspocus__server.mjs +3 -0
  35. package/dist/runtime-dependencies/@mcp-b__webmcp-local-relay.mjs +3 -0
  36. package/dist/runtime-dependencies/@mcp-b__webmcp-polyfill.mjs +3 -0
  37. package/dist/runtime-dependencies/htmlparser2.mjs +3 -0
  38. package/dist/runtime-dependencies/manifest.json +13 -1
  39. package/dist/runtime-dependencies/react-hotkeys-hook.mjs +3 -0
  40. package/dist/runtime-dependencies/react-syntax-highlighter__dist__cjs__create-element.js.mjs +3 -0
  41. package/dist/runtime-dependencies/y-protocols.mjs +3 -0
  42. package/dist/runtime-dependencies/y-protocols__awareness.mjs +3 -0
  43. package/dist/runtime-dependencies/yjs.mjs +3 -0
  44. package/dist/utils/cli-logger.cjs +1 -1
  45. package/dist/utils/cli-logger.js +2 -2
  46. package/dist/utils/license-status.cjs +71 -45
  47. package/dist/utils/license-status.d.cts +14 -5
  48. package/dist/utils/license-status.d.ts +14 -5
  49. package/dist/utils/license-status.js +1 -1
  50. package/eventcatalog/astro.config.mjs +23 -1
  51. package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
  52. package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
  53. package/eventcatalog/integrations/storage.mjs +20 -0
  54. package/eventcatalog/integrations/studio-server.mjs +32 -0
  55. package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
  56. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +2 -0
  57. package/eventcatalog/src/components/Grids/utils.tsx +0 -23
  58. package/eventcatalog/src/components/Header.astro +3 -3
  59. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +6 -1
  60. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +42 -3
  61. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -1
  62. package/eventcatalog/src/components/NavigationProgress.astro +7 -0
  63. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +2 -0
  64. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +2 -0
  65. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -0
  66. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +2 -0
  67. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +12 -4
  68. package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
  69. package/eventcatalog/src/components/Search/SearchModal.tsx +2 -10
  70. package/eventcatalog/src/components/Search/search-utils.ts +26 -0
  71. package/eventcatalog/src/components/Seo.astro +5 -1
  72. package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +53 -83
  73. package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
  74. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +131 -0
  75. package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
  76. package/eventcatalog/src/components/Tables/Discover/DiscoverTable.tsx +4 -62
  77. package/eventcatalog/src/components/Tables/Discover/columns.tsx +17 -150
  78. package/eventcatalog/src/components/Tables/Resources/ResourcesTable.tsx +425 -0
  79. package/eventcatalog/src/components/Tables/RowActionsMenu.tsx +157 -0
  80. package/eventcatalog/src/components/Tables/TablePagination.tsx +82 -0
  81. package/eventcatalog/src/features/ai/chat-api.ts +3 -3
  82. package/eventcatalog/src/features/auth/error.astro +4 -2
  83. package/eventcatalog/src/features/auth/login.astro +2 -1
  84. package/eventcatalog/src/features/auth/middleware/middleware-auth.ts +10 -0
  85. package/eventcatalog/src/features/auth/unauthorized.astro +1 -1
  86. package/eventcatalog/src/features/fields/pages/fields.astro +1 -1
  87. package/eventcatalog/src/features/integrations/eventcatalog-features.ts +34 -1
  88. package/eventcatalog/src/features/mcp/apps/architecture-diagram/node-chat.tsx +6 -45
  89. package/eventcatalog/src/features/mcp/apps/canvas/shared.ts +47 -0
  90. package/eventcatalog/src/features/mcp/apps/canvas/styles.css +24 -0
  91. package/eventcatalog/src/features/mcp/apps/canvas/view.tsx +278 -0
  92. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +272 -0
  93. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +77 -76
  94. package/eventcatalog/src/features/mcp/apps/shared/app-view.tsx +2 -1
  95. package/eventcatalog/src/features/mcp/mcp-apps.ts +6 -3
  96. package/eventcatalog/src/features/mcp/mcp-server.ts +36 -5
  97. package/eventcatalog/src/features/server/start.ts +25 -0
  98. package/eventcatalog/src/features/storage/database.ts +88 -0
  99. package/eventcatalog/src/features/storage/migrations.ts +85 -0
  100. package/eventcatalog/src/features/storage/sql/database.sql +5 -0
  101. package/eventcatalog/src/features/storage/sql/migrations/0001-studio-canvases.sql +12 -0
  102. package/eventcatalog/src/features/storage/sql/migrations.sql +15 -0
  103. package/eventcatalog/src/features/storage/sql.ts +28 -0
  104. package/eventcatalog/src/features/studio/agent-choreography.ts +384 -0
  105. package/eventcatalog/src/features/studio/agents.ts +28 -0
  106. package/eventcatalog/src/features/studio/api/canvases-api.ts +219 -0
  107. package/eventcatalog/src/features/studio/api/canvases.ts +25 -0
  108. package/eventcatalog/src/features/studio/api/client.ts +40 -0
  109. package/eventcatalog/src/features/studio/canvas-actions.ts +540 -0
  110. package/eventcatalog/src/features/studio/canvas-preview.ts +52 -0
  111. package/eventcatalog/src/features/studio/catalog-resources.ts +155 -0
  112. package/eventcatalog/src/features/studio/catalog.ts +173 -0
  113. package/eventcatalog/src/features/studio/components/CanvasHeader.tsx +518 -0
  114. package/eventcatalog/src/features/studio/components/CanvasStatusMenu.tsx +118 -0
  115. package/eventcatalog/src/features/studio/components/ConnectAgentDialog.tsx +434 -0
  116. package/eventcatalog/src/features/studio/components/ConnectionsChooser.tsx +143 -0
  117. package/eventcatalog/src/features/studio/components/DeleteCanvasDialog.tsx +65 -0
  118. package/eventcatalog/src/features/studio/components/Dialog.tsx +60 -0
  119. package/eventcatalog/src/features/studio/components/JoinForm.tsx +109 -0
  120. package/eventcatalog/src/features/studio/components/LeftPanel.tsx +203 -0
  121. package/eventcatalog/src/features/studio/components/NodeDetails.tsx +183 -0
  122. package/eventcatalog/src/features/studio/components/OpenInStudioDialog.tsx +79 -0
  123. package/eventcatalog/src/features/studio/components/Presence.tsx +293 -0
  124. package/eventcatalog/src/features/studio/components/ShareDialog.tsx +396 -0
  125. package/eventcatalog/src/features/studio/components/ShortcutsDialog.tsx +41 -0
  126. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2091 -0
  127. package/eventcatalog/src/features/studio/components/StudioNotice.astro +24 -0
  128. package/eventcatalog/src/features/studio/document-name.ts +7 -0
  129. package/eventcatalog/src/features/studio/from-visualiser.ts +138 -0
  130. package/eventcatalog/src/features/studio/hooks/presence-sender.ts +63 -0
  131. package/eventcatalog/src/features/studio/hooks/presence-store.ts +119 -0
  132. package/eventcatalog/src/features/studio/hooks/use-canvas-webmcp.ts +353 -0
  133. package/eventcatalog/src/features/studio/hooks/use-comments.ts +54 -0
  134. package/eventcatalog/src/features/studio/hooks/use-follow-camera.ts +109 -0
  135. package/eventcatalog/src/features/studio/hooks/use-studio-flow.ts +782 -0
  136. package/eventcatalog/src/features/studio/identity.ts +59 -0
  137. package/eventcatalog/src/features/studio/limits.ts +2 -0
  138. package/eventcatalog/src/features/studio/pages/[id].astro +69 -0
  139. package/eventcatalog/src/features/studio/pages/index.astro +784 -0
  140. package/eventcatalog/src/features/studio/pages/webmcp-relay.ts +27 -0
  141. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +495 -0
  142. package/eventcatalog/src/features/studio/server/runtime.ts +498 -0
  143. package/eventcatalog/src/features/studio/server/sign-in.ts +18 -0
  144. package/eventcatalog/src/features/studio/server/sql/storage.sql +18 -0
  145. package/eventcatalog/src/features/studio/server/storage.ts +90 -0
  146. package/eventcatalog/src/features/studio/server/tool-icons.ts +30 -0
  147. package/eventcatalog/src/features/studio/tool-sync.ts +75 -0
  148. package/eventcatalog/src/layouts/BaseLayout.astro +4 -4
  149. package/eventcatalog/src/layouts/VerticalSideBarLayout.astro +13 -7
  150. package/eventcatalog/src/pages/_index.astro +1 -1
  151. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.spec.ts +52 -63
  152. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/_index.data.ts +34 -46
  153. package/eventcatalog/src/pages/architecture/[type]/[id]/[version]/index.astro +11 -29
  154. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_ResourcesPage.astro +79 -0
  155. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.spec.ts +32 -14
  156. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_index.data.ts +63 -58
  157. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/index.astro +2 -62
  158. package/eventcatalog/src/pages/docs/[type]/[id]/index.astro +1 -1
  159. package/eventcatalog/src/pages/docs/services/[id]/[version]/resources/index.astro +13 -0
  160. package/eventcatalog/src/pages/index.astro +1 -1
  161. package/eventcatalog/src/pages/schemas/explorer/index.astro +1 -1
  162. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +1 -1
  163. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +24 -42
  164. package/eventcatalog/src/stores/sidebar-store/builders/flow.ts +2 -0
  165. package/eventcatalog/src/stores/sidebar-store/builders/message.ts +2 -0
  166. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +23 -23
  167. package/eventcatalog/src/stores/sidebar-store/builders/shared.ts +13 -0
  168. package/eventcatalog/src/stores/sidebar-store/builders/system.ts +18 -27
  169. package/eventcatalog/src/stores/sidebar-store/state.ts +5 -0
  170. package/eventcatalog/src/styles/tailwind.css +2 -2
  171. package/eventcatalog/src/types/react-syntax-highlighter.d.ts +10 -0
  172. package/eventcatalog/src/utils/collections/channels.ts +56 -1
  173. package/eventcatalog/src/utils/collections/flows.ts +50 -1
  174. package/eventcatalog/src/utils/collections/message-connections.ts +60 -0
  175. package/eventcatalog/src/utils/collections/message-triggers.ts +2 -1
  176. package/eventcatalog/src/utils/collections/resource-owners.ts +193 -0
  177. package/eventcatalog/src/utils/collections/resources.ts +310 -0
  178. package/eventcatalog/src/utils/collections/util.ts +31 -0
  179. package/eventcatalog/src/utils/feature.ts +8 -0
  180. package/eventcatalog/src/utils/git.ts +11 -5
  181. package/eventcatalog/src/utils/link-validation.ts +25 -19
  182. package/eventcatalog/src/utils/node-graphs/domain-entity-map.ts +3 -1
  183. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +3 -3
  184. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -2
  185. package/eventcatalog/src/utils/node-graphs/layout-node-graph.ts +20 -1
  186. package/eventcatalog/src/utils/node-graphs/node-resource.ts +67 -0
  187. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +1 -1
  188. package/eventcatalog/src/utils/page-title.ts +15 -0
  189. package/eventcatalog/src/utils/syntax-highlighter-renderer.ts +64 -0
  190. package/eventcatalog/src/utils/trial.ts +1 -1
  191. package/package.json +22 -16
  192. package/dist/runtime-dependencies/shelljs.mjs +0 -3
  193. package/eventcatalog/public/studio-bg.png +0 -0
  194. package/eventcatalog/src/components/Grids/DomainGrid.tsx +0 -838
  195. package/eventcatalog/src/components/Grids/MessageGrid.tsx +0 -344
  196. package/eventcatalog/src/components/Grids/SystemGrid.tsx +0 -185
  197. package/eventcatalog/src/components/Grids/components.tsx +0 -170
  198. package/eventcatalog/src/components/Grids/message-link.ts +0 -38
  199. package/eventcatalog/src/components/Studio/StudioPageModal.tsx +0 -150
  200. package/eventcatalog/src/components/Tables/SystemResources/SystemResourcesTable.tsx +0 -294
  201. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.spec.ts +0 -65
  202. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/resources/_resources.ts +0 -46
  203. package/eventcatalog/src/pages/studio.astro +0 -316
  204. package/dist/{chunk-AYNZXUB5.js → chunk-KHACCTW2.js} +3 -3
@@ -127,10 +127,10 @@ The user will ask you some questions about the software architecture catalog, yo
127
127
 
128
128
  At point the referer url (${referrer}) will be the URL of the page the user is on, you should use this to help you answer the question,
129
129
  You may be able to get the resource from the URL
130
- - Example if the url is like /docs|visualiser|architecture/{collection}/{id}/{version}
130
+ - Example if the url is like /docs|visualiser/{collection}/{id}/{version}
131
131
  - (e.g /docs/events/MyEvent/1.0.0) in this case the id is MyEvent and the version is 1.0.0 and collection is events.
132
132
  - (e.g /visualiser/domains/MyDomain/1.0.0) in this case the id is MyDomain and the version is 1.0.0 and collection is domains.
133
- - (e.g /architecture/services/MyService/1.0.0) in this case the id is MyService and the version is 1.0.0 and collection is services.
133
+ - (e.g /docs/services/MyService/1.0.0/resources) in this case the id is MyService and the version is 1.0.0 and collection is services; the user is looking at the resources the service uses.
134
134
 
135
135
  The referer URL is: ${referrer}
136
136
 
@@ -159,7 +159,7 @@ When responding:
159
159
  - The link options are:
160
160
  - If you want to get the documentation for a resource use the /docs/ prefix (e.g /docs/{collection}/{id}/{version})
161
161
  - If you want to let the user know they can visualize a resource use the /visualiser/ prefix (e.g /visualiser/{collection}/{id}/{version})
162
- - If you want to let the user know they can see the architecture of a resource use the /architecture/ prefix (e.g /architecture/{collection}/{id}/{version})
162
+ - If you want to show the user everything a domain, system, service, flow, channel or message (event, command, query) contains, uses or connects to, link its Resources page (e.g /docs/{collection}/{id}/{version}/resources). Only those collections have a Resources page.
163
163
  - If you don't know the version, use the getResource tool to fetch the resource and get the actual version number before creating the link.
164
164
  7. When you return a schema, use code blocks to render the schema to the user too, for example if the schema is in JSON format use \`\`\`json and if the schema is in YAML format use \`\`\`yaml
165
165
  8. IMPORTANT: After answering each question, ALWAYS use the suggestFollowUpQuestions tool to suggest 2-3 relevant follow-up questions the user might want to ask next. These should be contextual to the conversation and help the user explore related topics.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  import '../../styles/tailwind.css';
3
+ import { formatPageTitle } from '@utils/page-title';
3
4
  // src/pages/auth/error.astro
4
5
 
5
6
  const { searchParams } = new URL(Astro.request.url);
@@ -14,6 +15,7 @@ const errorMessages = {
14
15
 
15
16
  // @ts-ignore
16
17
  const errorMessage = errorMessages[error] || errorMessages.Default;
18
+ const pageTitle = formatPageTitle('Authentication Error');
17
19
  ---
18
20
 
19
21
  <!doctype html>
@@ -25,10 +27,10 @@ const errorMessage = errorMessages[error] || errorMessages.Default;
25
27
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
26
28
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
27
29
  <meta name="generator" content={Astro.generator} />
28
- <title>Authentication Error - EventCatalog</title>
30
+ <title>{pageTitle}</title>
29
31
  </head>
30
32
  <body>
31
- <main title="Authentication Error - EventCatalog">
33
+ <main title={pageTitle}>
32
34
  <div class="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
33
35
  <div class="max-w-md w-full space-y-8 text-center">
34
36
  <div>
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import '../../styles/tailwind.css';
3
3
  import config from '@config';
4
+ import { formatPageTitle } from '@utils/page-title';
4
5
  const { title, logo } = config;
5
6
  import { join } from 'node:path';
6
7
  import { isAuthEnabled, isSSR } from '@utils/feature';
@@ -106,7 +107,7 @@ const providerConfig = {
106
107
  <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
107
108
  <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
108
109
  <meta name="generator" content={Astro.generator} />
109
- <title>Sign In | {title}</title>
110
+ <title>{formatPageTitle('Sign In')}</title>
110
111
  <link rel="preconnect" href="https://fonts.googleapis.com" />
111
112
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
112
113
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
@@ -96,6 +96,14 @@ export function isMcpRoute(pathname: string) {
96
96
  return pathname === '/docs/mcp' || pathname.startsWith('/docs/mcp/');
97
97
  }
98
98
 
99
+ // The Studio API answers in JSON, rather than sending callers to the sign-in page
100
+ const isStudioApiRoute = (pathname: string) => pathname === '/api/studio' || pathname.startsWith('/api/studio/');
101
+ const signInRequired = () =>
102
+ new Response(JSON.stringify({ error: 'Sign in to use this API' }), {
103
+ status: 401,
104
+ headers: { 'Content-Type': 'application/json' },
105
+ });
106
+
99
107
  export const authMiddleware: MiddlewareHandler = async (context, next) => {
100
108
  const { request, redirect, locals } = context;
101
109
  const url = new URL(request.url);
@@ -131,6 +139,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
131
139
  const session = await getSession(request);
132
140
 
133
141
  if (!session) {
142
+ if (isStudioApiRoute(pathname)) return signInRequired();
134
143
  const callbackUrl = encodeURIComponent(pathname + url.search);
135
144
  return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
136
145
  }
@@ -157,6 +166,7 @@ export const authMiddleware: MiddlewareHandler = async (context, next) => {
157
166
  typedLocals.matchesPattern = matchesPattern;
158
167
  } catch (error) {
159
168
  console.error('Session error:', error);
169
+ if (isStudioApiRoute(pathname)) return signInRequired();
160
170
  const callbackUrl = encodeURIComponent(pathname + url.search);
161
171
  return redirect(`/auth/login?callbackUrl=${callbackUrl}`);
162
172
  }
@@ -2,7 +2,7 @@
2
2
  import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
3
3
  ---
4
4
 
5
- <VerticalSideBarLayout title="Unauthorized - EventCatalog" showNestedSideBar={false}>
5
+ <VerticalSideBarLayout title="Unauthorized" showNestedSideBar={false}>
6
6
  <div class="flex h-[calc(100vh-120px)] bg-white">
7
7
  <main class="flex w-full items-center justify-center bg-white text-center sm:p-12">
8
8
  <div class="w-full max-w-xl text-center">
@@ -7,7 +7,7 @@ import FieldsExplorer from '@components/FieldsExplorer/FieldsExplorer';
7
7
  export const prerender = false;
8
8
  ---
9
9
 
10
- <VerticalSideBarLayout title="Fields Explorer - EventCatalog" showNestedSideBar={false}>
10
+ <VerticalSideBarLayout title="Fields Explorer" showNestedSideBar={false}>
11
11
  <style is:global>
12
12
  #eventcatalog-header {
13
13
  left: calc(var(--ec-vertical-nav-width, 14rem) + var(--ec-fields-sidebar-width, 320px)) !important;
@@ -13,6 +13,7 @@ import {
13
13
  isFullCatalogAPIEnabled,
14
14
  isDevMode,
15
15
  isSSR,
16
+ isCanvasEnabled,
16
17
  } from '../../utils/feature';
17
18
  import { getCustomPageRoutes, isApiRoute, listCustomPageFiles, resolveCustomPagesPrefix } from '../custom-pages/routes';
18
19
 
@@ -111,7 +112,8 @@ export default function eventCatalogIntegration(): AstroIntegration {
111
112
  }
112
113
 
113
114
  // Handle routes for MCP Server (requires SSR)
114
- if (isEventCatalogMCPEnabled()) {
115
+ // Also for canvases, so agents can join them (the canvas tools are on the MCP server)
116
+ if (isEventCatalogMCPEnabled() || isCanvasEnabled()) {
115
117
  params.injectRoute({
116
118
  pattern: '/docs/mcp/[...path]',
117
119
  entrypoint: path.join(packageDirectory, 'src/features/mcp/mcp-server.ts'),
@@ -173,6 +175,37 @@ export default function eventCatalogIntegration(): AstroIntegration {
173
175
  entrypoint: path.join(packageDirectory, 'src/features/visualizer-layout/reset.ts'),
174
176
  });
175
177
  }
178
+
179
+ // What a server starts before anyone asks for it (the storage configured, migrated; Studio), called by
180
+ // `eventcatalog start` once it listens
181
+ if (isSSR()) {
182
+ params.injectRoute({
183
+ pattern: '/_eventcatalog/start',
184
+ entrypoint: path.join(packageDirectory, 'src/features/server/start.ts'),
185
+ });
186
+ }
187
+
188
+ // Collaborative canvases in EventCatalog Studio (/studio lists them, /studio?new=1 starts one). Their WebSocket
189
+ // server is in integrations/studio-server.mjs.
190
+ if (isCanvasEnabled()) {
191
+ params.injectRoute({
192
+ pattern: '/studio',
193
+ entrypoint: path.join(packageDirectory, 'src/features/studio/pages/index.astro'),
194
+ });
195
+ params.injectRoute({
196
+ pattern: '/studio/[id]',
197
+ entrypoint: path.join(packageDirectory, 'src/features/studio/pages/[id].astro'),
198
+ });
199
+ // The Studio API (canvases as a whole: listing, creating, deleting)
200
+ params.injectRoute({
201
+ pattern: '/api/studio/canvases/[...path]',
202
+ entrypoint: path.join(packageDirectory, 'src/features/studio/api/canvases.ts'),
203
+ });
204
+ params.injectRoute({
205
+ pattern: '/webmcp-relay/[file]',
206
+ entrypoint: path.join(packageDirectory, 'src/features/studio/pages/webmcp-relay.ts'),
207
+ });
208
+ }
176
209
  },
177
210
  'astro:server:setup': ({ server }) => {
178
211
  // When custom page files appear or disappear, refresh the manifest.
@@ -6,7 +6,8 @@
6
6
  import { useEffect, useRef, useState, type FormEvent, type KeyboardEvent, type RefObject } from 'react';
7
7
  import type { Node } from '@xyflow/react';
8
8
  import { Check, Network, SendHorizontal, X } from 'lucide-react';
9
- import type { ArchitectureDiagramCollection } from '@utils/node-graphs/architecture-diagram-types';
9
+ import { isArchitectureDiagramCollection } from '@utils/node-graphs/architecture-diagram-types';
10
+ import { getNodeCatalogResource, getNodeResourceDetails } from '@utils/node-graphs/node-resource';
10
11
  import type { DiagramLink } from './links';
11
12
  import { RESOURCE_TYPE_LABELS } from '../shared/resource-types';
12
13
 
@@ -36,53 +37,13 @@ const NODE_TYPE_LABELS: Record<string, string> = {
36
37
  externalSystem: 'External system',
37
38
  };
38
39
 
39
- // Where each node type keeps the resource it shows
40
- const RESOURCE_DATA_KEYS = [
41
- 'service',
42
- 'agent',
43
- 'message',
44
- 'channel',
45
- 'data',
46
- 'dataProduct',
47
- 'flow',
48
- 'entity',
49
- 'domain',
50
- 'system',
51
- 'custom',
52
- ];
53
-
54
- // The collection of each node type the architecture diagram tool can draw a diagram for
55
- const DIAGRAM_COLLECTIONS: Record<string, ArchitectureDiagramCollection> = {
56
- services: 'services',
57
- agents: 'agents',
58
- events: 'events',
59
- commands: 'commands',
60
- queries: 'queries',
61
- flows: 'flows',
62
- data: 'containers',
63
- containers: 'containers',
64
- 'data-products': 'data-products',
65
- domains: 'domains',
66
- 'context-domain': 'domains',
67
- 'domain-group': 'domains',
68
- systems: 'systems',
69
- 'system-group': 'systems',
70
- };
71
-
72
- // Domain and system boundaries only have their catalog id in the node id: {kind}-group-{id}-{version}
73
- const catalogIdOfGroup = (node: Node, version?: string) => {
74
- const match = node.id.match(/^(?:domain|system)-group-(.+)$/);
75
- if (!match) return undefined;
76
- return version && match[1].endsWith(`-${version}`) ? match[1].slice(0, -(version.length + 1)) : match[1];
77
- };
78
-
79
40
  export function describeNode(node: Node): SelectedNode {
80
41
  const data = (node.data ?? {}) as Record<string, any>;
81
- const resource = RESOURCE_DATA_KEYS.map((key) => data[key]).find(Boolean) ?? data;
82
- const details = resource?.data ?? resource;
42
+ const details = (getNodeResourceDetails(node) ?? data) as Record<string, any>;
83
43
  const version: string | undefined = details?.version;
84
- const id: string = details?.id ?? catalogIdOfGroup(node, version) ?? node.id;
85
- const collection = DIAGRAM_COLLECTIONS[node.type ?? ''];
44
+ const id: string = details?.id ?? node.id;
45
+ const resource = getNodeCatalogResource(node);
46
+ const collection = resource && isArchitectureDiagramCollection(resource.collection) ? resource.collection : undefined;
86
47
 
87
48
  return {
88
49
  nodeId: node.id,
@@ -0,0 +1,47 @@
1
+ // Shared by the canvas MCP App (client) and the MCP server: no server-only imports here
2
+ import type { CatalogRelation, CatalogResource } from '../../../studio/catalog-resources';
3
+ import { type CanvasStatus } from '@eventcatalog/visualiser/studio';
4
+
5
+ export const CANVAS_RESOURCE_URI = 'ui://eventcatalog/canvas.html';
6
+ /** Key of the payload in openCanvas results' `_meta` */
7
+ export const CANVAS_META_KEY = 'eventcatalog/canvas';
8
+ /** App-only tool the view loads the canvas from when the host doesn't pass the result's `_meta` */
9
+ export const CANVAS_VIEW_TOOL = 'getCanvasView';
10
+ /** App-only tool the view loads the catalog (for dragging resources in) from */
11
+ export const CANVAS_CATALOG_TOOL = 'getCanvasCatalog';
12
+ /** App-only tool the view syncs through when the chat's sandbox blocks the collaboration WebSocket */
13
+ export const CANVAS_SYNC_TOOL = 'syncCanvas';
14
+
15
+ /** A canvas listed in the view's picker */
16
+ export type CanvasListItem = {
17
+ canvasId: string;
18
+ title?: string;
19
+ status: CanvasStatus;
20
+ people: string[];
21
+ nodeCount: number;
22
+ openComments: number;
23
+ };
24
+
25
+ /**
26
+ * What the view shows: a canvas (it connects to it itself, so people and agents edit it live), or, when opened
27
+ * without one (e.g. from ChatGPT's sidebar), a list of canvases to open or start.
28
+ */
29
+ export type CanvasPayload = CanvasView | CanvasHome;
30
+
31
+ export type CanvasHome = { view: 'home'; canvases: CanvasListItem[] };
32
+
33
+ export type CanvasView = {
34
+ view: 'canvas';
35
+ canvasId: string;
36
+ title?: string;
37
+ /** The collaboration WebSocket, e.g. wss://catalog.example.com/_eventcatalog/studio */
38
+ socketUrl: string;
39
+ /** The canvas in EventCatalog */
40
+ canvasUrl: string;
41
+ /** The EventCatalog MCP server, for "Connect your agent" */
42
+ mcpUrl: string;
43
+ /** The chat's agent: the view joins the canvas as it (its name, colour and logo) */
44
+ agent?: { name: string; color: string };
45
+ };
46
+
47
+ export type CanvasCatalog = { resources: CatalogResource[]; relations: CatalogRelation[] };
@@ -0,0 +1,24 @@
1
+ /*
2
+ * The canvas view's Tailwind stylesheet (with Tailwind's base reset) and EventCatalog's theme variables,
3
+ * built from the classes of the collaborative canvas components it renders.
4
+ */
5
+ @import 'tailwindcss' source(none);
6
+ @import '../../../../styles/theme.css';
7
+ @import '@eventcatalog/visualiser/studio.css';
8
+
9
+ @source './*.tsx';
10
+ @source '../../../studio/components/*.{ts,tsx}';
11
+ /* Studio's shared canvas (nodes, comments, hotspots), from the visualiser in this repo */
12
+ @source '../../../../../../../visualiser/src/studio';
13
+
14
+ body {
15
+ margin: 0;
16
+ background: rgb(var(--ec-page-bg));
17
+ color: rgb(var(--ec-page-text));
18
+ font-family:
19
+ ui-sans-serif,
20
+ system-ui,
21
+ -apple-system,
22
+ 'Segoe UI',
23
+ sans-serif;
24
+ }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * The collaborative canvas as an MCP App: openCanvas shows it in the chat (ChatGPT, Claude...), where it
3
+ * joins the canvas (over the collaboration WebSocket, or through tool calls when the chat's sandbox blocks it).
4
+ * The user edits it there with everyone else on it, people in EventCatalog and agents, live.
5
+ *
6
+ * Opened without a canvas (e.g. from ChatGPT's sidebar or a thread tab, its entrypoints), it lists the canvases
7
+ * to open one or start one. Follows the OpenAI MCP extensions for ChatGPT: entrypoints, display modes, deep
8
+ * links (/canvas/<id>) and model context (background context, and the selection as titled attachments).
9
+ *
10
+ * Built into a single HTML file by scripts/build-mcp-apps.mjs and served as a ui:// resource.
11
+ */
12
+ import { useCallback, useEffect, useMemo, useState } from 'react';
13
+ import { ArrowLeft, ExternalLink, Maximize2, Minimize2, Plus, Users } from 'lucide-react';
14
+ import type { App } from '@modelcontextprotocol/ext-apps';
15
+ import styles from './styles.css?inline';
16
+ import visualiserStyles from '@eventcatalog/visualiser/styles.css?inline';
17
+ import StudioDesigner, { type SelectedNode } from '@features/studio/components/StudioDesigner';
18
+ import { getAgent } from '@features/studio/agents';
19
+ import { CANVAS_STATUS_LOOK, STATUS } from '@eventcatalog/visualiser/studio';
20
+ import { getToolResultPayload, mountView, useMcpAppView } from '../shared/app-view';
21
+ import {
22
+ CANVAS_CATALOG_TOOL,
23
+ CANVAS_META_KEY,
24
+ CANVAS_SYNC_TOOL,
25
+ CANVAS_VIEW_TOOL,
26
+ type CanvasCatalog,
27
+ type CanvasHome,
28
+ type CanvasPayload,
29
+ type CanvasView,
30
+ } from './shared';
31
+ import type { SyncResponse, ToolSync } from '@features/studio/tool-sync';
32
+
33
+ const isCanvasPayload = (payload?: CanvasPayload): payload is CanvasPayload =>
34
+ payload?.view === 'home' ? Array.isArray(payload.canvases) : Boolean(payload?.canvasId && payload.socketUrl);
35
+
36
+ const VIEW_OPTIONS = {
37
+ name: 'EventCatalog canvas',
38
+ metaKey: CANVAS_META_KEY,
39
+ viewTool: CANVAS_VIEW_TOOL,
40
+ isPayload: isCanvasPayload,
41
+ getViewToolArguments: ({ canvasId }: Record<string, any>) => ({ canvasId }),
42
+ loadErrorMessage: 'The canvas could not be opened',
43
+ };
44
+
45
+ const INLINE_HEIGHT = 640;
46
+ /** Deep links into the app (ChatGPT's `openai/deepLink`), e.g. /canvas/<id> */
47
+ const DEEP_LINK = /^\/canvas(?:es)?\/([0-9a-f-]{36})/i;
48
+
49
+ /** Loads what to show through a tool (the canvas view's own, or openCanvas to start a canvas) */
50
+ const loadPayload = async (app: App, name: string, args: Record<string, unknown>) => {
51
+ const result = await app.callServerTool({ name, arguments: args });
52
+ const payload = getToolResultPayload<CanvasPayload>(result, CANVAS_META_KEY);
53
+ if (result.isError || !isCanvasPayload(payload)) throw new Error('The canvas could not be opened');
54
+ return payload;
55
+ };
56
+
57
+ function CanvasPicker({
58
+ home,
59
+ onOpen,
60
+ onCreate,
61
+ }: {
62
+ home: CanvasHome;
63
+ onOpen: (id: string) => void;
64
+ onCreate: (title: string) => void;
65
+ }) {
66
+ const [title, setTitle] = useState('');
67
+ return (
68
+ <div className="mx-auto max-w-2xl space-y-6 p-6">
69
+ <div className="space-y-1">
70
+ <h1 className="text-lg font-semibold">Canvases</h1>
71
+ <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
72
+ Work together, live: people in EventCatalog, people in chats and agents, on the same canvas.
73
+ </p>
74
+ </div>
75
+ <form
76
+ className="flex gap-2"
77
+ onSubmit={(event) => {
78
+ event.preventDefault();
79
+ if (title.trim()) onCreate(title.trim());
80
+ }}
81
+ >
82
+ <input
83
+ value={title}
84
+ onChange={(event) => setTitle(event.target.value)}
85
+ placeholder="What are you designing?"
86
+ className="min-w-0 flex-1 rounded-lg border px-3 py-2 text-sm bg-[rgb(var(--ec-input-bg))] border-[rgb(var(--ec-input-border))] text-[rgb(var(--ec-input-text))]"
87
+ />
88
+ <button
89
+ type="submit"
90
+ disabled={!title.trim()}
91
+ className="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium disabled:opacity-50 bg-[rgb(var(--ec-button-bg))] text-[rgb(var(--ec-button-text))] hover:bg-[rgb(var(--ec-button-bg-hover))]"
92
+ >
93
+ <Plus size={16} /> New canvas
94
+ </button>
95
+ </form>
96
+ <div className="space-y-2">
97
+ {home.canvases.length === 0 && <p className="text-sm text-[rgb(var(--ec-page-text-muted))]">No canvases yet.</p>}
98
+ {home.canvases.map((canvas) => (
99
+ <button
100
+ key={canvas.canvasId}
101
+ onClick={() => onOpen(canvas.canvasId)}
102
+ className="flex w-full items-center justify-between gap-3 rounded-xl border px-4 py-3 text-left bg-[rgb(var(--ec-card-bg))] border-[rgb(var(--ec-page-border))] hover:border-[rgb(var(--ec-accent))]"
103
+ >
104
+ <span className="min-w-0">
105
+ <span className="flex items-center gap-2">
106
+ <span className="truncate text-sm font-semibold">{canvas.title ?? 'Untitled canvas'}</span>
107
+ <span
108
+ className={`shrink-0 rounded-full border px-1.5 py-px text-[10px] font-semibold ${CANVAS_STATUS_LOOK[canvas.status].pill}`}
109
+ >
110
+ {CANVAS_STATUS_LOOK[canvas.status].label}
111
+ </span>
112
+ </span>
113
+ <span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
114
+ {canvas.nodeCount} {canvas.nodeCount === 1 ? 'node' : 'nodes'}
115
+ {canvas.openComments > 0 && ` · ${canvas.openComments} open comments`}
116
+ </span>
117
+ </span>
118
+ {canvas.people.length > 0 && (
119
+ <span
120
+ className={`flex shrink-0 items-center gap-1 text-xs ${STATUS.success.text}`}
121
+ title={canvas.people.join(', ')}
122
+ >
123
+ <Users size={14} /> {canvas.people.length} here now
124
+ </span>
125
+ )}
126
+ </button>
127
+ ))}
128
+ </div>
129
+ </div>
130
+ );
131
+ }
132
+
133
+ function CanvasApp() {
134
+ const { app, payload, setPayload, error, hostContext } = useMcpAppView(VIEW_OPTIONS);
135
+ const [catalog, setCatalog] = useState<CanvasCatalog>({ resources: [], relations: [] });
136
+ const [selection, setSelection] = useState<SelectedNode[]>([]);
137
+ const [loadError, setLoadError] = useState<string | null>(null);
138
+ const canvas = payload?.view === 'canvas' ? payload : undefined;
139
+
140
+ const show = useCallback(
141
+ (load: Promise<CanvasPayload>) =>
142
+ load.then(setPayload).catch((cause) => setLoadError(String((cause as Error)?.message ?? cause))),
143
+ [setPayload]
144
+ );
145
+ const openCanvas = (canvasId?: string) => app && show(loadPayload(app, CANVAS_VIEW_TOOL, canvasId ? { canvasId } : {}));
146
+
147
+ // Deep links (ChatGPT's `openai/deepLink`): /canvas/<id> opens that canvas
148
+ const deepLink = (hostContext as Record<string, { url?: string } | undefined> | undefined)?.['openai/deepLink']?.url;
149
+ useEffect(() => {
150
+ const canvasId = deepLink?.match(DEEP_LINK)?.[1];
151
+ if (app && canvasId && canvasId !== canvas?.canvasId) void show(loadPayload(app, CANVAS_VIEW_TOOL, { canvasId }));
152
+ }, [app, deepLink]);
153
+
154
+ // Chats may not let the view open the collaboration WebSocket (sandbox CSP, local network rules...):
155
+ // then it syncs through the MCP server, the same way the model's tools reach the canvas
156
+ const syncViaTools = useMemo<ToolSync | undefined>(
157
+ () =>
158
+ app
159
+ ? async (request) => {
160
+ const result = await app.callServerTool({ name: CANVAS_SYNC_TOOL, arguments: request });
161
+ if (result.isError || !result.structuredContent) throw new Error('Sync failed');
162
+ return result.structuredContent as SyncResponse;
163
+ }
164
+ : undefined,
165
+ [app]
166
+ );
167
+
168
+ // The catalog (to drag resources in) is loaded separately, to keep tool results small
169
+ useEffect(() => {
170
+ if (!app || !canvas) return;
171
+ app
172
+ .callServerTool({ name: CANVAS_CATALOG_TOOL, arguments: {} })
173
+ .then((result) => {
174
+ const loaded = result.structuredContent as CanvasCatalog | undefined;
175
+ if (loaded?.resources) setCatalog(loaded);
176
+ })
177
+ .catch(() => {});
178
+ }, [app, canvas?.canvasId]);
179
+
180
+ // Model context (replaced on each update): which canvas is open as background context the user doesn't see,
181
+ // and what they've selected as an attachment they can see (and remove), titled with the names
182
+ useEffect(() => {
183
+ if (!app || !payload || !app.getHostCapabilities()?.updateModelContext) return;
184
+ const background = {
185
+ type: 'text' as const,
186
+ text: canvas
187
+ ? `The user has the canvas "${canvas.title ?? 'Untitled'}" open (canvasId ${canvas.canvasId}). Use getCanvas to see what is on it, and the canvas tools to work on it with them.`
188
+ : 'The user is looking at their canvases, to open one or start one.',
189
+ annotations: { audience: ['assistant' as const] },
190
+ };
191
+ const selected = selection.length
192
+ ? [
193
+ {
194
+ type: 'text' as const,
195
+ text: `Selected on the canvas: ${selection
196
+ .map(
197
+ (node) =>
198
+ `${node.name} (${node.type}${node.catalogResource ? `, catalog ${node.catalogResource}` : ''}, nodeId ${node.id})`
199
+ )
200
+ .join('; ')}`,
201
+ _meta: { 'openai/title': selection.map((node) => node.name).join(', ') },
202
+ },
203
+ ]
204
+ : [];
205
+ app.updateModelContext({ content: [background, ...selected] }).catch(() => {});
206
+ }, [app, payload, selection]);
207
+
208
+ if (error || loadError) return <p className="p-4 text-sm">{error ?? loadError}</p>;
209
+ if (!payload) return <p className="p-4 text-sm text-[rgb(var(--ec-page-text-muted))]">Opening the canvas…</p>;
210
+
211
+ const fullscreen = hostContext?.displayMode === 'fullscreen';
212
+ const canToggle = hostContext?.availableDisplayModes?.includes(fullscreen ? 'inline' : 'fullscreen');
213
+ const iconButton =
214
+ 'flex items-center gap-1 rounded-md px-2 py-1 text-xs text-[rgb(var(--ec-page-text-muted))] hover:bg-[rgb(var(--ec-page-border)/0.5)]';
215
+
216
+ // No join form inside a chat: it joins as the chat's agent (e.g. ChatGPT or Claude, with its logo), as the agent
217
+ // working on the canvas does, else as the chat it's in
218
+ const hostName = app?.getHostVersion()?.name;
219
+ const agentOf = (view: CanvasView) => view.agent ?? getAgent(undefined, hostName);
220
+
221
+ return (
222
+ <div className="flex flex-col" style={{ height: fullscreen ? '100vh' : INLINE_HEIGHT }}>
223
+ <div className="flex items-center justify-between border-b px-3 py-1.5 border-[rgb(var(--ec-page-border))]">
224
+ <span className="flex min-w-0 items-center gap-1">
225
+ {canvas && (
226
+ <button className={iconButton} title="All canvases" onClick={() => void openCanvas()}>
227
+ <ArrowLeft size={14} />
228
+ </button>
229
+ )}
230
+ <span className="truncate text-sm font-semibold">{canvas ? (canvas.title ?? 'Canvas') : 'Canvases'}</span>
231
+ </span>
232
+ <span className="flex items-center gap-1">
233
+ {canvas && (
234
+ <button className={iconButton} onClick={() => app?.openLink({ url: canvas.canvasUrl })}>
235
+ <ExternalLink size={14} />
236
+ Open in EventCatalog
237
+ </button>
238
+ )}
239
+ {canToggle && (
240
+ <button
241
+ className={iconButton}
242
+ title={fullscreen ? 'Exit full screen' : 'Full screen'}
243
+ onClick={() => app?.requestDisplayMode({ mode: fullscreen ? 'inline' : 'fullscreen' })}
244
+ >
245
+ {fullscreen ? <Minimize2 size={14} /> : <Maximize2 size={14} />}
246
+ </button>
247
+ )}
248
+ </span>
249
+ </div>
250
+ <div className="min-h-0 flex-1 overflow-y-auto">
251
+ {canvas ? (
252
+ <StudioDesigner
253
+ key={canvas.canvasId}
254
+ canvasId={canvas.canvasId}
255
+ socketUrl={canvas.socketUrl}
256
+ shareUrl={canvas.canvasUrl}
257
+ mcpUrl={canvas.mcpUrl}
258
+ syncViaTools={syncViaTools}
259
+ asAgent={agentOf(canvas)}
260
+ resources={catalog.resources}
261
+ relations={catalog.relations}
262
+ onSelectionChange={setSelection}
263
+ followChanges
264
+ paletteCollapsed
265
+ />
266
+ ) : (
267
+ <CanvasPicker
268
+ home={payload as CanvasHome}
269
+ onOpen={(canvasId) => void openCanvas(canvasId)}
270
+ onCreate={(title) => app && void show(loadPayload(app, 'openCanvas', { title }))}
271
+ />
272
+ )}
273
+ </div>
274
+ </div>
275
+ );
276
+ }
277
+
278
+ mountView(CanvasApp, [visualiserStyles, styles].join('\n'));