@rekog/mcp-nest 1.9.4 → 1.9.6
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/README.md +5 -1
- package/dist/mcp/decorators/prompt.decorator.d.ts +1 -2
- package/dist/mcp/decorators/prompt.decorator.d.ts.map +1 -1
- package/dist/mcp/decorators/prompt.decorator.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +2 -2
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/interfaces/dynamic-prompt.interface.d.ts +11 -0
- package/dist/mcp/interfaces/dynamic-prompt.interface.d.ts.map +1 -0
- package/dist/mcp/interfaces/dynamic-prompt.interface.js +3 -0
- package/dist/mcp/interfaces/dynamic-prompt.interface.js.map +1 -0
- package/dist/mcp/interfaces/dynamic-resource.interface.d.ts +11 -0
- package/dist/mcp/interfaces/dynamic-resource.interface.d.ts.map +1 -0
- package/dist/mcp/interfaces/dynamic-resource.interface.js +3 -0
- package/dist/mcp/interfaces/dynamic-resource.interface.js.map +1 -0
- package/dist/mcp/interfaces/dynamic-tool.interface.js.map +1 -1
- package/dist/mcp/interfaces/index.d.ts +2 -0
- package/dist/mcp/interfaces/index.d.ts.map +1 -1
- package/dist/mcp/interfaces/index.js.map +1 -1
- package/dist/mcp/mcp.module.d.ts.map +1 -1
- package/dist/mcp/mcp.module.js +15 -11
- package/dist/mcp/mcp.module.js.map +1 -1
- package/dist/mcp/services/handlers/mcp-handler.base.d.ts +4 -4
- package/dist/mcp/services/handlers/mcp-handler.base.d.ts.map +1 -1
- package/dist/mcp/services/handlers/mcp-handler.base.js.map +1 -1
- package/dist/mcp/services/handlers/mcp-prompts.handler.d.ts +2 -2
- package/dist/mcp/services/handlers/mcp-prompts.handler.d.ts.map +1 -1
- package/dist/mcp/services/handlers/mcp-prompts.handler.js +13 -3
- package/dist/mcp/services/handlers/mcp-prompts.handler.js.map +1 -1
- package/dist/mcp/services/handlers/mcp-resources.handler.d.ts +2 -2
- package/dist/mcp/services/handlers/mcp-resources.handler.d.ts.map +1 -1
- package/dist/mcp/services/handlers/mcp-resources.handler.js +12 -2
- package/dist/mcp/services/handlers/mcp-resources.handler.js.map +1 -1
- package/dist/mcp/services/handlers/mcp-tools.handler.d.ts +2 -2
- package/dist/mcp/services/handlers/mcp-tools.handler.d.ts.map +1 -1
- package/dist/mcp/services/handlers/mcp-tools.handler.js +5 -5
- package/dist/mcp/services/handlers/mcp-tools.handler.js.map +1 -1
- package/dist/mcp/services/mcp-dynamic-registry.service.d.ts +26 -0
- package/dist/mcp/services/mcp-dynamic-registry.service.d.ts.map +1 -0
- package/dist/mcp/services/mcp-dynamic-registry.service.js +133 -0
- package/dist/mcp/services/mcp-dynamic-registry.service.js.map +1 -0
- package/dist/mcp/services/mcp-executor.service.d.ts +2 -2
- package/dist/mcp/services/mcp-executor.service.d.ts.map +1 -1
- package/dist/mcp/services/mcp-executor.service.js +2 -2
- package/dist/mcp/services/mcp-executor.service.js.map +1 -1
- package/dist/mcp/services/{mcp-registry.service.d.ts → mcp-registry-discovery.service.d.ts} +16 -15
- package/dist/mcp/services/mcp-registry-discovery.service.d.ts.map +1 -0
- package/dist/mcp/services/{mcp-registry.service.js → mcp-registry-discovery.service.js} +41 -25
- package/dist/mcp/services/mcp-registry-discovery.service.js.map +1 -0
- package/dist/mcp/services/mcp-sse.service.d.ts +2 -2
- package/dist/mcp/services/mcp-sse.service.d.ts.map +1 -1
- package/dist/mcp/services/mcp-sse.service.js +2 -2
- package/dist/mcp/services/mcp-sse.service.js.map +1 -1
- package/dist/mcp/services/mcp-streamable-http.service.d.ts +2 -2
- package/dist/mcp/services/mcp-streamable-http.service.d.ts.map +1 -1
- package/dist/mcp/services/mcp-streamable-http.service.js +2 -2
- package/dist/mcp/services/mcp-streamable-http.service.js.map +1 -1
- package/dist/mcp/services/tool-authorization.service.d.ts +4 -4
- package/dist/mcp/services/tool-authorization.service.d.ts.map +1 -1
- package/dist/mcp/services/tool-authorization.service.js.map +1 -1
- package/dist/mcp/transport/stdio.service.d.ts +2 -2
- package/dist/mcp/transport/stdio.service.d.ts.map +1 -1
- package/dist/mcp/transport/stdio.service.js +7 -7
- package/dist/mcp/transport/stdio.service.js.map +1 -1
- package/dist/mcp/utils/capabilities-builder.d.ts +2 -2
- package/dist/mcp/utils/capabilities-builder.d.ts.map +1 -1
- package/dist/mcp/utils/capabilities-builder.js.map +1 -1
- package/dist/mcp/utils/mcp-server.factory.d.ts +2 -2
- package/dist/mcp/utils/mcp-server.factory.d.ts.map +1 -1
- package/dist/mcp/utils/mcp-server.factory.js.map +1 -1
- package/package.json +9 -9
- package/src/mcp/decorators/prompt.decorator.ts +1 -1
- package/src/mcp/index.ts +2 -2
- package/src/mcp/interfaces/dynamic-prompt.interface.ts +20 -0
- package/src/mcp/interfaces/dynamic-resource.interface.ts +22 -0
- package/src/mcp/interfaces/dynamic-tool.interface.ts +2 -2
- package/src/mcp/interfaces/index.ts +10 -0
- package/src/mcp/mcp.module.ts +15 -11
- package/src/mcp/services/handlers/mcp-handler.base.ts +6 -3
- package/src/mcp/services/handlers/mcp-prompts.handler.ts +36 -4
- package/src/mcp/services/handlers/mcp-resources.handler.ts +24 -2
- package/src/mcp/services/handlers/mcp-tools.handler.ts +11 -8
- package/src/mcp/services/mcp-dynamic-registry.service.ts +236 -0
- package/src/mcp/services/mcp-executor.service.ts +2 -2
- package/src/mcp/services/{mcp-registry.service.spec.ts → mcp-registry-discovery.service.spec.ts} +18 -12
- package/src/mcp/services/{mcp-registry.service.ts → mcp-registry-discovery.service.ts} +77 -35
- package/src/mcp/services/mcp-sse.service.ts +2 -2
- package/src/mcp/services/mcp-streamable-http.service.ts +2 -2
- package/src/mcp/services/tool-authorization.service.ts +4 -4
- package/src/mcp/transport/stdio.service.ts +8 -8
- package/src/mcp/utils/capabilities-builder.ts +2 -2
- package/src/mcp/utils/mcp-server.factory.ts +2 -2
- package/dist/authz/services/jwt-token.service.spec.d.ts +0 -2
- package/dist/authz/services/jwt-token.service.spec.d.ts.map +0 -1
- package/dist/authz/services/jwt-token.service.spec.js +0 -86
- package/dist/authz/services/jwt-token.service.spec.js.map +0 -1
- package/dist/authz/stores/memory-store.service.spec.d.ts +0 -2
- package/dist/authz/stores/memory-store.service.spec.d.ts.map +0 -1
- package/dist/authz/stores/memory-store.service.spec.js +0 -382
- package/dist/authz/stores/memory-store.service.spec.js.map +0 -1
- package/dist/authz/stores/typeorm/typeorm-store.service.spec.d.ts +0 -2
- package/dist/authz/stores/typeorm/typeorm-store.service.spec.d.ts.map +0 -1
- package/dist/authz/stores/typeorm/typeorm-store.service.spec.js +0 -340
- package/dist/authz/stores/typeorm/typeorm-store.service.spec.js.map +0 -1
- package/dist/mcp/services/mcp-registry.service.d.ts.map +0 -1
- package/dist/mcp/services/mcp-registry.service.js.map +0 -1
- package/dist/mcp/services/mcp-registry.service.spec.d.ts +0 -2
- package/dist/mcp/services/mcp-registry.service.spec.d.ts.map +0 -1
- package/dist/mcp/services/mcp-registry.service.spec.js +0 -245
- package/dist/mcp/services/mcp-registry.service.spec.js.map +0 -1
- package/dist/mcp/services/mcp-tool-builder.service.d.ts +0 -15
- package/dist/mcp/services/mcp-tool-builder.service.d.ts.map +0 -1
- package/dist/mcp/services/mcp-tool-builder.service.js +0 -70
- package/dist/mcp/services/mcp-tool-builder.service.js.map +0 -1
- package/dist/mcp/transport/custom-decorator.spec.d.ts +0 -2
- package/dist/mcp/transport/custom-decorator.spec.d.ts.map +0 -1
- package/dist/mcp/transport/custom-decorator.spec.js +0 -34
- package/dist/mcp/transport/custom-decorator.spec.js.map +0 -1
- package/dist/mcp/utils/mcp-logger.factory.spec.d.ts +0 -2
- package/dist/mcp/utils/mcp-logger.factory.spec.d.ts.map +0 -1
- package/dist/mcp/utils/mcp-logger.factory.spec.js +0 -150
- package/dist/mcp/utils/mcp-logger.factory.spec.js.map +0 -1
- package/dist/mcp/utils/normalize-endpoint.spec.d.ts +0 -2
- package/dist/mcp/utils/normalize-endpoint.spec.d.ts.map +0 -1
- package/dist/mcp/utils/normalize-endpoint.spec.js +0 -40
- package/dist/mcp/utils/normalize-endpoint.spec.js.map +0 -1
- package/src/mcp/services/mcp-tool-builder.service.ts +0 -151
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { Injectable, Inject, Logger } from '@nestjs/common';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import {
|
|
4
|
+
DynamicToolDefinition,
|
|
5
|
+
DynamicToolHandler,
|
|
6
|
+
} from '../interfaces/dynamic-tool.interface';
|
|
7
|
+
import {
|
|
8
|
+
DynamicResourceDefinition,
|
|
9
|
+
DynamicResourceHandler,
|
|
10
|
+
} from '../interfaces/dynamic-resource.interface';
|
|
11
|
+
import {
|
|
12
|
+
DynamicPromptDefinition,
|
|
13
|
+
DynamicPromptHandler,
|
|
14
|
+
} from '../interfaces/dynamic-prompt.interface';
|
|
15
|
+
import { McpRegistryDiscoveryService } from './mcp-registry-discovery.service';
|
|
16
|
+
import type { McpOptions } from '../interfaces';
|
|
17
|
+
import { createMcpLogger } from '../utils/mcp-logger.factory';
|
|
18
|
+
import { ToolMetadata } from '../decorators/tool.decorator';
|
|
19
|
+
import { ResourceMetadata } from '../decorators/resource.decorator';
|
|
20
|
+
import { PromptMetadata } from '../decorators/prompt.decorator';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Symbol used to identify dynamic tools in the registry.
|
|
24
|
+
* When a tool's providerClass equals this token, it's a dynamic tool.
|
|
25
|
+
*/
|
|
26
|
+
export const DYNAMIC_TOOL_HANDLER_TOKEN = Symbol('DYNAMIC_TOOL_HANDLER');
|
|
27
|
+
export const DYNAMIC_RESOURCE_HANDLER_TOKEN = Symbol(
|
|
28
|
+
'DYNAMIC_RESOURCE_HANDLER',
|
|
29
|
+
);
|
|
30
|
+
export const DYNAMIC_PROMPT_HANDLER_TOKEN = Symbol('DYNAMIC_PROMPT_HANDLER');
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Global maps of dynamic capability handlers, scoped by moduleId.
|
|
34
|
+
* Using module-level Maps ensures handlers persist across different
|
|
35
|
+
* McpRegistryService instances that may be created by the DI container.
|
|
36
|
+
*/
|
|
37
|
+
const globalHandlers = new Map<string, Map<string, DynamicToolHandler>>();
|
|
38
|
+
const globalResourceHandlers = new Map<
|
|
39
|
+
string,
|
|
40
|
+
Map<string, DynamicResourceHandler>
|
|
41
|
+
>();
|
|
42
|
+
const globalPromptHandlers = new Map<
|
|
43
|
+
string,
|
|
44
|
+
Map<string, DynamicPromptHandler>
|
|
45
|
+
>();
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Service for programmatically registering MCP tools at runtime.
|
|
49
|
+
*
|
|
50
|
+
* Use this to register tools with descriptions/parameters from databases
|
|
51
|
+
* or other dynamic sources during application bootstrap.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* @Injectable()
|
|
56
|
+
* export class DynamicToolsService implements OnModuleInit {
|
|
57
|
+
* constructor(
|
|
58
|
+
* private readonly capabilityBuilder: McpRegistryService,
|
|
59
|
+
* private readonly dbService: DatabaseService,
|
|
60
|
+
* ) {}
|
|
61
|
+
*
|
|
62
|
+
* async onModuleInit() {
|
|
63
|
+
* const collections = await this.dbService.getCollections();
|
|
64
|
+
*
|
|
65
|
+
* this.capabilityBuilder.registerTool({
|
|
66
|
+
* name: 'search-knowledge',
|
|
67
|
+
* description: `Search collections: ${collections.join(', ')}`,
|
|
68
|
+
* parameters: z.object({ query: z.string() }),
|
|
69
|
+
* handler: async (args, context) => {
|
|
70
|
+
* const results = await this.dbService.search(args.query);
|
|
71
|
+
* return { content: [{ type: 'text', text: JSON.stringify(results) }] };
|
|
72
|
+
* },
|
|
73
|
+
* });
|
|
74
|
+
* }
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
@Injectable()
|
|
79
|
+
export class McpRegistryService {
|
|
80
|
+
private readonly logger: Logger;
|
|
81
|
+
|
|
82
|
+
constructor(
|
|
83
|
+
private readonly registry: McpRegistryDiscoveryService,
|
|
84
|
+
@Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
|
|
85
|
+
@Inject('MCP_OPTIONS') private readonly options: McpOptions,
|
|
86
|
+
) {
|
|
87
|
+
this.logger = createMcpLogger(McpRegistryService.name, this.options);
|
|
88
|
+
[globalHandlers, globalResourceHandlers, globalPromptHandlers].forEach(
|
|
89
|
+
(store) => {
|
|
90
|
+
if (!store.has(mcpModuleId)) {
|
|
91
|
+
store.set(mcpModuleId, new Map());
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Register a dynamic tool for the current MCP server.
|
|
99
|
+
*
|
|
100
|
+
* Tools registered here will appear in the `tools/list` response and
|
|
101
|
+
* can be called via `tools/call` just like decorator-based tools.
|
|
102
|
+
*
|
|
103
|
+
* @param definition - The tool definition including name, description, parameters, and handler
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* registry.registerTool({
|
|
108
|
+
* name: 'search-knowledge',
|
|
109
|
+
* description: await getDescriptionFromDB(),
|
|
110
|
+
* parameters: z.object({ query: z.string() }),
|
|
111
|
+
* handler: async (args, context) => {
|
|
112
|
+
* const results = await searchService.search(args.query);
|
|
113
|
+
* return { content: [{ type: 'text', text: JSON.stringify(results) }] };
|
|
114
|
+
* },
|
|
115
|
+
* });
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
registerTool(definition: DynamicToolDefinition): void {
|
|
119
|
+
this.logger.debug(`Registering dynamic tool: ${definition.name}`);
|
|
120
|
+
|
|
121
|
+
// Store the handler for later execution (scoped by moduleId)
|
|
122
|
+
const moduleHandlers = globalHandlers.get(this.mcpModuleId)!;
|
|
123
|
+
moduleHandlers.set(definition.name, definition.handler);
|
|
124
|
+
|
|
125
|
+
// Default to empty object schema if no parameters provided (matches @Tool decorator behavior)
|
|
126
|
+
const parameters = definition.parameters ?? z.object({});
|
|
127
|
+
|
|
128
|
+
// Build metadata matching ToolMetadata interface
|
|
129
|
+
const metadata: ToolMetadata = {
|
|
130
|
+
name: definition.name,
|
|
131
|
+
description: definition.description,
|
|
132
|
+
parameters,
|
|
133
|
+
outputSchema: definition.outputSchema,
|
|
134
|
+
annotations: definition.annotations,
|
|
135
|
+
_meta: definition._meta,
|
|
136
|
+
isPublic: definition.isPublic,
|
|
137
|
+
requiredScopes: definition.requiredScopes,
|
|
138
|
+
requiredRoles: definition.requiredRoles,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Register with the registry
|
|
142
|
+
this.registry.registerDynamicCapability(this.mcpModuleId, {
|
|
143
|
+
type: 'tool',
|
|
144
|
+
metadata,
|
|
145
|
+
providerClass: DYNAMIC_TOOL_HANDLER_TOKEN,
|
|
146
|
+
methodName: definition.name,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
getHandler(toolName: string): DynamicToolHandler | undefined {
|
|
151
|
+
return globalHandlers.get(this.mcpModuleId)?.get(toolName);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
static getHandlerByModuleId(
|
|
155
|
+
mcpModuleId: string,
|
|
156
|
+
toolName: string,
|
|
157
|
+
): DynamicToolHandler | undefined {
|
|
158
|
+
return globalHandlers.get(mcpModuleId)?.get(toolName);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
registerResource(definition: DynamicResourceDefinition): void {
|
|
162
|
+
this.logger.debug(`Registering dynamic resource: ${definition.uri}`);
|
|
163
|
+
|
|
164
|
+
globalResourceHandlers
|
|
165
|
+
.get(this.mcpModuleId)!
|
|
166
|
+
.set(definition.uri, definition.handler);
|
|
167
|
+
|
|
168
|
+
const metadata: ResourceMetadata = {
|
|
169
|
+
uri: definition.uri,
|
|
170
|
+
name: definition.name ?? definition.uri,
|
|
171
|
+
description: definition.description,
|
|
172
|
+
mimeType: definition.mimeType,
|
|
173
|
+
_meta: definition._meta,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
this.registry.registerDynamicCapability(this.mcpModuleId, {
|
|
177
|
+
type: 'resource',
|
|
178
|
+
metadata,
|
|
179
|
+
providerClass: DYNAMIC_RESOURCE_HANDLER_TOKEN,
|
|
180
|
+
methodName: definition.uri,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
static getResourceHandlerByModuleId(
|
|
185
|
+
mcpModuleId: string,
|
|
186
|
+
uri: string,
|
|
187
|
+
): DynamicResourceHandler | undefined {
|
|
188
|
+
return globalResourceHandlers.get(mcpModuleId)?.get(uri);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
registerPrompt(definition: DynamicPromptDefinition): void {
|
|
192
|
+
this.logger.debug(`Registering dynamic prompt: ${definition.name}`);
|
|
193
|
+
|
|
194
|
+
globalPromptHandlers
|
|
195
|
+
.get(this.mcpModuleId)!
|
|
196
|
+
.set(definition.name, definition.handler);
|
|
197
|
+
|
|
198
|
+
const metadata: PromptMetadata = {
|
|
199
|
+
name: definition.name,
|
|
200
|
+
description: definition.description,
|
|
201
|
+
parameters: definition.parameters,
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
this.registry.registerDynamicCapability(this.mcpModuleId, {
|
|
205
|
+
type: 'prompt',
|
|
206
|
+
metadata,
|
|
207
|
+
providerClass: DYNAMIC_PROMPT_HANDLER_TOKEN,
|
|
208
|
+
methodName: definition.name,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
static getPromptHandlerByModuleId(
|
|
213
|
+
mcpModuleId: string,
|
|
214
|
+
name: string,
|
|
215
|
+
): DynamicPromptHandler | undefined {
|
|
216
|
+
return globalPromptHandlers.get(mcpModuleId)?.get(name);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
removeTool(name: string): void {
|
|
220
|
+
this.logger.debug(`Removing dynamic tool: ${name}`);
|
|
221
|
+
globalHandlers.get(this.mcpModuleId)?.delete(name);
|
|
222
|
+
this.registry.removeDynamicCapability(this.mcpModuleId, 'tool', name);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
removeResource(uri: string): void {
|
|
226
|
+
this.logger.debug(`Removing dynamic resource: ${uri}`);
|
|
227
|
+
globalResourceHandlers.get(this.mcpModuleId)?.delete(uri);
|
|
228
|
+
this.registry.removeDynamicCapability(this.mcpModuleId, 'resource', uri);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
removePrompt(name: string): void {
|
|
232
|
+
this.logger.debug(`Removing dynamic prompt: ${name}`);
|
|
233
|
+
globalPromptHandlers.get(this.mcpModuleId)?.delete(name);
|
|
234
|
+
this.registry.removeDynamicCapability(this.mcpModuleId, 'prompt', name);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
|
|
3
3
|
import { ModuleRef, Reflector } from '@nestjs/core';
|
|
4
|
-
import {
|
|
4
|
+
import { McpRegistryDiscoveryService } from './mcp-registry-discovery.service';
|
|
5
5
|
import { McpToolsHandler } from './handlers/mcp-tools.handler';
|
|
6
6
|
import { McpResourcesHandler } from './handlers/mcp-resources.handler';
|
|
7
7
|
import { McpPromptsHandler } from './handlers/mcp-prompts.handler';
|
|
@@ -22,7 +22,7 @@ export class McpExecutorService {
|
|
|
22
22
|
|
|
23
23
|
constructor(
|
|
24
24
|
moduleRef: ModuleRef,
|
|
25
|
-
registry:
|
|
25
|
+
registry: McpRegistryDiscoveryService,
|
|
26
26
|
@Inject('MCP_MODULE_ID') mcpModuleId: string,
|
|
27
27
|
@Inject('MCP_OPTIONS') options: McpOptions,
|
|
28
28
|
authService: ToolAuthorizationService,
|
package/src/mcp/services/{mcp-registry.service.spec.ts → mcp-registry-discovery.service.spec.ts}
RENAMED
|
@@ -5,13 +5,13 @@ import {
|
|
|
5
5
|
ValueProvider,
|
|
6
6
|
} from '@nestjs/common';
|
|
7
7
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
8
|
-
import {
|
|
8
|
+
import { McpRegistryDiscoveryService } from './mcp-registry-discovery.service';
|
|
9
9
|
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
|
|
10
10
|
import { Tool } from '../decorators/tool.decorator';
|
|
11
11
|
import { McpModule } from '../mcp.module';
|
|
12
12
|
|
|
13
|
-
describe('
|
|
14
|
-
let service:
|
|
13
|
+
describe('McpRegistryDiscoveryService', () => {
|
|
14
|
+
let service: McpRegistryDiscoveryService;
|
|
15
15
|
const mockMcpModuleId = 'test-mcp-module-id';
|
|
16
16
|
|
|
17
17
|
const mockResource = (name: string, uri: string) => ({
|
|
@@ -31,7 +31,7 @@ describe('McpRegistryService', () => {
|
|
|
31
31
|
beforeEach(async () => {
|
|
32
32
|
const module: TestingModule = await Test.createTestingModule({
|
|
33
33
|
providers: [
|
|
34
|
-
|
|
34
|
+
McpRegistryDiscoveryService,
|
|
35
35
|
{
|
|
36
36
|
provide: DiscoveryService,
|
|
37
37
|
useValue: {
|
|
@@ -43,7 +43,9 @@ describe('McpRegistryService', () => {
|
|
|
43
43
|
],
|
|
44
44
|
}).compile();
|
|
45
45
|
|
|
46
|
-
service = module.get<
|
|
46
|
+
service = module.get<McpRegistryDiscoveryService>(
|
|
47
|
+
McpRegistryDiscoveryService,
|
|
48
|
+
);
|
|
47
49
|
|
|
48
50
|
const mockResources = [
|
|
49
51
|
mockResource('res0', '/posts/comments'),
|
|
@@ -57,7 +59,7 @@ describe('McpRegistryService', () => {
|
|
|
57
59
|
mockResourceTemplate('resTemplate2', '/templates/{type}/{id}'),
|
|
58
60
|
];
|
|
59
61
|
|
|
60
|
-
(service as any).
|
|
62
|
+
(service as any).discoveredCapabilitiesByMcpModuleId = new Map([
|
|
61
63
|
[mockMcpModuleId, [...mockResources, ...mockResourceTemplates]],
|
|
62
64
|
]);
|
|
63
65
|
});
|
|
@@ -129,7 +131,7 @@ describe('McpRegistryService', () => {
|
|
|
129
131
|
* | |
|
|
130
132
|
* ToolsA ToolsB
|
|
131
133
|
*/
|
|
132
|
-
describe('
|
|
134
|
+
describe('McpRegistryDiscoveryService - Multiple discovery roots', () => {
|
|
133
135
|
const mcpModuleA = McpModule.forRoot({ name: 'server-a', version: '0.0.1' });
|
|
134
136
|
const mcpModuleB = McpModule.forRoot({ name: 'server-b', version: '0.0.1' });
|
|
135
137
|
|
|
@@ -169,7 +171,7 @@ describe('McpRegistryService - Multiple discovery roots', () => {
|
|
|
169
171
|
})
|
|
170
172
|
class ModuleB {}
|
|
171
173
|
|
|
172
|
-
let service:
|
|
174
|
+
let service: McpRegistryDiscoveryService;
|
|
173
175
|
const idModuleA = getMcpModuleId(mcpModuleA);
|
|
174
176
|
const idModuleB = getMcpModuleId(mcpModuleB);
|
|
175
177
|
|
|
@@ -178,7 +180,9 @@ describe('McpRegistryService - Multiple discovery roots', () => {
|
|
|
178
180
|
imports: [ModuleA, ModuleB],
|
|
179
181
|
}).compile();
|
|
180
182
|
|
|
181
|
-
service = module.get<
|
|
183
|
+
service = module.get<McpRegistryDiscoveryService>(
|
|
184
|
+
McpRegistryDiscoveryService,
|
|
185
|
+
);
|
|
182
186
|
service.onApplicationBootstrap();
|
|
183
187
|
});
|
|
184
188
|
|
|
@@ -215,7 +219,7 @@ describe('McpRegistryService - Multiple discovery roots', () => {
|
|
|
215
219
|
* |
|
|
216
220
|
* Tools (forFeature to both)
|
|
217
221
|
*/
|
|
218
|
-
describe('
|
|
222
|
+
describe('McpRegistryDiscoveryService - Single discovery root with multiple MCP servers', () => {
|
|
219
223
|
const mcpModuleA = McpModule.forRoot({ name: 'server-a', version: '0.0.1' });
|
|
220
224
|
const mcpModuleB = McpModule.forRoot({ name: 'server-b', version: '0.0.1' });
|
|
221
225
|
|
|
@@ -248,14 +252,16 @@ describe('McpRegistryService - Single discovery root with multiple MCP servers',
|
|
|
248
252
|
})
|
|
249
253
|
class AppModule {}
|
|
250
254
|
|
|
251
|
-
let service:
|
|
255
|
+
let service: McpRegistryDiscoveryService;
|
|
252
256
|
|
|
253
257
|
beforeAll(async () => {
|
|
254
258
|
const module: TestingModule = await Test.createTestingModule({
|
|
255
259
|
imports: [AppModule],
|
|
256
260
|
}).compile();
|
|
257
261
|
|
|
258
|
-
service = module.get<
|
|
262
|
+
service = module.get<McpRegistryDiscoveryService>(
|
|
263
|
+
McpRegistryDiscoveryService,
|
|
264
|
+
);
|
|
259
265
|
service.onApplicationBootstrap();
|
|
260
266
|
});
|
|
261
267
|
|
|
@@ -35,9 +35,9 @@ import {
|
|
|
35
35
|
} from '../constants/feature-registration.constants';
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* Interface representing a discovered
|
|
38
|
+
* Interface representing a discovered capability
|
|
39
39
|
*/
|
|
40
|
-
export type
|
|
40
|
+
export type DiscoveredCapability<T extends object> = {
|
|
41
41
|
type: 'tool' | 'resource' | 'resource-template' | 'prompt';
|
|
42
42
|
metadata: T;
|
|
43
43
|
providerClass: InjectionToken;
|
|
@@ -50,10 +50,12 @@ export type InjectionTokenWithName = InjectionToken & { name: string };
|
|
|
50
50
|
* Singleton service that discovers and registers tools during application bootstrap
|
|
51
51
|
*/
|
|
52
52
|
@Injectable()
|
|
53
|
-
export class
|
|
53
|
+
export class McpRegistryDiscoveryService implements OnApplicationBootstrap {
|
|
54
54
|
private readonly logger: Logger;
|
|
55
|
-
private
|
|
56
|
-
|
|
55
|
+
private discoveredCapabilitiesByMcpModuleId: Map<
|
|
56
|
+
string,
|
|
57
|
+
DiscoveredCapability<any>[]
|
|
58
|
+
> = new Map();
|
|
57
59
|
|
|
58
60
|
constructor(
|
|
59
61
|
private readonly discovery: DiscoveryService,
|
|
@@ -61,7 +63,10 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
61
63
|
private readonly modulesContainer: ModulesContainer,
|
|
62
64
|
@Optional() @Inject('MCP_OPTIONS') private readonly options?: McpOptions,
|
|
63
65
|
) {
|
|
64
|
-
this.logger = createMcpLogger(
|
|
66
|
+
this.logger = createMcpLogger(
|
|
67
|
+
McpRegistryDiscoveryService.name,
|
|
68
|
+
this.options,
|
|
69
|
+
);
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
onApplicationBootstrap() {
|
|
@@ -386,11 +391,11 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
386
391
|
metadata['name'] = methodName;
|
|
387
392
|
}
|
|
388
393
|
|
|
389
|
-
if (!this.
|
|
390
|
-
this.
|
|
394
|
+
if (!this.discoveredCapabilitiesByMcpModuleId.has(mcpModuleId)) {
|
|
395
|
+
this.discoveredCapabilitiesByMcpModuleId.set(mcpModuleId, []);
|
|
391
396
|
}
|
|
392
397
|
|
|
393
|
-
this.
|
|
398
|
+
this.discoveredCapabilitiesByMcpModuleId.get(mcpModuleId)?.push({
|
|
394
399
|
type,
|
|
395
400
|
metadata,
|
|
396
401
|
providerClass: token,
|
|
@@ -463,11 +468,11 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
463
468
|
baseMetadata.guards = guards;
|
|
464
469
|
}
|
|
465
470
|
|
|
466
|
-
if (!this.
|
|
467
|
-
this.
|
|
471
|
+
if (!this.discoveredCapabilitiesByMcpModuleId.has(mcpModuleId)) {
|
|
472
|
+
this.discoveredCapabilitiesByMcpModuleId.set(mcpModuleId, []);
|
|
468
473
|
}
|
|
469
474
|
|
|
470
|
-
this.
|
|
475
|
+
this.discoveredCapabilitiesByMcpModuleId.get(mcpModuleId)?.push({
|
|
471
476
|
type: 'tool',
|
|
472
477
|
metadata: baseMetadata,
|
|
473
478
|
providerClass: token,
|
|
@@ -517,15 +522,15 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
517
522
|
* Return all discovered MCP module IDs
|
|
518
523
|
*/
|
|
519
524
|
getMcpModuleIds(): string[] {
|
|
520
|
-
return Array.from(this.
|
|
525
|
+
return Array.from(this.discoveredCapabilitiesByMcpModuleId.keys());
|
|
521
526
|
}
|
|
522
527
|
|
|
523
528
|
/**
|
|
524
529
|
* Get all discovered tools
|
|
525
530
|
*/
|
|
526
|
-
getTools(mcpModuleId: string):
|
|
531
|
+
getTools(mcpModuleId: string): DiscoveredCapability<ToolMetadata>[] {
|
|
527
532
|
return (
|
|
528
|
-
this.
|
|
533
|
+
this.discoveredCapabilitiesByMcpModuleId
|
|
529
534
|
.get(mcpModuleId)
|
|
530
535
|
?.filter((tool) => tool.type === 'tool') ?? []
|
|
531
536
|
);
|
|
@@ -537,7 +542,7 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
537
542
|
findTool(
|
|
538
543
|
mcpModuleId: string,
|
|
539
544
|
name: string,
|
|
540
|
-
):
|
|
545
|
+
): DiscoveredCapability<ToolMetadata> | undefined {
|
|
541
546
|
return this.getTools(mcpModuleId).find(
|
|
542
547
|
(tool) => tool.metadata.name === name,
|
|
543
548
|
);
|
|
@@ -546,9 +551,9 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
546
551
|
/**
|
|
547
552
|
* Get all discovered resources
|
|
548
553
|
*/
|
|
549
|
-
getResources(mcpModuleId: string):
|
|
554
|
+
getResources(mcpModuleId: string): DiscoveredCapability<ResourceMetadata>[] {
|
|
550
555
|
return (
|
|
551
|
-
this.
|
|
556
|
+
this.discoveredCapabilitiesByMcpModuleId
|
|
552
557
|
.get(mcpModuleId)
|
|
553
558
|
?.filter((tool) => tool.type === 'resource') ?? []
|
|
554
559
|
);
|
|
@@ -560,7 +565,7 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
560
565
|
findResource(
|
|
561
566
|
mcpModuleId: string,
|
|
562
567
|
name: string,
|
|
563
|
-
):
|
|
568
|
+
): DiscoveredCapability<ResourceMetadata> | undefined {
|
|
564
569
|
return this.getResources(mcpModuleId).find(
|
|
565
570
|
(tool) => tool.metadata.name === name,
|
|
566
571
|
);
|
|
@@ -571,9 +576,9 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
571
576
|
*/
|
|
572
577
|
getResourceTemplates(
|
|
573
578
|
mcpModuleId: string,
|
|
574
|
-
):
|
|
579
|
+
): DiscoveredCapability<ResourceTemplateMetadata>[] {
|
|
575
580
|
return (
|
|
576
|
-
this.
|
|
581
|
+
this.discoveredCapabilitiesByMcpModuleId
|
|
577
582
|
.get(mcpModuleId)
|
|
578
583
|
?.filter((tool) => tool.type === 'resource-template') ?? []
|
|
579
584
|
);
|
|
@@ -585,7 +590,7 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
585
590
|
findResourceTemplate(
|
|
586
591
|
mcpModuleId: string,
|
|
587
592
|
name: string,
|
|
588
|
-
):
|
|
593
|
+
): DiscoveredCapability<ResourceTemplateMetadata> | undefined {
|
|
589
594
|
return this.getResourceTemplates(mcpModuleId).find(
|
|
590
595
|
(tool) => tool.metadata.name === name,
|
|
591
596
|
);
|
|
@@ -594,9 +599,9 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
594
599
|
/**
|
|
595
600
|
* Get all discovered prompts
|
|
596
601
|
*/
|
|
597
|
-
getPrompts(mcpModuleId: string):
|
|
602
|
+
getPrompts(mcpModuleId: string): DiscoveredCapability<PromptMetadata>[] {
|
|
598
603
|
return (
|
|
599
|
-
this.
|
|
604
|
+
this.discoveredCapabilitiesByMcpModuleId
|
|
600
605
|
.get(mcpModuleId)
|
|
601
606
|
?.filter((tool) => tool.type === 'prompt') ?? []
|
|
602
607
|
);
|
|
@@ -608,7 +613,7 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
608
613
|
findPrompt(
|
|
609
614
|
mcpModuleId: string,
|
|
610
615
|
name: string,
|
|
611
|
-
):
|
|
616
|
+
): DiscoveredCapability<PromptMetadata> | undefined {
|
|
612
617
|
return this.getPrompts(mcpModuleId).find(
|
|
613
618
|
(tool) => tool.metadata.name === name,
|
|
614
619
|
);
|
|
@@ -686,7 +691,7 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
686
691
|
uri: string,
|
|
687
692
|
):
|
|
688
693
|
| {
|
|
689
|
-
resource:
|
|
694
|
+
resource: DiscoveredCapability<ResourceMetadata>;
|
|
690
695
|
params: Record<string, string>;
|
|
691
696
|
}
|
|
692
697
|
| undefined {
|
|
@@ -728,7 +733,7 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
728
733
|
uri: string,
|
|
729
734
|
):
|
|
730
735
|
| {
|
|
731
|
-
resourceTemplate:
|
|
736
|
+
resourceTemplate: DiscoveredCapability<ResourceTemplateMetadata>;
|
|
732
737
|
params: Record<string, string>;
|
|
733
738
|
}
|
|
734
739
|
| undefined {
|
|
@@ -785,23 +790,60 @@ export class McpRegistryService implements OnApplicationBootstrap {
|
|
|
785
790
|
|
|
786
791
|
/**
|
|
787
792
|
* Register a tool programmatically (for dynamic tools).
|
|
788
|
-
* Use
|
|
793
|
+
* Use McpRegistryService.registerTool() instead of calling this directly.
|
|
789
794
|
*
|
|
790
795
|
* @param mcpModuleId - The module ID to register the tool with
|
|
791
|
-
* @param
|
|
796
|
+
* @param capability - The discovered capability object to register
|
|
792
797
|
*/
|
|
793
|
-
|
|
798
|
+
registerDynamicCapability<T extends object>(
|
|
799
|
+
mcpModuleId: string,
|
|
800
|
+
capability: DiscoveredCapability<T>,
|
|
801
|
+
): void {
|
|
802
|
+
if (!this.discoveredCapabilitiesByMcpModuleId.has(mcpModuleId)) {
|
|
803
|
+
this.discoveredCapabilitiesByMcpModuleId.set(mcpModuleId, []);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const capabilities =
|
|
807
|
+
this.discoveredCapabilitiesByMcpModuleId.get(mcpModuleId)!;
|
|
808
|
+
|
|
809
|
+
const existingIndex = capabilities.findIndex(
|
|
810
|
+
(c) =>
|
|
811
|
+
c.type === capability.type && c.methodName === capability.methodName,
|
|
812
|
+
);
|
|
813
|
+
|
|
814
|
+
if (existingIndex !== -1) {
|
|
815
|
+
this.logger.warn(
|
|
816
|
+
`Dynamic ${capability.type} '${capability.methodName}' already registered in module: ${mcpModuleId}. Overwriting.`,
|
|
817
|
+
);
|
|
818
|
+
capabilities[existingIndex] = capability;
|
|
819
|
+
} else {
|
|
820
|
+
this.logger.debug(
|
|
821
|
+
`Dynamic ${capability.type} registered: ${capability.metadata['name']} in module: ${mcpModuleId}`,
|
|
822
|
+
);
|
|
823
|
+
capabilities.push(capability);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
removeDynamicCapability(
|
|
794
828
|
mcpModuleId: string,
|
|
795
|
-
|
|
829
|
+
type: DiscoveredCapability<object>['type'],
|
|
830
|
+
identifier: string,
|
|
796
831
|
): void {
|
|
797
|
-
|
|
798
|
-
this.
|
|
832
|
+
const capabilities =
|
|
833
|
+
this.discoveredCapabilitiesByMcpModuleId.get(mcpModuleId);
|
|
834
|
+
if (!capabilities) {
|
|
835
|
+
return;
|
|
799
836
|
}
|
|
800
837
|
|
|
801
838
|
this.logger.debug(
|
|
802
|
-
`Dynamic
|
|
839
|
+
`Dynamic ${type} removed: ${identifier} in module: ${mcpModuleId}`,
|
|
803
840
|
);
|
|
804
841
|
|
|
805
|
-
this.
|
|
842
|
+
this.discoveredCapabilitiesByMcpModuleId.set(
|
|
843
|
+
mcpModuleId,
|
|
844
|
+
capabilities.filter(
|
|
845
|
+
(c) => !(c.type === type && c.methodName === identifier),
|
|
846
|
+
),
|
|
847
|
+
);
|
|
806
848
|
}
|
|
807
849
|
}
|
|
@@ -4,7 +4,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
4
4
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
5
5
|
import type { McpOptions } from '../interfaces';
|
|
6
6
|
import { McpExecutorService } from './mcp-executor.service';
|
|
7
|
-
import {
|
|
7
|
+
import { McpRegistryDiscoveryService } from './mcp-registry-discovery.service';
|
|
8
8
|
import { SsePingService } from './sse-ping.service';
|
|
9
9
|
import { normalizeEndpoint } from '../utils/normalize-endpoint';
|
|
10
10
|
import { HttpAdapterFactory } from '../adapters';
|
|
@@ -27,7 +27,7 @@ export class McpSseService {
|
|
|
27
27
|
@Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
|
|
28
28
|
private readonly applicationConfig: ApplicationConfig,
|
|
29
29
|
private readonly moduleRef: ModuleRef,
|
|
30
|
-
private readonly toolRegistry:
|
|
30
|
+
private readonly toolRegistry: McpRegistryDiscoveryService,
|
|
31
31
|
@Inject(SsePingService) private readonly pingService: SsePingService,
|
|
32
32
|
) {
|
|
33
33
|
this.logger = createMcpLogger(McpSseService.name, this.options);
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
} from '../interfaces/http-adapter.interface';
|
|
11
11
|
import type { McpOptions } from '../interfaces';
|
|
12
12
|
import { McpExecutorService } from './mcp-executor.service';
|
|
13
|
-
import {
|
|
13
|
+
import { McpRegistryDiscoveryService } from './mcp-registry-discovery.service';
|
|
14
14
|
import { createMcpLogger } from '../utils/mcp-logger.factory';
|
|
15
15
|
import { createMcpServer } from '../utils/mcp-server.factory';
|
|
16
16
|
|
|
@@ -28,7 +28,7 @@ export class McpStreamableHttpService implements OnModuleDestroy {
|
|
|
28
28
|
@Inject('MCP_OPTIONS') private readonly options: McpOptions,
|
|
29
29
|
@Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,
|
|
30
30
|
private readonly moduleRef: ModuleRef,
|
|
31
|
-
private readonly toolRegistry:
|
|
31
|
+
private readonly toolRegistry: McpRegistryDiscoveryService,
|
|
32
32
|
) {
|
|
33
33
|
// Determine if we're in stateless mode
|
|
34
34
|
this.isStatelessMode = !!options.streamableHttp?.statelessMode;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Injectable } from '@nestjs/common';
|
|
2
2
|
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
-
import {
|
|
3
|
+
import { DiscoveredCapability } from './mcp-registry-discovery.service';
|
|
4
4
|
import { ToolMetadata, SecurityScheme } from '../decorators/tool.decorator';
|
|
5
5
|
import { JwtPayload } from '../../authz/services/jwt-token.service';
|
|
6
6
|
|
|
@@ -17,7 +17,7 @@ export class ToolAuthorizationService {
|
|
|
17
17
|
* @returns Array of security schemes for the tool
|
|
18
18
|
*/
|
|
19
19
|
generateSecuritySchemes(
|
|
20
|
-
tool:
|
|
20
|
+
tool: DiscoveredCapability<ToolMetadata>,
|
|
21
21
|
moduleHasGuards: boolean,
|
|
22
22
|
): SecurityScheme[] {
|
|
23
23
|
const metadata = tool.metadata;
|
|
@@ -56,7 +56,7 @@ export class ToolAuthorizationService {
|
|
|
56
56
|
*/
|
|
57
57
|
canAccessTool(
|
|
58
58
|
user: JwtPayload | undefined,
|
|
59
|
-
tool:
|
|
59
|
+
tool: DiscoveredCapability<ToolMetadata>,
|
|
60
60
|
moduleHasGuards: boolean,
|
|
61
61
|
allowUnauthenticatedAccess: boolean = false,
|
|
62
62
|
): boolean {
|
|
@@ -132,7 +132,7 @@ export class ToolAuthorizationService {
|
|
|
132
132
|
*/
|
|
133
133
|
validateToolAccess(
|
|
134
134
|
user: JwtPayload | undefined,
|
|
135
|
-
tool:
|
|
135
|
+
tool: DiscoveredCapability<ToolMetadata>,
|
|
136
136
|
moduleHasGuards: boolean,
|
|
137
137
|
allowUnauthenticatedAccess: boolean = false,
|
|
138
138
|
): void {
|