@eventcatalog/core 5.4.2 → 5.5.1

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 (53) 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/catalog-runtime.cjs +1 -1
  6. package/dist/{chunk-VSBMYXLL.js → chunk-6KR5ZLEQ.js} +1 -1
  7. package/dist/{chunk-XB6FJMI3.js → chunk-EBZQJP7F.js} +1 -1
  8. package/dist/{chunk-HV4ONKTM.js → chunk-GPIKYBQ5.js} +1 -1
  9. package/dist/{chunk-EXRIIYEP.js → chunk-GZLVDQPM.js} +1 -1
  10. package/dist/{chunk-CQ4L4SMB.js → chunk-K5QKWVI3.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +2 -2
  14. package/dist/eventcatalog.js +14 -14
  15. package/dist/federation/federate.js +3 -3
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/utils/cli-logger.cjs +1 -1
  19. package/dist/utils/cli-logger.js +2 -2
  20. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +2 -0
  21. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +5 -0
  22. package/eventcatalog/src/components/SchemaExplorer/AvroSchemaViewer.tsx +5 -3
  23. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +6 -5
  24. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.tsx +6 -5
  25. package/eventcatalog/src/features/ai/chat-api.ts +6 -3
  26. package/eventcatalog/src/features/mcp/apps/architecture-diagram/shared.ts +3 -1
  27. package/eventcatalog/src/features/mcp/apps/architecture-diagram/view.tsx +6 -1
  28. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +80 -68
  29. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +87 -73
  30. package/eventcatalog/src/features/mcp/mcp-apps.ts +1 -1
  31. package/eventcatalog/src/features/mcp/mcp-scoped-tools.ts +2 -1
  32. package/eventcatalog/src/features/mcp/mcp-server.ts +14 -12
  33. package/eventcatalog/src/features/mcp/schema-viewer.ts +3 -3
  34. package/eventcatalog/src/features/studio/components/StudioDesigner.tsx +2 -0
  35. package/eventcatalog/src/features/studio/server/canvas-mcp.ts +1 -1
  36. package/eventcatalog/src/features/tools/catalog-tools.ts +143 -75
  37. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/entity-map/index.astro +1 -0
  38. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +1 -0
  39. package/eventcatalog/src/pages/visualiser/_mermaid-route.ts +48 -5
  40. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +1 -0
  41. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/[level].mermaid.ts +18 -0
  42. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +1 -0
  43. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/index.astro +1 -0
  44. package/eventcatalog/src/pages/visualiser/system-context-map/index.astro +1 -0
  45. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/[level].mermaid.ts +18 -0
  46. package/eventcatalog/src/pages/visualiser/systems/[id]/[version]/index.astro +1 -0
  47. package/eventcatalog/src/utils/node-graphs/architecture-diagram-types.ts +7 -1
  48. package/eventcatalog/src/utils/node-graphs/architecture-diagram.ts +5 -2
  49. package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +2 -0
  50. package/eventcatalog/src/utils/node-graphs/domain-levels-node-graph.ts +246 -29
  51. package/package.json +5 -5
  52. package/eventcatalog/src/utils/node-graphs/export-mermaid.ts +0 -375
  53. package/dist/{chunk-QJ7KKN2D.js → chunk-NCEZDQSW.js} +7 -7
@@ -93,7 +93,7 @@ export function registerMcpApp<Shape extends z.ZodRawShape, ViewShape extends z.
93
93
  if ('error' in result) return errorResult(result);
94
94
  return {
95
95
  // The model reads the text; hosts that support MCP Apps show the view from `_meta`
96
- content: [{ type: 'text' as const, text: JSON.stringify(result.forModel, null, 2) }],
96
+ content: [{ type: 'text' as const, text: JSON.stringify(result.forModel) }],
97
97
  ...(loadView && {
98
98
  _meta: {
99
99
  [app.metaKey]: await result.getPayload(),
@@ -12,6 +12,7 @@ import {
12
12
  paginate,
13
13
  } from '@features/tools/catalog-tools';
14
14
  import type { McpScope } from './mcp-scope';
15
+ import type { ArchitectureDiagramDetail } from '@utils/node-graphs/architecture-diagram-types';
15
16
 
16
17
  const notFound = (collection: string, id: string, version?: string) => ({
17
18
  error: `Resource not found: ${collection}/${id}${version ? ` (${version})` : ''}`,
@@ -186,7 +187,7 @@ export function createScopedCatalogTools(scope: McpScope) {
186
187
  resourceId: string;
187
188
  resourceVersion?: string;
188
189
  resourceCollection: string;
189
- detail?: 'overview' | 'full';
190
+ detail?: ArchitectureDiagramDetail;
190
191
  }) {
191
192
  const entry = scope.find(params.resourceCollection, params.resourceId, params.resourceVersion);
192
193
  if (!entry) return notFound(params.resourceCollection, params.resourceId, params.resourceVersion);
@@ -11,6 +11,7 @@ import { McpScopeNotFoundError, resolveMcpScope, type McpScope, type McpScopeKin
11
11
  import { getArchitectureDiagramView } from '@utils/node-graphs/architecture-diagram';
12
12
  import type {
13
13
  ArchitectureDiagramCollection,
14
+ ArchitectureDiagramDetail,
14
15
  ArchitectureDiagramView,
15
16
  Graph,
16
17
  } from '@utils/node-graphs/architecture-diagram-types';
@@ -30,7 +31,7 @@ const loadViewerView = getMcpAppViewLoader('viewer');
30
31
 
31
32
  const SHOW_RESOURCE_DESCRIPTION = [
32
33
  "Show the user a resource in EventCatalog's interactive viewer. Pick what to show with view:",
33
- '"architecture" shows an architecture diagram of a domain, system, service, agent, event, command, query, flow, data store or data product: what it connects to, what it publishes and consumes, and the systems and domains around it. Use it whenever the user wants to see or understand how something works or fits together, e.g. "show me how the Order Service works", "how does the Ordering domain fit together", "what does Payments talk to", or asks for a diagram, map or visual of something. Prefer it over drawing your own diagram. For domains and systems, pass detail "overview" for just the domains, systems and their relationships, or "full" (default) to include services, messages and channels.',
34
+ '"architecture" shows an architecture diagram of a domain, system, service, agent, event, command, query, flow, data store or data product: what it connects to, what it publishes and consumes, and the systems and domains around it. Use it whenever the user wants to see or understand how something works or fits together, e.g. "show me how the Order Service works", "how does the Ordering domain fit together", "what does Payments talk to", or asks for a diagram, map or visual of something. Prefer it over drawing your own diagram. Pick the level with detail: "overview" for just the domains, systems and their relationships (domains and systems), "services" for services and data stores without messages and channels, or "full" (default) to include messages and channels.',
34
35
  '"schema" shows the schema of a message (event, command or query) or other resource: its properties, types, descriptions and which ones are required. Use it whenever the user wants to see or understand a schema or payload, e.g. "show me the schema for OrderCreated", "what fields does PlaceOrder have", "what does the OrderShipped payload look like".',
35
36
  'Clients that support MCP Apps show one viewer and switch it to each thing you show, so call this again to show the user something else. The result also contains what is shown (the diagram as Mermaid in mermaidCode, or the schema code) so you can understand it.',
36
37
  'When the client shows the viewer, do not redraw the diagram or print the whole schema again in your reply: explain it and point out what matters. Otherwise, show the user the mermaidCode or the relevant parts of the schema code.',
@@ -47,7 +48,8 @@ type McpServerOptions = {
47
48
 
48
49
  const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
49
50
 
50
- // Helper to create consistent MCP tool handlers with error handling
51
+ // Helper to create consistent MCP tool handlers with error handling. Results are compact JSON: indenting them costs
52
+ // the model about a fifth more tokens for nothing
51
53
  function createToolHandler<T>(fn: (params: T) => Promise<any>, errorMessage: string) {
52
54
  return async (params: T) => {
53
55
  try {
@@ -61,7 +63,7 @@ function createToolHandler<T>(fn: (params: T) => Promise<any>, errorMessage: str
61
63
  }
62
64
 
63
65
  return {
64
- content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
66
+ content: [{ type: 'text' as const, text: JSON.stringify(result) }],
65
67
  };
66
68
  } catch (error) {
67
69
  return {
@@ -389,6 +391,7 @@ function createMcpServer(
389
391
  resourceId: string;
390
392
  resourceVersion: string;
391
393
  visualiserUrl: string;
394
+ detail?: ArchitectureDiagramDetail;
392
395
  }): Promise<ArchitectureDiagramPayload> => ({
393
396
  resource: {
394
397
  collection: diagram.resourceCollection,
@@ -405,6 +408,7 @@ function createMcpServer(
405
408
  version: diagram.resourceVersion,
406
409
  })
407
410
  ),
411
+ ...(diagram.detail ? { detail: diagram.detail } : {}),
408
412
  });
409
413
 
410
414
  // The resource's schemas, parsed for the view's JSON Schema, Avro and Protobuf viewers
@@ -455,7 +459,7 @@ function createMcpServer(
455
459
  resourceId: string;
456
460
  resourceVersion?: string;
457
461
  resourceCollection: string;
458
- detail?: 'overview' | 'full';
462
+ detail?: ArchitectureDiagramDetail;
459
463
  };
460
464
 
461
465
  const loadArchitectureView = async ({ view, ...params }: ResourceViewParams) => {
@@ -470,7 +474,8 @@ function createMcpServer(
470
474
  label: `the architecture diagram for ${result.resourceCollection}/${result.resourceId}`,
471
475
  getPayload: async (): Promise<ViewerPayload> => ({
472
476
  view: 'architecture',
473
- diagram: await getArchitectureDiagramPayload(result),
477
+ // Opens at the level asked for, as the Mermaid the model gets is; without one, at the diagram's default
478
+ diagram: await getArchitectureDiagramPayload({ ...result, detail: params.detail }),
474
479
  }),
475
480
  };
476
481
  };
@@ -517,12 +522,9 @@ function createMcpServer(
517
522
  description: SHOW_RESOURCE_DESCRIPTION,
518
523
  input: {
519
524
  ...resourceViewInput,
520
- detail: z
521
- .enum(['overview', 'full'])
525
+ detail: catalogTools.architectureDiagramDetailSchema
522
526
  .optional()
523
- .describe(
524
- 'For view "architecture" of domains and systems: "overview" shows only domains, systems and their relationships; "full" (default) also shows services, messages and channels'
525
- ),
527
+ .describe(`For view "architecture": ${catalogTools.architectureDiagramDetailSchema.description}`),
526
528
  },
527
529
  },
528
530
  viewTool: {
@@ -664,7 +666,7 @@ function createMcpServer(
664
666
  try {
665
667
  const result = await execute(params);
666
668
  return {
667
- content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
669
+ content: [{ type: 'text' as const, text: JSON.stringify(result) }],
668
670
  };
669
671
  } catch (error) {
670
672
  return {
@@ -839,7 +841,7 @@ function createMcpServer(
839
841
  contents: [
840
842
  {
841
843
  uri: uri.href,
842
- text: JSON.stringify({ ...(scope && { scope: scope.ref }), resources: allResources }, null, 2),
844
+ text: JSON.stringify({ ...(scope && { scope: scope.ref }), resources: allResources }),
843
845
  mimeType: 'application/json',
844
846
  },
845
847
  ],
@@ -3,7 +3,7 @@ import { getLanguageForHighlight } from '@components/SchemaExplorer/utils';
3
3
  import { getSchemaExtensionForFormat } from '@utils/collections/schemas';
4
4
  import type { SchemaViewerSchema } from './apps/schema-viewer/shared';
5
5
 
6
- type SchemaToolResult = { name?: string; format?: string; url?: string; code: string };
6
+ type SchemaToolResult = { name?: string; format?: string; path?: string; code: string };
7
7
 
8
8
  /**
9
9
  * The schemas returned by getSchemaForResource, ready for the schema viewer: parsed for its Properties tab
@@ -12,11 +12,11 @@ type SchemaToolResult = { name?: string; format?: string; url?: string; code: st
12
12
  export const toSchemaViewerSchemas = (schemas: SchemaToolResult[]): SchemaViewerSchema[] =>
13
13
  schemas.map((schema) => {
14
14
  // Schemas in the schemas collection have a format (jsonschema, avro, protobuf), others a file extension
15
- const format = schema.format ?? schema.url?.split('.').pop() ?? '';
15
+ const format = schema.format ?? schema.path?.split('.').pop() ?? '';
16
16
  const extension = getSchemaExtensionForFormat(format);
17
17
  return {
18
18
  ...parseSchemaForViewer(extension, schema.code),
19
- ...(schemas.length > 1 && { name: schema.name ?? schema.url?.split('/').pop() ?? format }),
19
+ ...(schemas.length > 1 && { name: schema.name ?? schema.path?.split('/').pop() ?? format }),
20
20
  format,
21
21
  language: getLanguageForHighlight(extension),
22
22
  code: schema.code,
@@ -97,6 +97,7 @@ import {
97
97
  type HotspotFields,
98
98
  type HotspotSeverity,
99
99
  type HotspotSpot,
100
+ RemeasureHandles,
100
101
  } from '@eventcatalog/visualiser/studio';
101
102
  import { X } from 'lucide-react';
102
103
  import type { CatalogRelation, CatalogResource } from '../catalog-resources';
@@ -1751,6 +1752,7 @@ function Canvas({
1751
1752
  maxZoom={2}
1752
1753
  >
1753
1754
  <DiagramBackground />
1755
+ <RemeasureHandles />
1754
1756
  {/* Above the selected node's top right corner: its details, to edit them */}
1755
1757
  {toEdit && <EditButton nodeId={toEdit.id} locked={locked} onClick={editSelected} />}
1756
1758
  <LegendPanel legend={legend} hiddenKeys={hiddenLegendKeys} onLegendClick={toggleLegendKey} />
@@ -52,7 +52,7 @@ const CANVAS_ICON_SVG =
52
52
  const CANVAS_ICON = { src: `data:image/svg+xml;base64,${btoa(CANVAS_ICON_SVG)}`, mimeType: 'image/svg+xml', sizes: ['20x20'] };
53
53
 
54
54
  const text = (value: unknown, isError = false) => ({
55
- content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value, null, 2) }],
55
+ content: [{ type: 'text' as const, text: typeof value === 'string' ? value : JSON.stringify(value) }],
56
56
  ...(isError && { isError }),
57
57
  });
58
58
 
@@ -4,18 +4,19 @@
4
4
  */
5
5
  import { getCollection } from 'astro:content';
6
6
  import { z } from 'zod';
7
- import { getSchemasFromResource, getSchemaFormatFromURL } from '@utils/collections/schemas';
8
- import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
7
+ import { getSchemaFormatFromURL } from '@utils/collections/schemas';
8
+ import { getItemsFromCollectionByIdAndSemverOrLatest, processSpecifications, versionMatches } from '@utils/collections/util';
9
9
  import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains';
10
10
  import { getAbsoluteFilePathForAstroFile } from '@utils/files';
11
11
  import fs from 'node:fs';
12
12
  import { getArchitectureDiagramGraph } from '@utils/node-graphs/architecture-diagram';
13
13
  import {
14
14
  ARCHITECTURE_DIAGRAM_COLLECTIONS,
15
+ ARCHITECTURE_DIAGRAM_DETAILS,
15
16
  isArchitectureDiagramCollection,
16
17
  type ArchitectureDiagramDetail,
17
18
  } from '@utils/node-graphs/architecture-diagram-types';
18
- import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
19
+ import { convertToMermaid } from '@eventcatalog/visualiser/layout';
19
20
  import { glob } from 'glob';
20
21
  import path from 'node:path';
21
22
  import {
@@ -201,6 +202,12 @@ export const resourceCollectionSchema = z.enum([
201
202
 
202
203
  export const visualiserCollectionSchema = z.enum(ARCHITECTURE_DIAGRAM_COLLECTIONS);
203
204
 
205
+ export const architectureDiagramDetailSchema = z
206
+ .enum(ARCHITECTURE_DIAGRAM_DETAILS)
207
+ .describe(
208
+ 'The level of the diagram, as on its Diagram page: "overview" (L1) shows only domains, systems and their relationships (domains and systems only); "services" (L2) shows services and data stores without messages and channels; "full" (L3, default) also shows messages and channels'
209
+ );
210
+
204
211
  // ============================================
205
212
  // Tool implementations (core logic)
206
213
  // ============================================
@@ -273,6 +280,74 @@ export async function getResource(params: { collection: string; id: string; vers
273
280
 
274
281
  type MessagePointer = { id: string; version?: string };
275
282
 
283
+ type MessageEntry = { collection: string; data: { id: string; version: string } };
284
+
285
+ /** Resources that send and receive messages, and the collections that hold them */
286
+ const MESSAGING_COLLECTIONS = ['services', 'agents', 'domains'] as const;
287
+
288
+ /** Old versions live in a versioned folder: what they send and receive is history, not the catalog today */
289
+ const isLatestVersion = (entry: { filePath?: string }) => !entry.filePath?.includes('versioned');
290
+
291
+ /**
292
+ * Whether a sends/receives pointer points at this version of a message, as on the message's pages: one without a
293
+ * version (or "latest") points at the latest version only; others match exactly, by semver range or by x-pattern (1.x)
294
+ */
295
+ const pointsAtMessage = (pointer: MessagePointer, message: MessageEntry, latestVersion: string) => {
296
+ if (pointer.id !== message.data.id) return false;
297
+ if (!pointer.version || pointer.version === 'latest') return message.data.version === latestVersion;
298
+ return versionMatches(message.data.version, pointer.version);
299
+ };
300
+
301
+ /** The services, agents and domains (latest versions) that send (or receive) this version of a message */
302
+ const getResourcesThat = async (direction: 'sends' | 'receives', message: MessageEntry) => {
303
+ const [latestMessage, ...collections] = await Promise.all([
304
+ findEntry(message.collection, message.data.id),
305
+ ...MESSAGING_COLLECTIONS.map((collection) => getCollection(collection)),
306
+ ]);
307
+ const latestVersion = latestMessage?.data.version ?? message.data.version;
308
+
309
+ return collections
310
+ .flat()
311
+ .filter(isLatestVersion)
312
+ .filter((resource) =>
313
+ ((resource.data as any)[direction] || []).some((pointer: MessagePointer) =>
314
+ pointsAtMessage(pointer, message, latestVersion)
315
+ )
316
+ );
317
+ };
318
+
319
+ /**
320
+ * The resources a domain or system is made of, with it: its services and agents, and for a domain its systems and
321
+ * subdomains (and everything inside them)
322
+ */
323
+ const getResourcesInside = async (container: any) => {
324
+ const [domains, systems, services, agents] = await Promise.all(
325
+ (['domains', 'systems', 'services', 'agents'] as const).map((collection) => getCollection(collection))
326
+ );
327
+ const resolve = (entries: any[], pointers: MessagePointer[] = []) =>
328
+ pointers
329
+ .map((pointer) => getItemsFromCollectionByIdAndSemverOrLatest(entries, pointer.id, pointer.version)[0])
330
+ .filter(Boolean);
331
+
332
+ const found = new Map<string, any>();
333
+ const visit = (entry: any) => {
334
+ const key = `${entry.collection}/${entry.data.id}`;
335
+ if (found.has(key)) return;
336
+ found.set(key, entry);
337
+ if (entry.collection !== 'domains' && entry.collection !== 'systems') return;
338
+ [
339
+ ...resolve(services, entry.data.services),
340
+ ...resolve(agents, entry.data.agents),
341
+ ...(entry.collection === 'domains'
342
+ ? [...resolve(systems, entry.data.systems), ...resolve(domains, entry.data.domains)]
343
+ : []),
344
+ ].forEach(visit);
345
+ };
346
+ visit(container);
347
+
348
+ return [...found.values()];
349
+ };
350
+
276
351
  /**
277
352
  * Resolve a sends/receives pointer to the documented message it points at
278
353
  */
@@ -314,17 +389,39 @@ export async function getMessagesProducedOrConsumedByResource(params: {
314
389
  getCollection('queries'),
315
390
  ]);
316
391
  const messages = [...events, ...commands, ...queries];
392
+ const summary = {
393
+ id: resource.data.id,
394
+ version: resource.data.version,
395
+ name: resource.data.name || resource.data.id,
396
+ collection: params.resourceCollection,
397
+ };
317
398
 
318
- return {
319
- resource: {
320
- id: resource.data.id,
321
- version: resource.data.version,
322
- name: resource.data.name || resource.data.id,
323
- collection: params.resourceCollection,
324
- },
325
- sends: (resource.data.sends || []).map((pointer: MessagePointer) => resolveMessagePointer(messages, pointer)),
326
- receives: (resource.data.receives || []).map((pointer: MessagePointer) => resolveMessagePointer(messages, pointer)),
399
+ if (params.resourceCollection !== 'domains' && params.resourceCollection !== 'systems') {
400
+ return {
401
+ resource: summary,
402
+ sends: (resource.data.sends || []).map((pointer: MessagePointer) => resolveMessagePointer(messages, pointer)),
403
+ receives: (resource.data.receives || []).map((pointer: MessagePointer) => resolveMessagePointer(messages, pointer)),
404
+ };
405
+ }
406
+
407
+ // A domain or system sends and receives what it and everything inside it does: `by` names who, e.g. services/orders
408
+ const members = await getResourcesInside(resource);
409
+ const collect = (direction: 'sends' | 'receives') => {
410
+ const found = new Map<string, ReturnType<typeof resolveMessagePointer> & { by: string[] }>();
411
+ for (const member of members) {
412
+ for (const pointer of (member.data[direction] || []) as MessagePointer[]) {
413
+ const message = resolveMessagePointer(messages, pointer);
414
+ const key = `${message.collection ?? ''}/${message.id}@${message.version}`;
415
+ const entry = found.get(key) ?? { ...message, by: [] };
416
+ const by = `${member.collection}/${member.data.id}`;
417
+ if (!entry.by.includes(by)) entry.by.push(by);
418
+ found.set(key, entry);
419
+ }
420
+ }
421
+ return [...found.values()];
327
422
  };
423
+
424
+ return { resource: summary, sends: collect('sends'), receives: collect('receives') };
328
425
  }
329
426
 
330
427
  /**
@@ -360,15 +457,27 @@ export async function getSchemaForResource(params: { resourceId: string; resourc
360
457
  }
361
458
  }
362
459
 
363
- const schema = await getSchemasFromResource(resource);
460
+ const files = [
461
+ ...(resource.data.schemaPath
462
+ ? [{ path: resource.data.schemaPath, format: getSchemaFormatFromURL(resource.data.schemaPath) }]
463
+ : []),
464
+ ...processSpecifications(resource.data.specifications).map((spec) => ({
465
+ name: spec.name,
466
+ path: spec.path,
467
+ format: spec.type,
468
+ })),
469
+ ];
470
+
471
+ // Read from beside the resource's own file (paths are relative to it); files that can't be read are left out
472
+ const schemas = files.flatMap((file) => {
473
+ try {
474
+ return [{ ...file, code: fs.readFileSync(getAbsoluteFilePathForAstroFile(resource.filePath ?? '', file.path), 'utf-8') }];
475
+ } catch {
476
+ return [];
477
+ }
478
+ });
364
479
 
365
- if (schema.length > 0) {
366
- return schema.map((schemaItem) => ({
367
- url: schemaItem.url,
368
- format: schemaItem.format,
369
- code: fs.readFileSync(schemaItem.url, 'utf-8'),
370
- }));
371
- }
480
+ if (schemas.length > 0) return schemas;
372
481
 
373
482
  return { message: 'No schemas found for this resource' };
374
483
  }
@@ -424,8 +533,6 @@ export async function findResourcesByOwner(params: { ownerId: string }) {
424
533
  * Get resources that produce (send) a specific message
425
534
  */
426
535
  export async function getProducersOfMessage(params: { messageId: string; messageVersion?: string; messageCollection: string }) {
427
- const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
428
- const routableResources = [...services, ...agents];
429
536
  const message = await findEntry(params.messageCollection, params.messageId, params.messageVersion);
430
537
 
431
538
  if (!message) {
@@ -433,21 +540,13 @@ export async function getProducersOfMessage(params: { messageId: string; message
433
540
  error: `Message not found: ${params.messageId} (${describeVersion(params.messageVersion)}) in ${params.messageCollection}`,
434
541
  };
435
542
  }
436
- const messageVersion = message.data.version;
437
543
 
438
- const producers = routableResources.filter((resource) => {
439
- const sends = (resource.data as any).sends || [];
440
- return sends.some((send: any) => {
441
- const idMatch = send.id === params.messageId;
442
- if (!send.version || send.version === 'latest') return idMatch;
443
- return idMatch && send.version === messageVersion;
444
- });
445
- });
544
+ const producers = await getResourcesThat('sends', message);
446
545
 
447
546
  return {
448
547
  message: {
449
548
  id: params.messageId,
450
- version: messageVersion,
549
+ version: message.data.version,
451
550
  collection: params.messageCollection,
452
551
  },
453
552
  producers: producers.map((s) => ({
@@ -504,8 +603,6 @@ export async function getC4Diagram(params: { viewId?: string }) {
504
603
  * Get resources that consume (receive) a specific message
505
604
  */
506
605
  export async function getConsumersOfMessage(params: { messageId: string; messageVersion?: string; messageCollection: string }) {
507
- const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
508
- const routableResources = [...services, ...agents];
509
606
  const message = await findEntry(params.messageCollection, params.messageId, params.messageVersion);
510
607
 
511
608
  if (!message) {
@@ -513,21 +610,13 @@ export async function getConsumersOfMessage(params: { messageId: string; message
513
610
  error: `Message not found: ${params.messageId} (${describeVersion(params.messageVersion)}) in ${params.messageCollection}`,
514
611
  };
515
612
  }
516
- const messageVersion = message.data.version;
517
613
 
518
- const consumers = routableResources.filter((resource) => {
519
- const receives = (resource.data as any).receives || [];
520
- return receives.some((receive: any) => {
521
- const idMatch = receive.id === params.messageId;
522
- if (!receive.version || receive.version === 'latest') return idMatch;
523
- return idMatch && receive.version === messageVersion;
524
- });
525
- });
614
+ const consumers = await getResourcesThat('receives', message);
526
615
 
527
616
  return {
528
617
  message: {
529
618
  id: params.messageId,
530
- version: messageVersion,
619
+ version: message.data.version,
531
620
  collection: params.messageCollection,
532
621
  },
533
622
  consumers: consumers.map((s) => ({
@@ -545,8 +634,6 @@ export async function getConsumersOfMessage(params: { messageId: string; message
545
634
  * Returns all affected services (producers and consumers) and their owners
546
635
  */
547
636
  export async function analyzeChangeImpact(params: { messageId: string; messageVersion?: string; messageCollection: string }) {
548
- const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
549
- const routableResources = [...services, ...agents];
550
637
  const message = await findEntry(params.messageCollection, params.messageId, params.messageVersion);
551
638
 
552
639
  if (!message) {
@@ -556,17 +643,7 @@ export async function analyzeChangeImpact(params: { messageId: string; messageVe
556
643
  }
557
644
  const messageVersion = message.data.version;
558
645
 
559
- // Find producers
560
- const producers = routableResources.filter((resource) => {
561
- const sends = (resource.data as any).sends || [];
562
- return sends.some((send: any) => send.id === params.messageId);
563
- });
564
-
565
- // Find consumers
566
- const consumers = routableResources.filter((resource) => {
567
- const receives = (resource.data as any).receives || [];
568
- return receives.some((receive: any) => receive.id === params.messageId);
569
- });
646
+ const [producers, consumers] = await Promise.all([getResourcesThat('sends', message), getResourcesThat('receives', message)]);
570
647
 
571
648
  // Collect all affected teams/owners
572
649
  const affectedOwners = new Set<string>();
@@ -789,19 +866,10 @@ export async function findMessageBySchemaId(params: {
789
866
  const matches = getItemsFromCollectionByIdAndSemverOrLatest(collection, params.messageId, params.messageVersion);
790
867
  const resource = matches[0];
791
868
  if (resource) {
792
- // Get producers and consumers
793
- const [services, agents] = await Promise.all([getCollection('services'), getCollection('agents')]);
794
- const routableResources = [...services, ...agents];
795
-
796
- const producers = routableResources.filter((resource) => {
797
- const sends = (resource.data as any).sends || [];
798
- return sends.some((send: any) => send.id === params.messageId);
799
- });
800
-
801
- const consumers = routableResources.filter((resource) => {
802
- const receives = (resource.data as any).receives || [];
803
- return receives.some((receive: any) => receive.id === params.messageId);
804
- });
869
+ const [producers, consumers] = await Promise.all([
870
+ getResourcesThat('sends', resource),
871
+ getResourcesThat('receives', resource),
872
+ ]);
805
873
 
806
874
  return {
807
875
  resource: {
@@ -1258,23 +1326,23 @@ export async function getCustomDoc(params: { id: string; section?: string }) {
1258
1326
 
1259
1327
  export const toolDescriptions = {
1260
1328
  getResources:
1261
- 'Use this tool to get events, agents, services, commands, queries, flows, domains, channels, entities from EventCatalog. Supports pagination via cursor and filtering by search term (searches name, id, and summary).',
1329
+ 'Use this tool to list the resources in a collection of EventCatalog (events, commands, queries, services, agents, domains, systems, flows, channels, entities, containers, data products, diagrams or ADRs). Supports pagination via cursor and filtering by search term (searches name, id, and summary).',
1262
1330
  getResource:
1263
1331
  'Use this tool to get a specific resource from EventCatalog by its id. Returns the frontmatter and markdown documentation. Omit the version to get the latest version.',
1264
1332
  getMessagesProducedOrConsumedByResource:
1265
- 'Use this tool to get the messages (events, commands, queries) a service, agent or domain sends and receives. Each message is resolved to its catalog id, version, name and summary; messages that are referenced but not documented in the catalog are marked notFound. Omit the version to use the latest version of the resource.',
1333
+ 'Use this tool to get the messages (events, commands, queries) a service, agent, domain or system sends and receives. Each message is resolved to its catalog id, version, name and summary; messages that are referenced but not documented in the catalog are marked notFound. For a domain or system, this includes what the services, agents, systems and subdomains inside it send and receive, and each message lists `by`: the resources that send or receive it (e.g. services/order-service). Omit the version to use the latest version of the resource.',
1266
1334
  getSchemaForResource:
1267
1335
  'Use this tool to get the schema or specifications (openapi or asyncapi or graphql) for a resource by its id and version',
1268
1336
  findResourcesByOwner:
1269
1337
  'Use this tool to find all resources (agents, services, events, commands, etc.) owned by a specific team or user',
1270
1338
  getProducersOfMessage:
1271
- 'Use this tool to find which agents or services produce (send) a specific message (event, command, or query)',
1339
+ 'Use this tool to find which services, agents or domains produce (send) a specific message (event, command, or query). Only the latest versions of those resources are included.',
1272
1340
  getConsumersOfMessage:
1273
- 'Use this tool to find which agents or services consume (receive) a specific message (event, command, or query)',
1341
+ 'Use this tool to find which services, agents or domains consume (receive) a specific message (event, command, or query). Only the latest versions of those resources are included.',
1274
1342
  getC4Diagram:
1275
1343
  'Use this tool to get the C4 diagram (or likeC4) in EventCatalog. These files usually have a .c4 extension on them',
1276
1344
  analyzeChangeImpact:
1277
- 'Use this tool to analyze the impact of changing a message. Returns all affected agents and services (producers and consumers), the teams that own them, and the blast radius of the change',
1345
+ 'Use this tool to analyze the impact of changing a message. Returns all affected services, agents and domains (producers and consumers), the teams that own them, and the blast radius of the change',
1278
1346
  explainBusinessFlow:
1279
1347
  'Use this tool to get detailed information about a business flow (state machine). Returns the flow definition, steps, mermaid diagram if available, and related agents and services',
1280
1348
  getTeams:
@@ -31,6 +31,7 @@ const {
31
31
  collection={`${collection}-entities`}
32
32
  title={`${props.data.name} (v${props.data.version}) - Entity Diagram`}
33
33
  mode="full"
34
+ syncRenderModeWithUrl
34
35
  linkTo="visualiser"
35
36
  version={props.data.version}
36
37
  linksToVisualiser={false}
@@ -33,6 +33,7 @@ const {
33
33
  collection={collection}
34
34
  title={`${props.data.name} (v${props.data.version})`}
35
35
  mode="full"
36
+ syncRenderModeWithUrl
36
37
  linkTo="visualiser"
37
38
  version={props.data.version}
38
39
  linksToVisualiser={false}
@@ -4,12 +4,17 @@
4
4
  *
5
5
  * Domains and systems have their own routes next to their Diagram pages
6
6
  * (visualiser/domains/[id]/[version]), which would otherwise handle these requests first.
7
+ * Their levels are served at /visualiser/{domains|systems}/{id}/{version}/{overview|services}.mermaid.
7
8
  */
8
9
  import { isAuthEnabled, isVisualiserEnabled } from '@utils/feature';
9
10
  import { pageDataLoader } from '@utils/page-loaders/page-data-loader';
10
11
  import { getArchitectureDiagramGraph } from '@utils/node-graphs/architecture-diagram';
11
- import { isArchitectureDiagramCollection } from '@utils/node-graphs/architecture-diagram-types';
12
- import { convertToMermaid } from '@utils/node-graphs/export-mermaid';
12
+ import {
13
+ ARCHITECTURE_DIAGRAM_DETAILS,
14
+ isArchitectureDiagramCollection,
15
+ type ArchitectureDiagramDetail,
16
+ } from '@utils/node-graphs/architecture-diagram-types';
17
+ import { convertToMermaid } from '@eventcatalog/visualiser/layout';
13
18
  import type { PageTypes } from '@types';
14
19
 
15
20
  export type MermaidDiagramType =
@@ -34,7 +39,35 @@ export async function getMermaidDiagramPaths(types: MermaidDiagramType[]) {
34
39
  );
35
40
  }
36
41
 
37
- export async function createMermaidDiagramResponse({ type, id, version }: { type?: string; id?: string; version?: string }) {
42
+ /** Levels served at /visualiser/{domains|systems}/{id}/{version}/{level}.mermaid ({version}.mermaid is the full diagram) */
43
+ export const MERMAID_LEVELS = ['overview', 'services'] as const satisfies ArchitectureDiagramDetail[];
44
+
45
+ const LEVEL_DESCRIPTIONS: Record<ArchitectureDiagramDetail, string> = {
46
+ overview: 'L1 overview (domains, systems and their relationships)',
47
+ services: 'L2 services and data stores',
48
+ full: 'L3 full (services, messages and channels)',
49
+ };
50
+
51
+ /** Every version of each domain or system with each level, for building their Mermaid files in static builds */
52
+ export async function getMermaidLevelPaths(type: 'domains' | 'systems') {
53
+ const paths = await getMermaidDiagramPaths([type]);
54
+ return paths.flatMap(({ id, version }) => MERMAID_LEVELS.map((level) => ({ params: { id, version, level } })));
55
+ }
56
+
57
+ const isMermaidDetail = (detail: string): detail is ArchitectureDiagramDetail =>
58
+ (ARCHITECTURE_DIAGRAM_DETAILS as readonly string[]).includes(detail);
59
+
60
+ export async function createMermaidDiagramResponse({
61
+ type,
62
+ id,
63
+ version,
64
+ detail = 'full',
65
+ }: {
66
+ type?: string;
67
+ id?: string;
68
+ version?: string;
69
+ detail?: string;
70
+ }) {
38
71
  if (!type || !id || !version || !isVisualiserEnabled()) {
39
72
  return new Response('Not found', { status: 404 });
40
73
  }
@@ -44,9 +77,18 @@ export async function createMermaidDiagramResponse({ type, id, version }: { type
44
77
  return new Response(`Unsupported type: ${type}`, { status: 400 });
45
78
  }
46
79
 
80
+ if (!isMermaidDetail(detail)) {
81
+ return new Response('Not found', { status: 404 });
82
+ }
83
+
47
84
  try {
48
- // Built the same way as the resource's Diagram page
49
- const { nodes, edges } = await getArchitectureDiagramGraph({ collection: type, id, version });
85
+ // Built the same way as the resource's Diagram page, at the level asked for
86
+ const { nodes, edges } = await getArchitectureDiagramGraph({
87
+ collection: type,
88
+ id,
89
+ version,
90
+ ...(detail !== 'full' && { detail }),
91
+ });
50
92
 
51
93
  if (!nodes || nodes.length === 0) {
52
94
  return new Response('No diagram data available', { status: 404 });
@@ -60,6 +102,7 @@ export async function createMermaidDiagramResponse({ type, id, version }: { type
60
102
  // Add header comment with metadata
61
103
  const header = `%% EventCatalog Mermaid Diagram
62
104
  %% Resource: ${type}/${id} (v${version})
105
+ %% Level: ${LEVEL_DESCRIPTIONS[detail]}
63
106
  %% Generated: ${new Date().toISOString()}
64
107
 
65
108
  `;
@@ -34,6 +34,7 @@ const { data } = props;
34
34
  title={data.name}
35
35
  client:only="react"
36
36
  mode="full"
37
+ syncRenderModeWithUrl
37
38
  linkTo="visualiser"
38
39
  isChatEnabled={isChatEnabled}
39
40
  />
@@ -0,0 +1,18 @@
1
+ /**
2
+ * API endpoint to return mermaid diagram code for a level of a domain diagram
3
+ * URL: /visualiser/domains/{id}/{version}/{overview|services}.mermaid
4
+ *
5
+ * `overview` is level 1 (domains, systems and their relationships) and `services` level 2
6
+ * (services and data stores), as on the Diagram page. {version}.mermaid is the full diagram (level 3).
7
+ */
8
+ import type { APIRoute, GetStaticPaths } from 'astro';
9
+ import { isAuthEnabled } from '@utils/feature';
10
+ import { createMermaidDiagramResponse, getMermaidLevelPaths } from '../../../_mermaid-route';
11
+
12
+ // Prerender static pages when auth is disabled, use SSR when auth is enabled
13
+ export const prerender = !isAuthEnabled();
14
+
15
+ export const getStaticPaths: GetStaticPaths = () => getMermaidLevelPaths('domains');
16
+
17
+ export const GET: APIRoute = async ({ params }) =>
18
+ createMermaidDiagramResponse({ type: 'domains', id: params.id, version: params.version, detail: params.level });