@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.
- 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-OBA7TFHM.js → chunk-32ZHIOUO.js} +3 -3
- package/dist/{chunk-XTSY2LTR.js → chunk-GZOFTODZ.js} +1 -1
- package/dist/{chunk-DEGSBGEF.js → chunk-RQCRNCF6.js} +1 -1
- package/dist/{chunk-OZNVNRI7.js → chunk-TE4QQSPT.js} +1 -1
- package/dist/{chunk-NZUMNTJK.js → chunk-ZLUUUTWP.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/development/ask-your-architecture/03-mcp-server/getting-started.md +12 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.config.d.cts +5 -0
- package/dist/eventcatalog.config.d.ts +5 -0
- package/dist/eventcatalog.js +9 -9
- 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/src/components/CopyAsMarkdown.tsx +73 -5
- package/eventcatalog/src/components/McpConnectDialog.tsx +98 -0
- package/eventcatalog/src/components/McpIcon.tsx +14 -0
- package/eventcatalog/src/enterprise/feature.ts +1 -1
- package/eventcatalog/src/enterprise/mcp/mcp-scope.ts +159 -0
- package/eventcatalog/src/enterprise/mcp/mcp-scoped-tools.ts +197 -0
- package/eventcatalog/src/enterprise/mcp/mcp-server.ts +287 -176
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +13 -0
- package/eventcatalog/src/types/mcp-modules.d.ts +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the EventCatalog Commercial License.
|
|
3
|
+
* See /packages/core/eventcatalog/src/enterprise/LICENSE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import utils, { type CatalogGraphResourceType, type CatalogGraphRoot, type FlatCatalogGraph } from '@eventcatalog/sdk';
|
|
7
|
+
import { getCollection } from 'astro:content';
|
|
8
|
+
import { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
|
|
9
|
+
|
|
10
|
+
export const MCP_SCOPE_COLLECTIONS = [
|
|
11
|
+
'domains',
|
|
12
|
+
'systems',
|
|
13
|
+
'services',
|
|
14
|
+
'agents',
|
|
15
|
+
'events',
|
|
16
|
+
'commands',
|
|
17
|
+
'queries',
|
|
18
|
+
'flows',
|
|
19
|
+
'channels',
|
|
20
|
+
'entities',
|
|
21
|
+
'containers',
|
|
22
|
+
'data-products',
|
|
23
|
+
'adrs',
|
|
24
|
+
'diagrams',
|
|
25
|
+
] as const;
|
|
26
|
+
|
|
27
|
+
export type McpScopeCollection = (typeof MCP_SCOPE_COLLECTIONS)[number];
|
|
28
|
+
export type McpScopeKind = 'domain' | 'system';
|
|
29
|
+
|
|
30
|
+
export type McpScopeRef = {
|
|
31
|
+
kind: McpScopeKind;
|
|
32
|
+
id: string;
|
|
33
|
+
version?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type CatalogEntry = {
|
|
37
|
+
collection: string;
|
|
38
|
+
data: {
|
|
39
|
+
id: string;
|
|
40
|
+
version: string;
|
|
41
|
+
name?: string;
|
|
42
|
+
summary?: string;
|
|
43
|
+
hidden?: boolean;
|
|
44
|
+
[key: string]: any;
|
|
45
|
+
};
|
|
46
|
+
[key: string]: any;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type CollectionLoader = (collection: McpScopeCollection) => Promise<CatalogEntry[]>;
|
|
50
|
+
type GraphLoader = (root: CatalogGraphRoot, options: { flat: true }) => Promise<FlatCatalogGraph | undefined>;
|
|
51
|
+
|
|
52
|
+
const graphTypeToCollection: Record<CatalogGraphResourceType, McpScopeCollection> = {
|
|
53
|
+
domain: 'domains',
|
|
54
|
+
system: 'systems',
|
|
55
|
+
service: 'services',
|
|
56
|
+
agent: 'agents',
|
|
57
|
+
event: 'events',
|
|
58
|
+
command: 'commands',
|
|
59
|
+
query: 'queries',
|
|
60
|
+
flow: 'flows',
|
|
61
|
+
channel: 'channels',
|
|
62
|
+
entity: 'entities',
|
|
63
|
+
container: 'containers',
|
|
64
|
+
'data-product': 'data-products',
|
|
65
|
+
adr: 'adrs',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const entryKey = (id: string, version: string) => `${id}@${version}`;
|
|
69
|
+
const encodeUriPart = (value: string) => encodeURIComponent(value);
|
|
70
|
+
|
|
71
|
+
export class McpScopeNotFoundError extends Error {}
|
|
72
|
+
|
|
73
|
+
export class McpScope {
|
|
74
|
+
readonly entries = new Map<string, Map<string, CatalogEntry>>();
|
|
75
|
+
readonly ref: Required<McpScopeRef>;
|
|
76
|
+
readonly root: CatalogEntry;
|
|
77
|
+
readonly uriPrefix: string;
|
|
78
|
+
|
|
79
|
+
constructor(ref: McpScopeRef, root: CatalogEntry) {
|
|
80
|
+
this.ref = { ...ref, version: root.data.version } as Required<McpScopeRef>;
|
|
81
|
+
this.root = root;
|
|
82
|
+
this.uriPrefix = `eventcatalog://${ref.kind}s/${encodeUriPart(root.data.id)}/${encodeUriPart(root.data.version)}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
add(entry: CatalogEntry | undefined) {
|
|
86
|
+
if (!entry || entry.data.hidden === true) return false;
|
|
87
|
+
|
|
88
|
+
const collectionEntries = this.entries.get(entry.collection) ?? new Map<string, CatalogEntry>();
|
|
89
|
+
const key = entryKey(entry.data.id, entry.data.version);
|
|
90
|
+
if (collectionEntries.has(key)) return false;
|
|
91
|
+
|
|
92
|
+
collectionEntries.set(key, entry);
|
|
93
|
+
this.entries.set(entry.collection, collectionEntries);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
has(collection: string, id: string, version?: string) {
|
|
98
|
+
const entries = Array.from(this.entries.get(collection)?.values() ?? []);
|
|
99
|
+
return getItemsFromCollectionByIdAndSemverOrLatest(entries, id, version).length > 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
list(collection: string) {
|
|
103
|
+
return Array.from(this.entries.get(collection)?.values() ?? []);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
listAll() {
|
|
107
|
+
return Array.from(this.entries.values()).flatMap((entries) => Array.from(entries.values()));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
get name() {
|
|
111
|
+
return this.root.data.name || this.root.data.id;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const defaultCollectionLoader: CollectionLoader = async (collection) => getCollection(collection as any) as any;
|
|
116
|
+
const catalogDirectory = process.env.PROJECT_DIR || process.cwd();
|
|
117
|
+
const getCatalogGraph = utils(catalogDirectory).getGraph;
|
|
118
|
+
const defaultGraphLoader: GraphLoader = (root, options) => getCatalogGraph(root, options);
|
|
119
|
+
|
|
120
|
+
const scopeNotFound = (ref: McpScopeRef) =>
|
|
121
|
+
new McpScopeNotFoundError(
|
|
122
|
+
`${ref.kind === 'domain' ? 'Domain' : 'System'} not found: ${ref.id}${ref.version ? ` (${ref.version})` : ''}`
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
export async function resolveMcpScope(
|
|
126
|
+
ref: McpScopeRef,
|
|
127
|
+
loadCollection: CollectionLoader = defaultCollectionLoader,
|
|
128
|
+
loadGraph: GraphLoader = defaultGraphLoader
|
|
129
|
+
) {
|
|
130
|
+
const graph = await loadGraph({ type: ref.kind, id: ref.id, version: ref.version }, { flat: true });
|
|
131
|
+
if (!graph) throw scopeNotFound(ref);
|
|
132
|
+
|
|
133
|
+
const graphCollections = new Set(graph.resources.map((resource) => graphTypeToCollection[resource.type]));
|
|
134
|
+
|
|
135
|
+
const loadedCollections = await Promise.all(
|
|
136
|
+
Array.from(graphCollections).map(async (collection) => [collection, await loadCollection(collection)] as const)
|
|
137
|
+
);
|
|
138
|
+
const collections = new Map(
|
|
139
|
+
loadedCollections.map(([collection, entries]) => [
|
|
140
|
+
collection,
|
|
141
|
+
new Map(entries.map((entry) => [entryKey(entry.data.id, entry.data.version), entry])),
|
|
142
|
+
])
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const resolve = (collection: McpScopeCollection, id: string, version: string) =>
|
|
146
|
+
collections.get(collection)?.get(entryKey(id, version));
|
|
147
|
+
|
|
148
|
+
const rootCollection = graphTypeToCollection[graph.root.type];
|
|
149
|
+
const root = resolve(rootCollection, graph.root.id, graph.root.version);
|
|
150
|
+
if (!root || root.data.hidden === true) throw scopeNotFound(ref);
|
|
151
|
+
|
|
152
|
+
const scope = new McpScope(ref, root);
|
|
153
|
+
for (const resource of graph.resources) {
|
|
154
|
+
const collection = graphTypeToCollection[resource.type];
|
|
155
|
+
scope.add(resolve(collection, resource.id, resource.version));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return scope;
|
|
159
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the EventCatalog Commercial License.
|
|
3
|
+
* See /packages/core/eventcatalog/src/enterprise/LICENSE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
analyzeChangeImpact,
|
|
8
|
+
explainBusinessFlow,
|
|
9
|
+
explainUbiquitousLanguageTerms,
|
|
10
|
+
findMessageBySchemaId,
|
|
11
|
+
getConsumersOfMessage,
|
|
12
|
+
getMessagesProducedOrConsumedByResource,
|
|
13
|
+
getProducersOfMessage,
|
|
14
|
+
getResource,
|
|
15
|
+
getSchemaForResource,
|
|
16
|
+
paginate,
|
|
17
|
+
} from '@enterprise/tools/catalog-tools';
|
|
18
|
+
import type { McpScope } from './mcp-scope';
|
|
19
|
+
|
|
20
|
+
const notFound = (collection: string, id: string, version?: string) => ({
|
|
21
|
+
error: `Resource not found: ${collection}/${id}${version ? ` (${version})` : ''}`,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const isScopedReference = (scope: McpScope, resource: any, defaultCollection?: string) => {
|
|
25
|
+
const collection = resource?.collection || defaultCollection;
|
|
26
|
+
return Boolean(collection && resource?.id && scope.has(collection, resource.id, resource.version));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const filterScopedReferences = (scope: McpScope, resources: any[] | undefined, defaultCollection?: string) =>
|
|
30
|
+
(resources || []).filter((resource) => isScopedReference(scope, resource, defaultCollection));
|
|
31
|
+
|
|
32
|
+
export function createScopedCatalogTools(scope: McpScope) {
|
|
33
|
+
return {
|
|
34
|
+
async getResources(params: { collection: string; cursor?: string; search?: string }) {
|
|
35
|
+
let resources = scope.list(params.collection).map((resource) => ({
|
|
36
|
+
id: resource.data.id,
|
|
37
|
+
version: resource.data.version,
|
|
38
|
+
name: resource.data.name,
|
|
39
|
+
summary: resource.data.summary,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
if (params.search) {
|
|
43
|
+
const searchTerm = params.search.toLowerCase().trim();
|
|
44
|
+
resources = resources.filter(
|
|
45
|
+
(resource) =>
|
|
46
|
+
resource.id?.toLowerCase().includes(searchTerm) ||
|
|
47
|
+
resource.name?.toLowerCase().includes(searchTerm) ||
|
|
48
|
+
resource.summary?.toLowerCase().includes(searchTerm)
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = paginate(resources, params.cursor);
|
|
53
|
+
if ('error' in result) return result;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
resources: result.items,
|
|
57
|
+
nextCursor: result.nextCursor,
|
|
58
|
+
totalCount: result.totalCount,
|
|
59
|
+
scope: scope.ref,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async getResource(params: { collection: string; id: string; version: string }) {
|
|
64
|
+
if (!scope.has(params.collection, params.id, params.version)) return notFound(params.collection, params.id, params.version);
|
|
65
|
+
return getResource(params);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async getMessagesProducedOrConsumedByResource(params: {
|
|
69
|
+
resourceId: string;
|
|
70
|
+
resourceVersion: string;
|
|
71
|
+
resourceCollection: string;
|
|
72
|
+
}) {
|
|
73
|
+
if (!scope.has(params.resourceCollection, params.resourceId, params.resourceVersion)) {
|
|
74
|
+
return notFound(params.resourceCollection, params.resourceId, params.resourceVersion);
|
|
75
|
+
}
|
|
76
|
+
return getMessagesProducedOrConsumedByResource(params);
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async getSchemaForResource(params: { resourceId: string; resourceVersion: string; resourceCollection: string }) {
|
|
80
|
+
if (!scope.has(params.resourceCollection, params.resourceId, params.resourceVersion)) {
|
|
81
|
+
return notFound(params.resourceCollection, params.resourceId, params.resourceVersion);
|
|
82
|
+
}
|
|
83
|
+
return getSchemaForResource(params);
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
async findResourcesByOwner(params: { ownerId: string }) {
|
|
87
|
+
const resources = scope
|
|
88
|
+
.listAll()
|
|
89
|
+
.filter((resource) =>
|
|
90
|
+
(resource.data.owners || []).some((owner: string | { id: string }) =>
|
|
91
|
+
typeof owner === 'string' ? owner === params.ownerId : owner.id === params.ownerId
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
.map((resource) => ({
|
|
95
|
+
collection: resource.collection,
|
|
96
|
+
id: resource.data.id,
|
|
97
|
+
version: resource.data.version,
|
|
98
|
+
name: resource.data.name || resource.data.id,
|
|
99
|
+
}));
|
|
100
|
+
|
|
101
|
+
return { ownerId: params.ownerId, totalCount: resources.length, resources, scope: scope.ref };
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
async getProducersOfMessage(params: { messageId: string; messageVersion: string; messageCollection: string }) {
|
|
105
|
+
if (!scope.has(params.messageCollection, params.messageId, params.messageVersion)) {
|
|
106
|
+
return notFound(params.messageCollection, params.messageId, params.messageVersion);
|
|
107
|
+
}
|
|
108
|
+
const result = await getProducersOfMessage(params);
|
|
109
|
+
if ('error' in result) return result;
|
|
110
|
+
const producers = filterScopedReferences(scope, result.producers);
|
|
111
|
+
return { ...result, producers, count: producers.length, scope: scope.ref };
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async getConsumersOfMessage(params: { messageId: string; messageVersion: string; messageCollection: string }) {
|
|
115
|
+
if (!scope.has(params.messageCollection, params.messageId, params.messageVersion)) {
|
|
116
|
+
return notFound(params.messageCollection, params.messageId, params.messageVersion);
|
|
117
|
+
}
|
|
118
|
+
const result = await getConsumersOfMessage(params);
|
|
119
|
+
if ('error' in result) return result;
|
|
120
|
+
const consumers = filterScopedReferences(scope, result.consumers);
|
|
121
|
+
return { ...result, consumers, count: consumers.length, scope: scope.ref };
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
async analyzeChangeImpact(params: { messageId: string; messageVersion: string; messageCollection: string }) {
|
|
125
|
+
if (!scope.has(params.messageCollection, params.messageId, params.messageVersion)) {
|
|
126
|
+
return notFound(params.messageCollection, params.messageId, params.messageVersion);
|
|
127
|
+
}
|
|
128
|
+
const result = await analyzeChangeImpact(params);
|
|
129
|
+
if ('error' in result) return result;
|
|
130
|
+
|
|
131
|
+
const producers = filterScopedReferences(scope, result.producers);
|
|
132
|
+
const consumers = filterScopedReferences(scope, result.consumers);
|
|
133
|
+
const affected = [...producers, ...consumers];
|
|
134
|
+
const affectedOwners = new Set<string>();
|
|
135
|
+
affected.forEach((resource) =>
|
|
136
|
+
(resource.owners || []).forEach((owner: string | { id: string }) =>
|
|
137
|
+
affectedOwners.add(typeof owner === 'string' ? owner : owner.id)
|
|
138
|
+
)
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
...result,
|
|
143
|
+
scope: scope.ref,
|
|
144
|
+
impact: {
|
|
145
|
+
producerCount: producers.length,
|
|
146
|
+
consumerCount: consumers.length,
|
|
147
|
+
totalResourcesAffected: new Set(affected.map((resource) => `${resource.collection}:${resource.id}`)).size,
|
|
148
|
+
totalServicesAffected: new Set(
|
|
149
|
+
affected.filter((resource) => resource.collection === 'services').map((resource) => resource.id)
|
|
150
|
+
).size,
|
|
151
|
+
totalAgentsAffected: new Set(
|
|
152
|
+
affected.filter((resource) => resource.collection === 'agents').map((resource) => resource.id)
|
|
153
|
+
).size,
|
|
154
|
+
teamsAffected: Array.from(affectedOwners),
|
|
155
|
+
},
|
|
156
|
+
producers,
|
|
157
|
+
consumers,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async explainBusinessFlow(params: { flowId: string; flowVersion: string }) {
|
|
162
|
+
if (!scope.has('flows', params.flowId, params.flowVersion)) return notFound('flows', params.flowId, params.flowVersion);
|
|
163
|
+
const result = await explainBusinessFlow(params);
|
|
164
|
+
if ('error' in result) return result;
|
|
165
|
+
return {
|
|
166
|
+
...result,
|
|
167
|
+
relatedServices: filterScopedReferences(scope, result.relatedServices, 'services'),
|
|
168
|
+
relatedAgents: filterScopedReferences(scope, result.relatedAgents, 'agents'),
|
|
169
|
+
scope: scope.ref,
|
|
170
|
+
};
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
async findMessageBySchemaId(params: {
|
|
174
|
+
messageId: string;
|
|
175
|
+
messageVersion?: string;
|
|
176
|
+
collection?: 'events' | 'commands' | 'queries';
|
|
177
|
+
}) {
|
|
178
|
+
const result = await findMessageBySchemaId(params);
|
|
179
|
+
if ('error' in result) return result;
|
|
180
|
+
if (!scope.has(result.resource.collection, result.resource.id, result.resource.version)) {
|
|
181
|
+
return notFound(result.resource.collection, params.messageId, params.messageVersion);
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
...result,
|
|
185
|
+
producers: filterScopedReferences(scope, result.producers),
|
|
186
|
+
consumers: filterScopedReferences(scope, result.consumers),
|
|
187
|
+
scope: scope.ref,
|
|
188
|
+
};
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
async explainUbiquitousLanguageTerms(params: { domainId: string; domainVersion?: string }) {
|
|
192
|
+
if (!scope.has('domains', params.domainId, params.domainVersion))
|
|
193
|
+
return notFound('domains', params.domainId, params.domainVersion);
|
|
194
|
+
return explainUbiquitousLanguageTerms(params);
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|