@eventcatalog/core 3.3.0 → 3.4.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.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-I4CMEOEN.js → chunk-GLMX3ZTY.js} +1 -1
- package/dist/{chunk-NGKYYZZP.js → chunk-KFZIBXRQ.js} +1 -1
- package/dist/{chunk-QZF5ZYJB.js → chunk-MJRHV77M.js} +1 -1
- package/dist/{chunk-UPSN5H7S.js → chunk-Q4DKMESA.js} +1 -1
- package/dist/{chunk-OAUYXPXT.js → chunk-VAGFX36R.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +4 -3
- package/dist/eventcatalog.js +8 -7
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/integrations/eventcatalog-features.ts +9 -0
- package/eventcatalog/src/content.config.ts +9 -6
- package/eventcatalog/src/enterprise/ai/chat-api.ts +27 -83
- package/eventcatalog/src/enterprise/custom-documentation/pages/docs/custom/index.astro +1 -0
- package/eventcatalog/src/enterprise/mcp/mcp-server.ts +512 -0
- package/eventcatalog/src/enterprise/tools/catalog-tools.ts +690 -0
- package/eventcatalog/src/enterprise/tools/index.ts +5 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +24 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +2 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +22 -1
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx +28 -4
- package/eventcatalog/src/pages/docs/[type]/[id]/[version].mdx.ts +0 -4
- package/eventcatalog/src/utils/feature.ts +2 -0
- package/eventcatalog/tsconfig.json +1 -1
- package/package.json +4 -2
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared catalog tools for accessing EventCatalog resources.
|
|
3
|
+
* Used by both the AI Chat feature and MCP Server.
|
|
4
|
+
*/
|
|
5
|
+
import { getCollection, getEntry } from 'astro:content';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { getSchemasFromResource } from '@utils/collections/schemas';
|
|
8
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
|
|
9
|
+
import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
|
|
12
|
+
// ============================================
|
|
13
|
+
// Pagination utilities
|
|
14
|
+
// ============================================
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_PAGE_SIZE = 50;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Encode position to opaque cursor string.
|
|
20
|
+
* Uses base64url for URL-safe encoding.
|
|
21
|
+
*/
|
|
22
|
+
export function encodeCursor(position: number): string {
|
|
23
|
+
return Buffer.from(String(position)).toString('base64url');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Decode cursor string to position.
|
|
28
|
+
* Returns null if cursor is invalid.
|
|
29
|
+
*/
|
|
30
|
+
export function decodeCursor(cursor: string): number | null {
|
|
31
|
+
try {
|
|
32
|
+
const decoded = Buffer.from(cursor, 'base64url').toString('utf8');
|
|
33
|
+
const position = parseInt(decoded, 10);
|
|
34
|
+
return isNaN(position) || position < 0 ? null : position;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Maximum cursor position to prevent abuse
|
|
41
|
+
const MAX_CURSOR_POSITION = 100000;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generic pagination helper for any array of items
|
|
45
|
+
*/
|
|
46
|
+
export function paginate<T>(
|
|
47
|
+
items: T[],
|
|
48
|
+
cursor?: string,
|
|
49
|
+
pageSize: number = DEFAULT_PAGE_SIZE
|
|
50
|
+
): { items: T[]; nextCursor?: string; totalCount: number } | { error: string } {
|
|
51
|
+
let startIndex = 0;
|
|
52
|
+
|
|
53
|
+
if (cursor) {
|
|
54
|
+
const decoded = decodeCursor(cursor);
|
|
55
|
+
if (decoded === null) {
|
|
56
|
+
return { error: 'Invalid or malformed cursor' };
|
|
57
|
+
}
|
|
58
|
+
if (decoded > MAX_CURSOR_POSITION) {
|
|
59
|
+
return { error: 'Cursor position exceeds maximum allowed value' };
|
|
60
|
+
}
|
|
61
|
+
startIndex = decoded;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const endIndex = startIndex + pageSize;
|
|
65
|
+
const paginatedItems = items.slice(startIndex, endIndex);
|
|
66
|
+
const hasMore = endIndex < items.length;
|
|
67
|
+
|
|
68
|
+
const result: { items: T[]; nextCursor?: string; totalCount: number } = {
|
|
69
|
+
items: paginatedItems,
|
|
70
|
+
totalCount: items.length,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (hasMore) {
|
|
74
|
+
result.nextCursor = encodeCursor(endIndex);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================
|
|
81
|
+
// Shared schemas for tool inputs
|
|
82
|
+
// ============================================
|
|
83
|
+
|
|
84
|
+
export const collectionSchema = z.enum([
|
|
85
|
+
'events',
|
|
86
|
+
'services',
|
|
87
|
+
'commands',
|
|
88
|
+
'queries',
|
|
89
|
+
'flows',
|
|
90
|
+
'domains',
|
|
91
|
+
'channels',
|
|
92
|
+
'entities',
|
|
93
|
+
'containers',
|
|
94
|
+
'diagrams',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
export const messageCollectionSchema = z.enum(['events', 'commands', 'queries']);
|
|
98
|
+
|
|
99
|
+
export const resourceCollectionSchema = z.enum([
|
|
100
|
+
'services',
|
|
101
|
+
'events',
|
|
102
|
+
'commands',
|
|
103
|
+
'queries',
|
|
104
|
+
'flows',
|
|
105
|
+
'domains',
|
|
106
|
+
'channels',
|
|
107
|
+
'entities',
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
// ============================================
|
|
111
|
+
// Tool implementations (core logic)
|
|
112
|
+
// ============================================
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Get resources from a collection with optional pagination and search
|
|
116
|
+
*/
|
|
117
|
+
export async function getResources(params: { collection: string; cursor?: string; search?: string }) {
|
|
118
|
+
const resources = await getCollection(params.collection as any);
|
|
119
|
+
let allResults = resources.map((resource: any) => ({
|
|
120
|
+
id: resource.data.id,
|
|
121
|
+
version: resource.data.version,
|
|
122
|
+
name: resource.data.name,
|
|
123
|
+
summary: resource.data.summary,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
// Apply search filter if provided
|
|
127
|
+
if (params.search) {
|
|
128
|
+
const searchTerm = params.search.toLowerCase().trim();
|
|
129
|
+
allResults = allResults.filter(
|
|
130
|
+
(resource) =>
|
|
131
|
+
resource.id?.toLowerCase().includes(searchTerm) ||
|
|
132
|
+
resource.name?.toLowerCase().includes(searchTerm) ||
|
|
133
|
+
resource.summary?.toLowerCase().includes(searchTerm)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const paginatedResult = paginate(allResults, params.cursor);
|
|
138
|
+
|
|
139
|
+
if ('error' in paginatedResult) {
|
|
140
|
+
return paginatedResult;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
resources: paginatedResult.items,
|
|
145
|
+
nextCursor: paginatedResult.nextCursor,
|
|
146
|
+
totalCount: paginatedResult.totalCount,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get a specific resource by id and version
|
|
152
|
+
*/
|
|
153
|
+
export async function getResource(params: { collection: string; id: string; version: string }) {
|
|
154
|
+
const resource = await getEntry(params.collection as any, `${params.id}-${params.version}`);
|
|
155
|
+
|
|
156
|
+
if (!resource) {
|
|
157
|
+
return { error: `Resource not found: ${params.id}-${params.version}` };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return resource;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Get messages produced or consumed by a resource
|
|
165
|
+
*/
|
|
166
|
+
export async function getMessagesProducedOrConsumedByResource(params: {
|
|
167
|
+
resourceId: string;
|
|
168
|
+
resourceVersion: string;
|
|
169
|
+
resourceCollection: string;
|
|
170
|
+
}) {
|
|
171
|
+
const resource = await getEntry(params.resourceCollection as any, `${params.resourceId}-${params.resourceVersion}`);
|
|
172
|
+
|
|
173
|
+
if (!resource) {
|
|
174
|
+
return {
|
|
175
|
+
error: `Resource not found with id ${params.resourceId} and version ${params.resourceVersion} and collection ${params.resourceCollection}`,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return resource;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get schema or specifications for a resource
|
|
184
|
+
*/
|
|
185
|
+
export async function getSchemaForResource(params: { resourceId: string; resourceVersion: string; resourceCollection: string }) {
|
|
186
|
+
const resource = await getEntry(params.resourceCollection as any, `${params.resourceId}-${params.resourceVersion}`);
|
|
187
|
+
|
|
188
|
+
if (!resource) {
|
|
189
|
+
return {
|
|
190
|
+
error: `Resource not found with id ${params.resourceId} and version ${params.resourceVersion} and collection ${params.resourceCollection}`,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const schema = await getSchemasFromResource(resource);
|
|
195
|
+
|
|
196
|
+
if (schema.length > 0) {
|
|
197
|
+
return schema.map((schemaItem) => ({
|
|
198
|
+
url: schemaItem.url,
|
|
199
|
+
format: schemaItem.format,
|
|
200
|
+
code: fs.readFileSync(schemaItem.url, 'utf-8'),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return { message: 'No schemas found for this resource' };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Find all resources owned by a specific team or user
|
|
209
|
+
*/
|
|
210
|
+
export async function findResourcesByOwner(params: { ownerId: string }) {
|
|
211
|
+
const collectionsToSearch = ['events', 'commands', 'queries', 'services', 'domains', 'flows', 'channels', 'entities'] as const;
|
|
212
|
+
|
|
213
|
+
const results: Array<{ collection: string; id: string; version?: string; name: string }> = [];
|
|
214
|
+
|
|
215
|
+
for (const collectionName of collectionsToSearch) {
|
|
216
|
+
const resources = await getCollection(collectionName);
|
|
217
|
+
|
|
218
|
+
for (const resource of resources) {
|
|
219
|
+
const owners = (resource.data as any).owners || [];
|
|
220
|
+
const ownerIds = owners.map((o: any) => (typeof o === 'string' ? o : o.id));
|
|
221
|
+
|
|
222
|
+
if (ownerIds.includes(params.ownerId)) {
|
|
223
|
+
results.push({
|
|
224
|
+
collection: collectionName,
|
|
225
|
+
id: (resource.data as any).id,
|
|
226
|
+
version: (resource.data as any).version,
|
|
227
|
+
name: (resource.data as any).name || (resource.data as any).id,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (results.length === 0) {
|
|
234
|
+
return { message: `No resources found owned by: ${params.ownerId}`, resources: [] };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { ownerId: params.ownerId, totalCount: results.length, resources: results };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get services that produce (send) a specific message
|
|
242
|
+
*/
|
|
243
|
+
export async function getProducersOfMessage(params: { messageId: string; messageVersion: string; messageCollection: string }) {
|
|
244
|
+
const services = await getCollection('services');
|
|
245
|
+
const message = await getEntry(params.messageCollection as any, `${params.messageId}-${params.messageVersion}`);
|
|
246
|
+
|
|
247
|
+
if (!message) {
|
|
248
|
+
return {
|
|
249
|
+
error: `Message not found: ${params.messageId}-${params.messageVersion} in ${params.messageCollection}`,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const producers = services.filter((service) => {
|
|
254
|
+
const sends = (service.data as any).sends || [];
|
|
255
|
+
return sends.some((send: any) => {
|
|
256
|
+
const idMatch = send.id === params.messageId;
|
|
257
|
+
if (!send.version || send.version === 'latest') return idMatch;
|
|
258
|
+
return idMatch && send.version === params.messageVersion;
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
return {
|
|
263
|
+
message: {
|
|
264
|
+
id: params.messageId,
|
|
265
|
+
version: params.messageVersion,
|
|
266
|
+
collection: params.messageCollection,
|
|
267
|
+
},
|
|
268
|
+
producers: producers.map((s) => ({
|
|
269
|
+
id: (s.data as any).id,
|
|
270
|
+
version: (s.data as any).version,
|
|
271
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
272
|
+
})),
|
|
273
|
+
count: producers.length,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Get services that consume (receive) a specific message
|
|
279
|
+
*/
|
|
280
|
+
export async function getConsumersOfMessage(params: { messageId: string; messageVersion: string; messageCollection: string }) {
|
|
281
|
+
const services = await getCollection('services');
|
|
282
|
+
const message = await getEntry(params.messageCollection as any, `${params.messageId}-${params.messageVersion}`);
|
|
283
|
+
|
|
284
|
+
if (!message) {
|
|
285
|
+
return {
|
|
286
|
+
error: `Message not found: ${params.messageId}-${params.messageVersion} in ${params.messageCollection}`,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const consumers = services.filter((service) => {
|
|
291
|
+
const receives = (service.data as any).receives || [];
|
|
292
|
+
return receives.some((receive: any) => {
|
|
293
|
+
const idMatch = receive.id === params.messageId;
|
|
294
|
+
if (!receive.version || receive.version === 'latest') return idMatch;
|
|
295
|
+
return idMatch && receive.version === params.messageVersion;
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
message: {
|
|
301
|
+
id: params.messageId,
|
|
302
|
+
version: params.messageVersion,
|
|
303
|
+
collection: params.messageCollection,
|
|
304
|
+
},
|
|
305
|
+
consumers: consumers.map((s) => ({
|
|
306
|
+
id: (s.data as any).id,
|
|
307
|
+
version: (s.data as any).version,
|
|
308
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
309
|
+
})),
|
|
310
|
+
count: consumers.length,
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Analyze the impact of changing a message (event, command, query)
|
|
316
|
+
* Returns all affected services (producers and consumers) and their owners
|
|
317
|
+
*/
|
|
318
|
+
export async function analyzeChangeImpact(params: { messageId: string; messageVersion: string; messageCollection: string }) {
|
|
319
|
+
const services = await getCollection('services');
|
|
320
|
+
const message = await getEntry(params.messageCollection as any, `${params.messageId}-${params.messageVersion}`);
|
|
321
|
+
|
|
322
|
+
if (!message) {
|
|
323
|
+
return {
|
|
324
|
+
error: `Message not found: ${params.messageId}-${params.messageVersion} in ${params.messageCollection}`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Find producers
|
|
329
|
+
const producers = services.filter((service) => {
|
|
330
|
+
const sends = (service.data as any).sends || [];
|
|
331
|
+
return sends.some((send: any) => send.id === params.messageId);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Find consumers
|
|
335
|
+
const consumers = services.filter((service) => {
|
|
336
|
+
const receives = (service.data as any).receives || [];
|
|
337
|
+
return receives.some((receive: any) => receive.id === params.messageId);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
// Collect all affected teams/owners
|
|
341
|
+
const affectedOwners = new Set<string>();
|
|
342
|
+
[...producers, ...consumers].forEach((service) => {
|
|
343
|
+
const owners = (service.data as any).owners || [];
|
|
344
|
+
owners.forEach((o: any) => {
|
|
345
|
+
const ownerId = typeof o === 'string' ? o : o.id;
|
|
346
|
+
affectedOwners.add(ownerId);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Also check message owners
|
|
351
|
+
const messageOwners = (message.data as any).owners || [];
|
|
352
|
+
messageOwners.forEach((o: any) => {
|
|
353
|
+
const ownerId = typeof o === 'string' ? o : o.id;
|
|
354
|
+
affectedOwners.add(ownerId);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
message: {
|
|
359
|
+
id: params.messageId,
|
|
360
|
+
version: params.messageVersion,
|
|
361
|
+
collection: params.messageCollection,
|
|
362
|
+
name: (message.data as any).name || params.messageId,
|
|
363
|
+
},
|
|
364
|
+
impact: {
|
|
365
|
+
producerCount: producers.length,
|
|
366
|
+
consumerCount: consumers.length,
|
|
367
|
+
totalServicesAffected: new Set([...producers, ...consumers].map((s) => (s.data as any).id)).size,
|
|
368
|
+
teamsAffected: Array.from(affectedOwners),
|
|
369
|
+
},
|
|
370
|
+
producers: producers.map((s) => ({
|
|
371
|
+
id: (s.data as any).id,
|
|
372
|
+
version: (s.data as any).version,
|
|
373
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
374
|
+
owners: (s.data as any).owners || [],
|
|
375
|
+
})),
|
|
376
|
+
consumers: consumers.map((s) => ({
|
|
377
|
+
id: (s.data as any).id,
|
|
378
|
+
version: (s.data as any).version,
|
|
379
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
380
|
+
owners: (s.data as any).owners || [],
|
|
381
|
+
})),
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get detailed information about a flow (state machine / business process)
|
|
387
|
+
* Returns the flow with its steps, description, and related services
|
|
388
|
+
*/
|
|
389
|
+
export async function explainBusinessFlow(params: { flowId: string; flowVersion: string }) {
|
|
390
|
+
const flow = await getEntry('flows', `${params.flowId}-${params.flowVersion}`);
|
|
391
|
+
|
|
392
|
+
if (!flow) {
|
|
393
|
+
return { error: `Flow not found: ${params.flowId}-${params.flowVersion}` };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Get related services that use this flow
|
|
397
|
+
const services = await getCollection('services');
|
|
398
|
+
const relatedServices = services.filter((service) => {
|
|
399
|
+
const flows = (service.data as any).flows || [];
|
|
400
|
+
return flows.some((f: any) => f.id === params.flowId);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
flow: {
|
|
405
|
+
id: (flow.data as any).id,
|
|
406
|
+
version: (flow.data as any).version,
|
|
407
|
+
name: (flow.data as any).name || (flow.data as any).id,
|
|
408
|
+
summary: (flow.data as any).summary,
|
|
409
|
+
owners: (flow.data as any).owners || [],
|
|
410
|
+
steps: (flow.data as any).steps || [],
|
|
411
|
+
mermaid: (flow.data as any).mermaid,
|
|
412
|
+
},
|
|
413
|
+
// Include the markdown content which often contains detailed business logic
|
|
414
|
+
content: flow.body,
|
|
415
|
+
relatedServices: relatedServices.map((s) => ({
|
|
416
|
+
id: (s.data as any).id,
|
|
417
|
+
version: (s.data as any).version,
|
|
418
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
419
|
+
})),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================
|
|
424
|
+
// Team and User tools (no versions)
|
|
425
|
+
// ============================================
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get all teams with optional pagination
|
|
429
|
+
*/
|
|
430
|
+
export async function getTeams(params: { cursor?: string }) {
|
|
431
|
+
const teams = await getCollection('teams');
|
|
432
|
+
const allResults = teams.map((team: any) => ({
|
|
433
|
+
id: team.data.id,
|
|
434
|
+
name: team.data.name,
|
|
435
|
+
email: team.data.email,
|
|
436
|
+
slackDirectMessageUrl: team.data.slackDirectMessageUrl,
|
|
437
|
+
members: team.data.members || [],
|
|
438
|
+
}));
|
|
439
|
+
|
|
440
|
+
const paginatedResult = paginate(allResults, params.cursor);
|
|
441
|
+
|
|
442
|
+
if ('error' in paginatedResult) {
|
|
443
|
+
return paginatedResult;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
teams: paginatedResult.items,
|
|
448
|
+
nextCursor: paginatedResult.nextCursor,
|
|
449
|
+
totalCount: paginatedResult.totalCount,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Get a specific team by id
|
|
455
|
+
*/
|
|
456
|
+
export async function getTeam(params: { id: string }) {
|
|
457
|
+
const teams = await getCollection('teams');
|
|
458
|
+
const team = teams.find((t: any) => t.data.id === params.id);
|
|
459
|
+
|
|
460
|
+
if (!team) {
|
|
461
|
+
return { error: `Team not found: ${params.id}` };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
id: team.data.id,
|
|
466
|
+
name: team.data.name,
|
|
467
|
+
email: team.data.email,
|
|
468
|
+
slackDirectMessageUrl: team.data.slackDirectMessageUrl,
|
|
469
|
+
members: team.data.members || [],
|
|
470
|
+
summary: team.data.summary,
|
|
471
|
+
content: team.body,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Get all users with optional pagination
|
|
477
|
+
*/
|
|
478
|
+
export async function getUsers(params: { cursor?: string }) {
|
|
479
|
+
const users = await getCollection('users');
|
|
480
|
+
const allResults = users.map((user: any) => ({
|
|
481
|
+
id: user.data.id,
|
|
482
|
+
name: user.data.name,
|
|
483
|
+
email: user.data.email,
|
|
484
|
+
role: user.data.role,
|
|
485
|
+
slackDirectMessageUrl: user.data.slackDirectMessageUrl,
|
|
486
|
+
}));
|
|
487
|
+
|
|
488
|
+
const paginatedResult = paginate(allResults, params.cursor);
|
|
489
|
+
|
|
490
|
+
if ('error' in paginatedResult) {
|
|
491
|
+
return paginatedResult;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
users: paginatedResult.items,
|
|
496
|
+
nextCursor: paginatedResult.nextCursor,
|
|
497
|
+
totalCount: paginatedResult.totalCount,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Get a specific user by id
|
|
503
|
+
*/
|
|
504
|
+
export async function getUser(params: { id: string }) {
|
|
505
|
+
const users = await getCollection('users');
|
|
506
|
+
const user = users.find((u: any) => u.data.id === params.id);
|
|
507
|
+
|
|
508
|
+
if (!user) {
|
|
509
|
+
return { error: `User not found: ${params.id}` };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
id: user.data.id,
|
|
514
|
+
name: user.data.name,
|
|
515
|
+
email: user.data.email,
|
|
516
|
+
role: user.data.role,
|
|
517
|
+
slackDirectMessageUrl: user.data.slackDirectMessageUrl,
|
|
518
|
+
summary: (user.data as any).summary,
|
|
519
|
+
content: user.body,
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ============================================
|
|
524
|
+
// Schema-based resource lookup
|
|
525
|
+
// ============================================
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Find a message resource and its producers/consumers by id and version
|
|
529
|
+
* Designed to be used with schema files that contain x-eventcatalog-id extensions
|
|
530
|
+
* If no version is provided, returns the latest version
|
|
531
|
+
*/
|
|
532
|
+
export async function findMessageBySchemaId(params: {
|
|
533
|
+
messageId: string;
|
|
534
|
+
messageVersion?: string;
|
|
535
|
+
collection?: 'events' | 'commands' | 'queries';
|
|
536
|
+
}) {
|
|
537
|
+
const collectionsToSearch = params.collection ? [params.collection] : (['events', 'commands', 'queries'] as const);
|
|
538
|
+
|
|
539
|
+
for (const collectionName of collectionsToSearch) {
|
|
540
|
+
const collection = await getCollection(collectionName);
|
|
541
|
+
const matches = getItemsFromCollectionByIdAndSemverOrLatest(collection, params.messageId, params.messageVersion);
|
|
542
|
+
const resource = matches[0];
|
|
543
|
+
if (resource) {
|
|
544
|
+
// Get producers and consumers
|
|
545
|
+
const services = await getCollection('services');
|
|
546
|
+
|
|
547
|
+
const producers = services.filter((service) => {
|
|
548
|
+
const sends = (service.data as any).sends || [];
|
|
549
|
+
return sends.some((send: any) => send.id === params.messageId);
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
const consumers = services.filter((service) => {
|
|
553
|
+
const receives = (service.data as any).receives || [];
|
|
554
|
+
return receives.some((receive: any) => receive.id === params.messageId);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
resource: {
|
|
559
|
+
id: (resource.data as any).id,
|
|
560
|
+
version: (resource.data as any).version,
|
|
561
|
+
name: (resource.data as any).name,
|
|
562
|
+
collection: collectionName,
|
|
563
|
+
summary: (resource.data as any).summary,
|
|
564
|
+
owners: (resource.data as any).owners || [],
|
|
565
|
+
},
|
|
566
|
+
producers: producers.map((s) => ({
|
|
567
|
+
id: (s.data as any).id,
|
|
568
|
+
version: (s.data as any).version,
|
|
569
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
570
|
+
})),
|
|
571
|
+
consumers: consumers.map((s) => ({
|
|
572
|
+
id: (s.data as any).id,
|
|
573
|
+
version: (s.data as any).version,
|
|
574
|
+
name: (s.data as any).name || (s.data as any).id,
|
|
575
|
+
})),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return {
|
|
581
|
+
error: `Message not found: ${params.messageId}${params.messageVersion ? ` (version ${params.messageVersion})` : ''}`,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ============================================
|
|
586
|
+
// Ubiquitous Language (DDD)
|
|
587
|
+
// ============================================
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Get ubiquitous language terms for a domain (including subdomains)
|
|
591
|
+
* Returns the domain's language/glossary if defined
|
|
592
|
+
*/
|
|
593
|
+
export async function explainUbiquitousLanguageTerms(params: { domainId: string; domainVersion?: string }) {
|
|
594
|
+
const domains = await getCollection('domains');
|
|
595
|
+
|
|
596
|
+
// Use the existing utility to find the domain by id and version (or latest)
|
|
597
|
+
const matches = getItemsFromCollectionByIdAndSemverOrLatest(domains, params.domainId, params.domainVersion);
|
|
598
|
+
const domain = matches[0];
|
|
599
|
+
|
|
600
|
+
if (!domain) {
|
|
601
|
+
return {
|
|
602
|
+
error: `Domain not found: ${params.domainId}${params.domainVersion ? ` (version ${params.domainVersion})` : ''}`,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Get ubiquitous language including subdomains
|
|
607
|
+
const ubiquitousLanguageData = await getUbiquitousLanguageWithSubdomains(domain as any);
|
|
608
|
+
const { domain: domainUL, subdomains, duplicateTerms } = ubiquitousLanguageData;
|
|
609
|
+
|
|
610
|
+
// Extract terms from domain's ubiquitous language
|
|
611
|
+
const domainTerms =
|
|
612
|
+
domainUL?.data?.dictionary?.map((term: any) => ({
|
|
613
|
+
term: term.name,
|
|
614
|
+
description: term.summary,
|
|
615
|
+
icon: term.icon,
|
|
616
|
+
domainId: params.domainId,
|
|
617
|
+
domainName: (domain.data as any).name || params.domainId,
|
|
618
|
+
isDuplicate: duplicateTerms.has(term.name.toLowerCase()),
|
|
619
|
+
})) || [];
|
|
620
|
+
|
|
621
|
+
// Extract terms from subdomains
|
|
622
|
+
const subdomainTerms = subdomains.flatMap(({ subdomain, ubiquitousLanguage }) => {
|
|
623
|
+
if (!ubiquitousLanguage?.data?.dictionary) return [];
|
|
624
|
+
return ubiquitousLanguage.data.dictionary.map((term: any) => ({
|
|
625
|
+
term: term.name,
|
|
626
|
+
description: term.summary,
|
|
627
|
+
icon: term.icon,
|
|
628
|
+
domainId: (subdomain.data as any).id,
|
|
629
|
+
domainName: (subdomain.data as any).name || (subdomain.data as any).id,
|
|
630
|
+
isSubdomain: true,
|
|
631
|
+
isDuplicate: duplicateTerms.has(term.name.toLowerCase()),
|
|
632
|
+
}));
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
const allTerms = [...domainTerms, ...subdomainTerms];
|
|
636
|
+
|
|
637
|
+
if (allTerms.length === 0) {
|
|
638
|
+
return {
|
|
639
|
+
domainId: params.domainId,
|
|
640
|
+
domainVersion: (domain.data as any).version,
|
|
641
|
+
domainName: (domain.data as any).name || params.domainId,
|
|
642
|
+
message: 'No ubiquitous language terms defined for this domain or its subdomains',
|
|
643
|
+
terms: [],
|
|
644
|
+
subdomainCount: subdomains.length,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return {
|
|
649
|
+
domainId: params.domainId,
|
|
650
|
+
domainVersion: (domain.data as any).version,
|
|
651
|
+
domainName: (domain.data as any).name || params.domainId,
|
|
652
|
+
terms: allTerms,
|
|
653
|
+
totalCount: allTerms.length,
|
|
654
|
+
domainTermCount: domainTerms.length,
|
|
655
|
+
subdomainTermCount: subdomainTerms.length,
|
|
656
|
+
subdomainCount: subdomains.length,
|
|
657
|
+
duplicateTerms: Array.from(duplicateTerms),
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// ============================================
|
|
662
|
+
// Tool metadata (descriptions)
|
|
663
|
+
// ============================================
|
|
664
|
+
|
|
665
|
+
export const toolDescriptions = {
|
|
666
|
+
getResources:
|
|
667
|
+
'Use this tool to get events, services, commands, queries, flows, domains, channels, entities from EventCatalog. Supports pagination via cursor and filtering by search term (searches name, id, and summary).',
|
|
668
|
+
getResource: 'Use this tool to get a specific resource from EventCatalog by its id and version',
|
|
669
|
+
getMessagesProducedOrConsumedByResource:
|
|
670
|
+
'Use this tool to get the messages produced or consumed by a resource by its id and version. Look at the `sends` and `receives` properties to get the messages produced or consumed by the resource',
|
|
671
|
+
getSchemaForResource:
|
|
672
|
+
'Use this tool to get the schema or specifications (openapi or asyncapi or graphql) for a resource by its id and version',
|
|
673
|
+
findResourcesByOwner: 'Use this tool to find all resources (services, events, commands, etc.) owned by a specific team or user',
|
|
674
|
+
getProducersOfMessage: 'Use this tool to find which services produce (send) a specific message (event, command, or query)',
|
|
675
|
+
getConsumersOfMessage: 'Use this tool to find which services consume (receive) a specific message (event, command, or query)',
|
|
676
|
+
analyzeChangeImpact:
|
|
677
|
+
'Use this tool to analyze the impact of changing a message. Returns all affected services (producers and consumers), the teams that own them, and the blast radius of the change',
|
|
678
|
+
explainBusinessFlow:
|
|
679
|
+
'Use this tool to get detailed information about a business flow (state machine). Returns the flow definition, steps, mermaid diagram if available, and related services',
|
|
680
|
+
getTeams:
|
|
681
|
+
'Use this tool to get all teams in EventCatalog. Teams are groups of users that own resources. Supports pagination via cursor.',
|
|
682
|
+
getTeam: 'Use this tool to get a specific team by its id. Returns team details including members.',
|
|
683
|
+
getUsers:
|
|
684
|
+
'Use this tool to get all users in EventCatalog. Users are individuals who can own resources or be members of teams. Supports pagination via cursor.',
|
|
685
|
+
getUser: 'Use this tool to get a specific user by their id. Returns user details including role and contact info.',
|
|
686
|
+
findMessageBySchemaId:
|
|
687
|
+
'Use this tool when a user shares a schema file (Avro, JSON Schema, Protobuf) and wants to find it in EventCatalog. Look for "x-eventcatalog-id" and "x-eventcatalog-version" in the schema - these may be properties in the schema OR in comments (e.g. // x-eventcatalog-id: OrderCreated). Pass the id as messageId. If version exists, pass it as messageVersion, otherwise omit it to get the latest version. Returns the message resource along with its producers and consumers.',
|
|
688
|
+
explainUbiquitousLanguageTerms:
|
|
689
|
+
'Use this tool to explain ubiquitous language terms from Domain-Driven Design for a specific domain. Returns the glossary of terms defined for the domain and its subdomains, including duplicate term detection.',
|
|
690
|
+
};
|