@eventcatalog/core 4.2.6 → 4.3.0

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-OBA7TFHM.js → chunk-32ZHIOUO.js} +3 -3
  6. package/dist/{chunk-XTSY2LTR.js → chunk-GZOFTODZ.js} +1 -1
  7. package/dist/{chunk-DEGSBGEF.js → chunk-RQCRNCF6.js} +1 -1
  8. package/dist/{chunk-OZNVNRI7.js → chunk-TE4QQSPT.js} +1 -1
  9. package/dist/{chunk-NZUMNTJK.js → chunk-ZLUUUTWP.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +12 -1
  13. package/dist/eventcatalog.cjs +1 -1
  14. package/dist/eventcatalog.config.d.cts +5 -0
  15. package/dist/eventcatalog.config.d.ts +5 -0
  16. package/dist/eventcatalog.js +9 -9
  17. package/dist/generate.cjs +1 -1
  18. package/dist/generate.js +3 -3
  19. package/dist/utils/cli-logger.cjs +1 -1
  20. package/dist/utils/cli-logger.js +2 -2
  21. package/eventcatalog/src/components/CopyAsMarkdown.tsx +73 -5
  22. package/eventcatalog/src/components/McpConnectDialog.tsx +98 -0
  23. package/eventcatalog/src/components/McpIcon.tsx +14 -0
  24. package/eventcatalog/src/enterprise/feature.ts +1 -1
  25. package/eventcatalog/src/enterprise/mcp/mcp-scope.ts +159 -0
  26. package/eventcatalog/src/enterprise/mcp/mcp-scoped-tools.ts +197 -0
  27. package/eventcatalog/src/enterprise/mcp/mcp-server.ts +287 -176
  28. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +13 -0
  29. package/eventcatalog/src/types/mcp-modules.d.ts +1 -0
  30. package/package.json +1 -1
@@ -10,33 +10,11 @@ import { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/
10
10
  import { z } from 'zod';
11
11
  import { join } from 'node:path';
12
12
  import { isEventCatalogScaleEnabled } from '@utils/feature';
13
- import {
14
- getResources,
15
- getResource,
16
- getMessagesProducedOrConsumedByResource,
17
- getSchemaForResource,
18
- findResourcesByOwner,
19
- getProducersOfMessage,
20
- getConsumersOfMessage,
21
- analyzeChangeImpact,
22
- explainBusinessFlow,
23
- getTeams,
24
- getTeam,
25
- getUsers,
26
- getUser,
27
- findMessageBySchemaId,
28
- explainUbiquitousLanguageTerms,
29
- getCustomDocs,
30
- searchCustomDocs,
31
- getCustomDoc,
32
- collectionSchema,
33
- resourceCollectionSchema,
34
- messageCollectionSchema,
35
- toolDescriptions,
36
- getC4Diagram,
37
- } from '@enterprise/tools/catalog-tools';
13
+ import * as catalogTools from '@enterprise/tools/catalog-tools';
38
14
  import { getCollection } from 'astro:content';
39
15
  import { createMcpAuthErrorResponse, validateMcpRequest } from './mcp-auth';
16
+ import { createScopedCatalogTools } from './mcp-scoped-tools';
17
+ import { McpScopeNotFoundError, resolveMcpScope, type McpScope, type McpScopeKind } from './mcp-scope';
40
18
 
41
19
  const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
42
20
 
@@ -81,196 +59,202 @@ try {
81
59
  }
82
60
 
83
61
  // Create MCP Server with tools that access Astro collections
84
- function createMcpServer() {
62
+ function createMcpServer(scope?: McpScope) {
85
63
  const server = new McpServer({
86
- name: 'EventCatalog MCP Server',
64
+ name: scope ? `EventCatalog MCP Server — ${scope.name} ${scope.ref.kind}` : 'EventCatalog MCP Server',
87
65
  version: '1.0.0',
88
66
  });
89
67
 
68
+ const tools = scope ? createScopedCatalogTools(scope) : catalogTools;
69
+
90
70
  // Register all built-in tools using the helper
91
71
  server.registerTool(
92
72
  'getResources',
93
73
  {
94
- description: toolDescriptions.getResources,
74
+ description: catalogTools.toolDescriptions.getResources,
95
75
  inputSchema: z.object({
96
- collection: collectionSchema.describe('The collection to get the resources from'),
76
+ collection: catalogTools.collectionSchema.describe('The collection to get the resources from'),
97
77
  cursor: z.string().optional().describe('Pagination cursor from previous response'),
98
78
  search: z.string().optional().describe('Search term to filter resources by name, id, or summary (case-insensitive)'),
99
79
  }),
100
80
  },
101
- createToolHandler(getResources, 'Failed to get resources')
81
+ createToolHandler(tools.getResources, 'Failed to get resources')
102
82
  );
103
83
 
104
84
  server.registerTool(
105
85
  'getResource',
106
86
  {
107
- description: toolDescriptions.getResource,
87
+ description: catalogTools.toolDescriptions.getResource,
108
88
  inputSchema: z.object({
109
- collection: collectionSchema.describe('The collection to get the resource from'),
89
+ collection: catalogTools.collectionSchema.describe('The collection to get the resource from'),
110
90
  id: z.string().describe('The id of the resource to get'),
111
91
  version: z.string().describe('The version of the resource to get'),
112
92
  }),
113
93
  },
114
- createToolHandler(getResource, 'Failed to get resource')
94
+ createToolHandler(tools.getResource, 'Failed to get resource')
115
95
  );
116
96
 
117
97
  server.registerTool(
118
98
  'getMessagesProducedOrConsumedByResource',
119
99
  {
120
- description: toolDescriptions.getMessagesProducedOrConsumedByResource,
100
+ description: catalogTools.toolDescriptions.getMessagesProducedOrConsumedByResource,
121
101
  inputSchema: z.object({
122
102
  resourceId: z.string().describe('The id of the resource to get the messages produced or consumed for'),
123
103
  resourceVersion: z.string().describe('The version of the resource to get the messages produced or consumed for'),
124
- resourceCollection: resourceCollectionSchema
104
+ resourceCollection: catalogTools.resourceCollectionSchema
125
105
  .describe('The collection of the resource to get the messages produced or consumed for')
126
106
  .default('services'),
127
107
  }),
128
108
  },
129
- createToolHandler(getMessagesProducedOrConsumedByResource, 'Failed to get messages')
109
+ createToolHandler(tools.getMessagesProducedOrConsumedByResource, 'Failed to get messages')
130
110
  );
131
111
 
132
112
  server.registerTool(
133
113
  'getSchemaForResource',
134
114
  {
135
- description: toolDescriptions.getSchemaForResource,
115
+ description: catalogTools.toolDescriptions.getSchemaForResource,
136
116
  inputSchema: z.object({
137
117
  resourceId: z.string().describe('The id of the resource to get the schema for'),
138
118
  resourceVersion: z.string().describe('The version of the resource to get the schema for'),
139
- resourceCollection: resourceCollectionSchema
119
+ resourceCollection: catalogTools.resourceCollectionSchema
140
120
  .describe('The collection of the resource to get the schema for')
141
121
  .default('services'),
142
122
  }),
143
123
  },
144
- createToolHandler(getSchemaForResource, 'Failed to get schema')
124
+ createToolHandler(tools.getSchemaForResource, 'Failed to get schema')
145
125
  );
146
126
 
147
127
  server.registerTool(
148
128
  'findResourcesByOwner',
149
129
  {
150
- description: toolDescriptions.findResourcesByOwner,
130
+ description: catalogTools.toolDescriptions.findResourcesByOwner,
151
131
  inputSchema: z.object({
152
132
  ownerId: z.string().describe('The id of the owner (team or user) to find resources for'),
153
133
  }),
154
134
  },
155
- createToolHandler(findResourcesByOwner, 'Failed to find resources')
135
+ createToolHandler(tools.findResourcesByOwner, 'Failed to find resources')
156
136
  );
157
137
 
158
138
  server.registerTool(
159
139
  'getProducersOfMessage',
160
140
  {
161
- description: toolDescriptions.getProducersOfMessage,
141
+ description: catalogTools.toolDescriptions.getProducersOfMessage,
162
142
  inputSchema: z.object({
163
143
  messageId: z.string().describe('The id of the message to find producers for'),
164
144
  messageVersion: z.string().describe('The version of the message'),
165
- messageCollection: messageCollectionSchema
145
+ messageCollection: catalogTools.messageCollectionSchema
166
146
  .describe('The collection type of the message (events, commands, or queries)')
167
147
  .default('events'),
168
148
  }),
169
149
  },
170
- createToolHandler(getProducersOfMessage, 'Failed to get producers')
150
+ createToolHandler(tools.getProducersOfMessage, 'Failed to get producers')
171
151
  );
172
152
 
173
153
  server.registerTool(
174
154
  'getConsumersOfMessage',
175
155
  {
176
- description: toolDescriptions.getConsumersOfMessage,
156
+ description: catalogTools.toolDescriptions.getConsumersOfMessage,
177
157
  inputSchema: z.object({
178
158
  messageId: z.string().describe('The id of the message to find consumers for'),
179
159
  messageVersion: z.string().describe('The version of the message'),
180
- messageCollection: messageCollectionSchema
160
+ messageCollection: catalogTools.messageCollectionSchema
181
161
  .describe('The collection type of the message (events, commands, or queries)')
182
162
  .default('events'),
183
163
  }),
184
164
  },
185
- createToolHandler(getConsumersOfMessage, 'Failed to get consumers')
165
+ createToolHandler(tools.getConsumersOfMessage, 'Failed to get consumers')
186
166
  );
187
167
 
188
- server.registerTool(
189
- 'getC4Diagram',
190
- {
191
- description: toolDescriptions.getC4Diagram,
192
- inputSchema: z.object({
193
- viewId: z.string().describe('The id of the LikeC4 view to return source files for').optional(),
194
- }),
195
- },
196
- createToolHandler(getC4Diagram, 'Failed to get c4 diagram')
197
- );
168
+ if (!scope) {
169
+ server.registerTool(
170
+ 'getC4Diagram',
171
+ {
172
+ description: catalogTools.toolDescriptions.getC4Diagram,
173
+ inputSchema: z.object({
174
+ viewId: z.string().describe('The id of the LikeC4 view to return source files for').optional(),
175
+ }),
176
+ },
177
+ createToolHandler(catalogTools.getC4Diagram, 'Failed to get c4 diagram')
178
+ );
179
+ }
198
180
 
199
181
  server.registerTool(
200
182
  'analyzeChangeImpact',
201
183
  {
202
- description: toolDescriptions.analyzeChangeImpact,
184
+ description: catalogTools.toolDescriptions.analyzeChangeImpact,
203
185
  inputSchema: z.object({
204
186
  messageId: z.string().describe('The id of the message to analyze impact for'),
205
187
  messageVersion: z.string().describe('The version of the message'),
206
- messageCollection: messageCollectionSchema
188
+ messageCollection: catalogTools.messageCollectionSchema
207
189
  .describe('The collection type of the message (events, commands, or queries)')
208
190
  .default('events'),
209
191
  }),
210
192
  },
211
- createToolHandler(analyzeChangeImpact, 'Failed to analyze impact')
193
+ createToolHandler(tools.analyzeChangeImpact, 'Failed to analyze impact')
212
194
  );
213
195
 
214
196
  server.registerTool(
215
197
  'explainBusinessFlow',
216
198
  {
217
- description: toolDescriptions.explainBusinessFlow,
199
+ description: catalogTools.toolDescriptions.explainBusinessFlow,
218
200
  inputSchema: z.object({
219
201
  flowId: z.string().describe('The id of the flow to explain'),
220
202
  flowVersion: z.string().describe('The version of the flow'),
221
203
  }),
222
204
  },
223
- createToolHandler(explainBusinessFlow, 'Failed to explain flow')
205
+ createToolHandler(tools.explainBusinessFlow, 'Failed to explain flow')
224
206
  );
225
207
 
226
- server.registerTool(
227
- 'getTeams',
228
- {
229
- description: toolDescriptions.getTeams,
230
- inputSchema: z.object({
231
- cursor: z.string().optional().describe('Pagination cursor from previous response'),
232
- }),
233
- },
234
- createToolHandler(getTeams, 'Failed to get teams')
235
- );
208
+ if (!scope) {
209
+ server.registerTool(
210
+ 'getTeams',
211
+ {
212
+ description: catalogTools.toolDescriptions.getTeams,
213
+ inputSchema: z.object({
214
+ cursor: z.string().optional().describe('Pagination cursor from previous response'),
215
+ }),
216
+ },
217
+ createToolHandler(catalogTools.getTeams, 'Failed to get teams')
218
+ );
236
219
 
237
- server.registerTool(
238
- 'getTeam',
239
- {
240
- description: toolDescriptions.getTeam,
241
- inputSchema: z.object({
242
- id: z.string().describe('The id of the team to get'),
243
- }),
244
- },
245
- createToolHandler(getTeam, 'Failed to get team')
246
- );
220
+ server.registerTool(
221
+ 'getTeam',
222
+ {
223
+ description: catalogTools.toolDescriptions.getTeam,
224
+ inputSchema: z.object({
225
+ id: z.string().describe('The id of the team to get'),
226
+ }),
227
+ },
228
+ createToolHandler(catalogTools.getTeam, 'Failed to get team')
229
+ );
247
230
 
248
- server.registerTool(
249
- 'getUsers',
250
- {
251
- description: toolDescriptions.getUsers,
252
- inputSchema: z.object({
253
- cursor: z.string().optional().describe('Pagination cursor from previous response'),
254
- }),
255
- },
256
- createToolHandler(getUsers, 'Failed to get users')
257
- );
231
+ server.registerTool(
232
+ 'getUsers',
233
+ {
234
+ description: catalogTools.toolDescriptions.getUsers,
235
+ inputSchema: z.object({
236
+ cursor: z.string().optional().describe('Pagination cursor from previous response'),
237
+ }),
238
+ },
239
+ createToolHandler(catalogTools.getUsers, 'Failed to get users')
240
+ );
258
241
 
259
- server.registerTool(
260
- 'getUser',
261
- {
262
- description: toolDescriptions.getUser,
263
- inputSchema: z.object({
264
- id: z.string().describe('The id of the user to get'),
265
- }),
266
- },
267
- createToolHandler(getUser, 'Failed to get user')
268
- );
242
+ server.registerTool(
243
+ 'getUser',
244
+ {
245
+ description: catalogTools.toolDescriptions.getUser,
246
+ inputSchema: z.object({
247
+ id: z.string().describe('The id of the user to get'),
248
+ }),
249
+ },
250
+ createToolHandler(catalogTools.getUser, 'Failed to get user')
251
+ );
252
+ }
269
253
 
270
254
  server.registerTool(
271
255
  'findMessageBySchemaId',
272
256
  {
273
- description: toolDescriptions.findMessageBySchemaId,
257
+ description: catalogTools.toolDescriptions.findMessageBySchemaId,
274
258
  inputSchema: z.object({
275
259
  messageId: z.string().describe('The message id (from x-eventcatalog-id in the schema)'),
276
260
  messageVersion: z
@@ -279,64 +263,68 @@ function createMcpServer() {
279
263
  .describe(
280
264
  'The message version (from x-eventcatalog-version in the schema). If not provided, returns the latest version.'
281
265
  ),
282
- collection: messageCollectionSchema
266
+ collection: catalogTools.messageCollectionSchema
283
267
  .optional()
284
268
  .describe('Optional hint for which collection to search (events, commands, or queries)'),
285
269
  }),
286
270
  },
287
- createToolHandler(findMessageBySchemaId, 'Failed to find message')
271
+ createToolHandler(tools.findMessageBySchemaId, 'Failed to find message')
288
272
  );
289
273
 
290
- server.registerTool(
291
- 'explainUbiquitousLanguageTerms',
292
- {
293
- description: toolDescriptions.explainUbiquitousLanguageTerms,
294
- inputSchema: z.object({
295
- domainId: z.string().describe('The id of the domain to get ubiquitous language terms for'),
296
- domainVersion: z.string().optional().describe('The version of the domain. If not provided, uses the latest version.'),
297
- }),
298
- },
299
- createToolHandler(explainUbiquitousLanguageTerms, 'Failed to get ubiquitous language terms')
300
- );
274
+ if (!scope || scope.ref.kind === 'domain') {
275
+ server.registerTool(
276
+ 'explainUbiquitousLanguageTerms',
277
+ {
278
+ description: catalogTools.toolDescriptions.explainUbiquitousLanguageTerms,
279
+ inputSchema: z.object({
280
+ domainId: z.string().describe('The id of the domain to get ubiquitous language terms for'),
281
+ domainVersion: z.string().optional().describe('The version of the domain. If not provided, uses the latest version.'),
282
+ }),
283
+ },
284
+ createToolHandler(tools.explainUbiquitousLanguageTerms, 'Failed to get ubiquitous language terms')
285
+ );
286
+ }
301
287
 
302
- server.registerTool(
303
- 'getCustomDocs',
304
- {
305
- description: toolDescriptions.getCustomDocs,
306
- inputSchema: z.object({
307
- cursor: z.string().optional().describe('Pagination cursor from previous response'),
308
- search: z.string().optional().describe('Search term to filter docs by title, id, or summary (case-insensitive)'),
309
- }),
310
- },
311
- createToolHandler(getCustomDocs, 'Failed to get custom documentation pages')
312
- );
288
+ if (!scope) {
289
+ server.registerTool(
290
+ 'getCustomDocs',
291
+ {
292
+ description: catalogTools.toolDescriptions.getCustomDocs,
293
+ inputSchema: z.object({
294
+ cursor: z.string().optional().describe('Pagination cursor from previous response'),
295
+ search: z.string().optional().describe('Search term to filter docs by title, id, or summary (case-insensitive)'),
296
+ }),
297
+ },
298
+ createToolHandler(catalogTools.getCustomDocs, 'Failed to get custom documentation pages')
299
+ );
313
300
 
314
- server.registerTool(
315
- 'searchCustomDocs',
316
- {
317
- description: toolDescriptions.searchCustomDocs,
318
- inputSchema: z.object({
319
- query: z.string().describe('Full-text search query, e.g. keywords describing the topic to find'),
320
- limit: z.number().optional().describe('Maximum number of results to return (default 10)'),
321
- }),
322
- },
323
- createToolHandler(searchCustomDocs, 'Failed to search custom documentation')
324
- );
301
+ server.registerTool(
302
+ 'searchCustomDocs',
303
+ {
304
+ description: catalogTools.toolDescriptions.searchCustomDocs,
305
+ inputSchema: z.object({
306
+ query: z.string().describe('Full-text search query, e.g. keywords describing the topic to find'),
307
+ limit: z.number().optional().describe('Maximum number of results to return (default 10)'),
308
+ }),
309
+ },
310
+ createToolHandler(catalogTools.searchCustomDocs, 'Failed to search custom documentation')
311
+ );
325
312
 
326
- server.registerTool(
327
- 'getCustomDoc',
328
- {
329
- description: toolDescriptions.getCustomDoc,
330
- inputSchema: z.object({
331
- id: z.string().describe('The id or slug of the custom documentation page'),
332
- section: z.string().optional().describe('Optional section heading to return only that section of the page'),
333
- }),
334
- },
335
- createToolHandler(getCustomDoc, 'Failed to get custom documentation page')
336
- );
313
+ server.registerTool(
314
+ 'getCustomDoc',
315
+ {
316
+ description: catalogTools.toolDescriptions.getCustomDoc,
317
+ inputSchema: z.object({
318
+ id: z.string().describe('The id or slug of the custom documentation page'),
319
+ section: z.string().optional().describe('Optional section heading to return only that section of the page'),
320
+ }),
321
+ },
322
+ createToolHandler(catalogTools.getCustomDoc, 'Failed to get custom documentation page')
323
+ );
324
+ }
337
325
 
338
326
  // Register extended tools from user configuration
339
- for (const [toolName, toolConfig] of Object.entries(extendedTools)) {
327
+ for (const [toolName, toolConfig] of Object.entries(scope ? {} : extendedTools)) {
340
328
  if (!toolConfig || typeof toolConfig !== 'object') continue;
341
329
 
342
330
  // Extract tool properties (Vercel AI SDK format)
@@ -386,9 +374,13 @@ function createMcpServer() {
386
374
  'agents',
387
375
  'services',
388
376
  'domains',
377
+ 'systems',
389
378
  'flows',
390
379
  'channels',
391
380
  'entities',
381
+ 'containers',
382
+ 'diagrams',
383
+ 'data-products',
392
384
  'adrs',
393
385
  ] as const,
394
386
  },
@@ -416,6 +408,12 @@ function createMcpServer() {
416
408
  description: 'All services in EventCatalog',
417
409
  collections: ['services'] as const,
418
410
  },
411
+ {
412
+ name: 'All Systems in EventCatalog',
413
+ uri: 'eventcatalog://systems',
414
+ description: 'All systems in EventCatalog',
415
+ collections: ['systems'] as const,
416
+ },
419
417
  {
420
418
  name: 'All Agents in EventCatalog',
421
419
  uri: 'eventcatalog://agents',
@@ -464,6 +462,12 @@ function createMcpServer() {
464
462
  description: 'All flows in EventCatalog',
465
463
  collections: ['flows'] as const,
466
464
  },
465
+ {
466
+ name: 'All Data Products in EventCatalog',
467
+ uri: 'eventcatalog://data-products',
468
+ description: 'All data products in EventCatalog',
469
+ collections: ['data-products'] as const,
470
+ },
467
471
  {
468
472
  name: 'All Teams in EventCatalog',
469
473
  uri: 'eventcatalog://teams',
@@ -479,16 +483,26 @@ function createMcpServer() {
479
483
  ];
480
484
 
481
485
  for (const resource of resourceDefinitions) {
486
+ if (scope && resource.collections.every((collection) => collection === 'teams' || collection === 'users')) continue;
487
+
488
+ const resourceUri = scope
489
+ ? `${scope.uriPrefix}/resources${resource.uri === 'eventcatalog://all' ? '' : `/${resource.uri.replace('eventcatalog://', '')}`}`
490
+ : resource.uri;
491
+ const resourceName = scope ? resource.name.replace('in EventCatalog', `in ${scope.name} ${scope.ref.kind}`) : resource.name;
492
+ const resourceDescription = scope
493
+ ? resource.description.replace('in EventCatalog', `in ${scope.name} ${scope.ref.kind}`)
494
+ : resource.description;
495
+
482
496
  server.registerResource(
483
- resource.name,
484
- resource.uri,
485
- { description: resource.description, mimeType: 'application/json' },
497
+ resourceName,
498
+ resourceUri,
499
+ { description: resourceDescription, mimeType: 'application/json' },
486
500
  async (uri: URL) => {
487
501
  const allResources: any[] = [];
488
502
 
489
503
  for (const collectionName of resource.collections) {
490
504
  try {
491
- const items = await getCollection(collectionName as any);
505
+ const items = scope ? scope.list(collectionName) : await getCollection(collectionName as any);
492
506
  for (const item of items) {
493
507
  allResources.push({
494
508
  type: collectionName,
@@ -507,7 +521,7 @@ function createMcpServer() {
507
521
  contents: [
508
522
  {
509
523
  uri: uri.href,
510
- text: JSON.stringify({ resources: allResources }, null, 2),
524
+ text: JSON.stringify({ ...(scope && { scope: scope.ref }), resources: allResources }, null, 2),
511
525
  mimeType: 'application/json',
512
526
  },
513
527
  ],
@@ -525,8 +539,39 @@ function createMcpServer() {
525
539
  // Create Hono app for MCP routes
526
540
  const app = new Hono().basePath('/docs/mcp');
527
541
 
528
- // Built-in tool names derived from toolDescriptions
529
- const builtInTools = Object.keys(toolDescriptions);
542
+ const globalBuiltInTools = [
543
+ 'getResources',
544
+ 'getResource',
545
+ 'getMessagesProducedOrConsumedByResource',
546
+ 'getSchemaForResource',
547
+ 'findResourcesByOwner',
548
+ 'getProducersOfMessage',
549
+ 'getConsumersOfMessage',
550
+ 'getC4Diagram',
551
+ 'analyzeChangeImpact',
552
+ 'explainBusinessFlow',
553
+ 'getTeams',
554
+ 'getTeam',
555
+ 'getUsers',
556
+ 'getUser',
557
+ 'findMessageBySchemaId',
558
+ 'explainUbiquitousLanguageTerms',
559
+ 'getCustomDocs',
560
+ 'searchCustomDocs',
561
+ 'getCustomDoc',
562
+ ];
563
+
564
+ const scopedBuiltInTools = globalBuiltInTools.filter(
565
+ (tool) =>
566
+ !['getC4Diagram', 'getTeams', 'getTeam', 'getUsers', 'getUser', 'getCustomDocs', 'searchCustomDocs', 'getCustomDoc'].includes(
567
+ tool
568
+ )
569
+ );
570
+
571
+ const getScopedBuiltInTools = (scope: McpScope) =>
572
+ scope.ref.kind === 'system'
573
+ ? scopedBuiltInTools.filter((tool) => tool !== 'explainUbiquitousLanguageTerms')
574
+ : scopedBuiltInTools;
530
575
 
531
576
  // MCP Resource URIs
532
577
  const mcpResources = [
@@ -538,32 +583,85 @@ const mcpResources = [
538
583
  'eventcatalog://adrs',
539
584
  'eventcatalog://services',
540
585
  'eventcatalog://domains',
586
+ 'eventcatalog://systems',
587
+ 'eventcatalog://channels',
541
588
  'eventcatalog://entities',
589
+ 'eventcatalog://containers',
590
+ 'eventcatalog://diagrams',
591
+ 'eventcatalog://data-products',
542
592
  'eventcatalog://flows',
543
593
  'eventcatalog://teams',
544
594
  'eventcatalog://users',
545
595
  ];
546
596
 
547
- // Health check endpoint
548
- app.get('/', async (c: Context) => {
597
+ const getMcpResourceUris = (scope?: McpScope) =>
598
+ scope
599
+ ? mcpResources
600
+ .filter((uri) => uri !== 'eventcatalog://teams' && uri !== 'eventcatalog://users')
601
+ .map(
602
+ (uri) => `${scope.uriPrefix}/resources${uri === 'eventcatalog://all' ? '' : `/${uri.replace('eventcatalog://', '')}`}`
603
+ )
604
+ : mcpResources;
605
+
606
+ const getScopeRef = (c: Context, kind: McpScopeKind) => ({
607
+ kind,
608
+ id: c.req.param('id'),
609
+ version: c.req.param('version') || 'latest',
610
+ });
611
+
612
+ const resolveRequestScope = async (c: Context, kind?: McpScopeKind) => (kind ? resolveMcpScope(getScopeRef(c, kind)) : undefined);
613
+
614
+ const createScopeNotFoundResponse = (error: McpScopeNotFoundError) =>
615
+ new Response(JSON.stringify({ error: 'scope_not_found', message: error.message }), {
616
+ status: 404,
617
+ headers: { 'Content-Type': 'application/json' },
618
+ });
619
+
620
+ const acceptsEventStream = (request: Request) =>
621
+ request.headers.get('accept')?.toLowerCase().includes('text/event-stream') ?? false;
622
+
623
+ const createSseNotSupportedResponse = () =>
624
+ new Response(
625
+ JSON.stringify({
626
+ jsonrpc: '2.0',
627
+ error: { code: -32000, message: 'Method not allowed: this server does not provide an SSE stream.' },
628
+ id: null,
629
+ }),
630
+ {
631
+ status: 405,
632
+ headers: { Allow: 'POST', 'Content-Type': 'application/json' },
633
+ }
634
+ );
635
+
636
+ const handleGetRequest = async (c: Context, kind?: McpScopeKind) => {
549
637
  const auth = await validateMcpRequest(c.req.raw);
550
638
 
551
639
  if (!auth.ok) {
552
640
  return createMcpAuthErrorResponse(auth);
553
641
  }
554
642
 
555
- return c.json({
556
- name: 'EventCatalog MCP Server',
557
- version: '1.1.0',
558
- status: 'running',
559
- tools: [...builtInTools, ...extendedToolNames],
560
- extendedTools: extendedToolNames.length > 0 ? extendedToolNames : undefined,
561
- resources: mcpResources,
562
- });
563
- });
643
+ // Streamable HTTP clients may open an optional GET SSE channel for server-initiated messages.
644
+ // This endpoint is stateless and does not support that channel, so the MCP specification requires a 405.
645
+ if (acceptsEventStream(c.req.raw)) return createSseNotSupportedResponse();
564
646
 
565
- // MCP protocol endpoint - handles POST requests for MCP protocol
566
- app.post('/', async (c: Context) => {
647
+ try {
648
+ const scope = await resolveRequestScope(c, kind);
649
+ return c.json({
650
+ name: scope ? `EventCatalog MCP Server — ${scope.name} ${scope.ref.kind}` : 'EventCatalog MCP Server',
651
+ version: '1.2.0',
652
+ status: 'running',
653
+ ...(scope && { scope: scope.ref }),
654
+ tools: scope ? getScopedBuiltInTools(scope) : [...globalBuiltInTools, ...extendedToolNames],
655
+ extendedTools: !scope && extendedToolNames.length > 0 ? extendedToolNames : undefined,
656
+ resources: getMcpResourceUris(scope),
657
+ });
658
+ } catch (error) {
659
+ if (error instanceof McpScopeNotFoundError) return createScopeNotFoundResponse(error);
660
+ throw error;
661
+ }
662
+ };
663
+
664
+ const handleMcpRequest = async (c: Context, kind?: McpScopeKind) => {
567
665
  try {
568
666
  const auth = await validateMcpRequest(c.req.raw);
569
667
 
@@ -571,17 +669,16 @@ app.post('/', async (c: Context) => {
571
669
  return createMcpAuthErrorResponse(auth);
572
670
  }
573
671
 
574
- // Create fresh server and transport per request — the MCP SDK's
575
- // WebStandardStreamableHTTPServerTransport is single-use in stateless
576
- // mode: it sets _hasHandledRequest=true after the first call and throws
577
- // on any subsequent request. McpServer equally rejects reconnection.
578
- const server = createMcpServer();
672
+ const scope = await resolveRequestScope(c, kind);
673
+ const server = createMcpServer(scope);
579
674
  const transport = new WebStandardStreamableHTTPServerTransport({
580
675
  sessionIdGenerator: undefined,
581
676
  });
582
677
  await server.connect(transport);
583
678
  return await transport.handleRequest(c.req.raw);
584
679
  } catch (error) {
680
+ if (error instanceof McpScopeNotFoundError) return createScopeNotFoundResponse(error);
681
+
585
682
  console.error('MCP request error:', error);
586
683
  return c.json(
587
684
  {
@@ -595,7 +692,21 @@ app.post('/', async (c: Context) => {
595
692
  500
596
693
  );
597
694
  }
598
- });
695
+ };
696
+
697
+ // Browser GETs return health metadata; MCP SSE negotiation is rejected with 405 because this server is stateless.
698
+ app.get('/', (c: Context) => handleGetRequest(c));
699
+
700
+ for (const kind of ['domain', 'system'] as const) {
701
+ const path = `${kind}s`;
702
+ app.get(`/${path}/:id`, (c: Context) => handleGetRequest(c, kind));
703
+ app.get(`/${path}/:id/:version`, (c: Context) => handleGetRequest(c, kind));
704
+ app.post(`/${path}/:id`, (c: Context) => handleMcpRequest(c, kind));
705
+ app.post(`/${path}/:id/:version`, (c: Context) => handleMcpRequest(c, kind));
706
+ }
707
+
708
+ // MCP protocol endpoint - handles POST requests for MCP protocol
709
+ app.post('/', (c: Context) => handleMcpRequest(c));
599
710
 
600
711
  // Astro API route handler - delegates all requests to Hono
601
712
  // Note: SSR and Scale plan checks are handled at build time by the integration