@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.
Files changed (128) hide show
  1. package/README.md +5 -1
  2. package/dist/mcp/decorators/prompt.decorator.d.ts +1 -2
  3. package/dist/mcp/decorators/prompt.decorator.d.ts.map +1 -1
  4. package/dist/mcp/decorators/prompt.decorator.js.map +1 -1
  5. package/dist/mcp/index.d.ts +2 -2
  6. package/dist/mcp/index.d.ts.map +1 -1
  7. package/dist/mcp/index.js +2 -2
  8. package/dist/mcp/index.js.map +1 -1
  9. package/dist/mcp/interfaces/dynamic-prompt.interface.d.ts +11 -0
  10. package/dist/mcp/interfaces/dynamic-prompt.interface.d.ts.map +1 -0
  11. package/dist/mcp/interfaces/dynamic-prompt.interface.js +3 -0
  12. package/dist/mcp/interfaces/dynamic-prompt.interface.js.map +1 -0
  13. package/dist/mcp/interfaces/dynamic-resource.interface.d.ts +11 -0
  14. package/dist/mcp/interfaces/dynamic-resource.interface.d.ts.map +1 -0
  15. package/dist/mcp/interfaces/dynamic-resource.interface.js +3 -0
  16. package/dist/mcp/interfaces/dynamic-resource.interface.js.map +1 -0
  17. package/dist/mcp/interfaces/dynamic-tool.interface.js.map +1 -1
  18. package/dist/mcp/interfaces/index.d.ts +2 -0
  19. package/dist/mcp/interfaces/index.d.ts.map +1 -1
  20. package/dist/mcp/interfaces/index.js.map +1 -1
  21. package/dist/mcp/mcp.module.d.ts.map +1 -1
  22. package/dist/mcp/mcp.module.js +15 -11
  23. package/dist/mcp/mcp.module.js.map +1 -1
  24. package/dist/mcp/services/handlers/mcp-handler.base.d.ts +4 -4
  25. package/dist/mcp/services/handlers/mcp-handler.base.d.ts.map +1 -1
  26. package/dist/mcp/services/handlers/mcp-handler.base.js.map +1 -1
  27. package/dist/mcp/services/handlers/mcp-prompts.handler.d.ts +2 -2
  28. package/dist/mcp/services/handlers/mcp-prompts.handler.d.ts.map +1 -1
  29. package/dist/mcp/services/handlers/mcp-prompts.handler.js +13 -3
  30. package/dist/mcp/services/handlers/mcp-prompts.handler.js.map +1 -1
  31. package/dist/mcp/services/handlers/mcp-resources.handler.d.ts +2 -2
  32. package/dist/mcp/services/handlers/mcp-resources.handler.d.ts.map +1 -1
  33. package/dist/mcp/services/handlers/mcp-resources.handler.js +12 -2
  34. package/dist/mcp/services/handlers/mcp-resources.handler.js.map +1 -1
  35. package/dist/mcp/services/handlers/mcp-tools.handler.d.ts +2 -2
  36. package/dist/mcp/services/handlers/mcp-tools.handler.d.ts.map +1 -1
  37. package/dist/mcp/services/handlers/mcp-tools.handler.js +5 -5
  38. package/dist/mcp/services/handlers/mcp-tools.handler.js.map +1 -1
  39. package/dist/mcp/services/mcp-dynamic-registry.service.d.ts +26 -0
  40. package/dist/mcp/services/mcp-dynamic-registry.service.d.ts.map +1 -0
  41. package/dist/mcp/services/mcp-dynamic-registry.service.js +133 -0
  42. package/dist/mcp/services/mcp-dynamic-registry.service.js.map +1 -0
  43. package/dist/mcp/services/mcp-executor.service.d.ts +2 -2
  44. package/dist/mcp/services/mcp-executor.service.d.ts.map +1 -1
  45. package/dist/mcp/services/mcp-executor.service.js +2 -2
  46. package/dist/mcp/services/mcp-executor.service.js.map +1 -1
  47. package/dist/mcp/services/{mcp-registry.service.d.ts → mcp-registry-discovery.service.d.ts} +16 -15
  48. package/dist/mcp/services/mcp-registry-discovery.service.d.ts.map +1 -0
  49. package/dist/mcp/services/{mcp-registry.service.js → mcp-registry-discovery.service.js} +41 -25
  50. package/dist/mcp/services/mcp-registry-discovery.service.js.map +1 -0
  51. package/dist/mcp/services/mcp-sse.service.d.ts +2 -2
  52. package/dist/mcp/services/mcp-sse.service.d.ts.map +1 -1
  53. package/dist/mcp/services/mcp-sse.service.js +2 -2
  54. package/dist/mcp/services/mcp-sse.service.js.map +1 -1
  55. package/dist/mcp/services/mcp-streamable-http.service.d.ts +2 -2
  56. package/dist/mcp/services/mcp-streamable-http.service.d.ts.map +1 -1
  57. package/dist/mcp/services/mcp-streamable-http.service.js +2 -2
  58. package/dist/mcp/services/mcp-streamable-http.service.js.map +1 -1
  59. package/dist/mcp/services/tool-authorization.service.d.ts +4 -4
  60. package/dist/mcp/services/tool-authorization.service.d.ts.map +1 -1
  61. package/dist/mcp/services/tool-authorization.service.js.map +1 -1
  62. package/dist/mcp/transport/stdio.service.d.ts +2 -2
  63. package/dist/mcp/transport/stdio.service.d.ts.map +1 -1
  64. package/dist/mcp/transport/stdio.service.js +7 -7
  65. package/dist/mcp/transport/stdio.service.js.map +1 -1
  66. package/dist/mcp/utils/capabilities-builder.d.ts +2 -2
  67. package/dist/mcp/utils/capabilities-builder.d.ts.map +1 -1
  68. package/dist/mcp/utils/capabilities-builder.js.map +1 -1
  69. package/dist/mcp/utils/mcp-server.factory.d.ts +2 -2
  70. package/dist/mcp/utils/mcp-server.factory.d.ts.map +1 -1
  71. package/dist/mcp/utils/mcp-server.factory.js.map +1 -1
  72. package/package.json +9 -9
  73. package/src/mcp/decorators/prompt.decorator.ts +1 -1
  74. package/src/mcp/index.ts +2 -2
  75. package/src/mcp/interfaces/dynamic-prompt.interface.ts +20 -0
  76. package/src/mcp/interfaces/dynamic-resource.interface.ts +22 -0
  77. package/src/mcp/interfaces/dynamic-tool.interface.ts +2 -2
  78. package/src/mcp/interfaces/index.ts +10 -0
  79. package/src/mcp/mcp.module.ts +15 -11
  80. package/src/mcp/services/handlers/mcp-handler.base.ts +6 -3
  81. package/src/mcp/services/handlers/mcp-prompts.handler.ts +36 -4
  82. package/src/mcp/services/handlers/mcp-resources.handler.ts +24 -2
  83. package/src/mcp/services/handlers/mcp-tools.handler.ts +11 -8
  84. package/src/mcp/services/mcp-dynamic-registry.service.ts +236 -0
  85. package/src/mcp/services/mcp-executor.service.ts +2 -2
  86. package/src/mcp/services/{mcp-registry.service.spec.ts → mcp-registry-discovery.service.spec.ts} +18 -12
  87. package/src/mcp/services/{mcp-registry.service.ts → mcp-registry-discovery.service.ts} +77 -35
  88. package/src/mcp/services/mcp-sse.service.ts +2 -2
  89. package/src/mcp/services/mcp-streamable-http.service.ts +2 -2
  90. package/src/mcp/services/tool-authorization.service.ts +4 -4
  91. package/src/mcp/transport/stdio.service.ts +8 -8
  92. package/src/mcp/utils/capabilities-builder.ts +2 -2
  93. package/src/mcp/utils/mcp-server.factory.ts +2 -2
  94. package/dist/authz/services/jwt-token.service.spec.d.ts +0 -2
  95. package/dist/authz/services/jwt-token.service.spec.d.ts.map +0 -1
  96. package/dist/authz/services/jwt-token.service.spec.js +0 -86
  97. package/dist/authz/services/jwt-token.service.spec.js.map +0 -1
  98. package/dist/authz/stores/memory-store.service.spec.d.ts +0 -2
  99. package/dist/authz/stores/memory-store.service.spec.d.ts.map +0 -1
  100. package/dist/authz/stores/memory-store.service.spec.js +0 -382
  101. package/dist/authz/stores/memory-store.service.spec.js.map +0 -1
  102. package/dist/authz/stores/typeorm/typeorm-store.service.spec.d.ts +0 -2
  103. package/dist/authz/stores/typeorm/typeorm-store.service.spec.d.ts.map +0 -1
  104. package/dist/authz/stores/typeorm/typeorm-store.service.spec.js +0 -340
  105. package/dist/authz/stores/typeorm/typeorm-store.service.spec.js.map +0 -1
  106. package/dist/mcp/services/mcp-registry.service.d.ts.map +0 -1
  107. package/dist/mcp/services/mcp-registry.service.js.map +0 -1
  108. package/dist/mcp/services/mcp-registry.service.spec.d.ts +0 -2
  109. package/dist/mcp/services/mcp-registry.service.spec.d.ts.map +0 -1
  110. package/dist/mcp/services/mcp-registry.service.spec.js +0 -245
  111. package/dist/mcp/services/mcp-registry.service.spec.js.map +0 -1
  112. package/dist/mcp/services/mcp-tool-builder.service.d.ts +0 -15
  113. package/dist/mcp/services/mcp-tool-builder.service.d.ts.map +0 -1
  114. package/dist/mcp/services/mcp-tool-builder.service.js +0 -70
  115. package/dist/mcp/services/mcp-tool-builder.service.js.map +0 -1
  116. package/dist/mcp/transport/custom-decorator.spec.d.ts +0 -2
  117. package/dist/mcp/transport/custom-decorator.spec.d.ts.map +0 -1
  118. package/dist/mcp/transport/custom-decorator.spec.js +0 -34
  119. package/dist/mcp/transport/custom-decorator.spec.js.map +0 -1
  120. package/dist/mcp/utils/mcp-logger.factory.spec.d.ts +0 -2
  121. package/dist/mcp/utils/mcp-logger.factory.spec.d.ts.map +0 -1
  122. package/dist/mcp/utils/mcp-logger.factory.spec.js +0 -150
  123. package/dist/mcp/utils/mcp-logger.factory.spec.js.map +0 -1
  124. package/dist/mcp/utils/normalize-endpoint.spec.d.ts +0 -2
  125. package/dist/mcp/utils/normalize-endpoint.spec.d.ts.map +0 -1
  126. package/dist/mcp/utils/normalize-endpoint.spec.js +0 -40
  127. package/dist/mcp/utils/normalize-endpoint.spec.js.map +0 -1
  128. 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 { McpRegistryService } from './mcp-registry.service';
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: McpRegistryService,
25
+ registry: McpRegistryDiscoveryService,
26
26
  @Inject('MCP_MODULE_ID') mcpModuleId: string,
27
27
  @Inject('MCP_OPTIONS') options: McpOptions,
28
28
  authService: ToolAuthorizationService,
@@ -5,13 +5,13 @@ import {
5
5
  ValueProvider,
6
6
  } from '@nestjs/common';
7
7
  import { Test, TestingModule } from '@nestjs/testing';
8
- import { McpRegistryService } from './mcp-registry.service';
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('McpRegistryService', () => {
14
- let service: McpRegistryService;
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
- McpRegistryService,
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<McpRegistryService>(McpRegistryService);
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).discoveredToolsByMcpModuleId = new Map([
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('McpRegistryService - Multiple discovery roots', () => {
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: McpRegistryService;
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<McpRegistryService>(McpRegistryService);
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('McpRegistryService - Single discovery root with multiple MCP servers', () => {
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: McpRegistryService;
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<McpRegistryService>(McpRegistryService);
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 tool
38
+ * Interface representing a discovered capability
39
39
  */
40
- export type DiscoveredTool<T extends object> = {
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 McpRegistryService implements OnApplicationBootstrap {
53
+ export class McpRegistryDiscoveryService implements OnApplicationBootstrap {
54
54
  private readonly logger: Logger;
55
- private discoveredToolsByMcpModuleId: Map<string, DiscoveredTool<any>[]> =
56
- new Map();
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(McpRegistryService.name, this.options);
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.discoveredToolsByMcpModuleId.has(mcpModuleId)) {
390
- this.discoveredToolsByMcpModuleId.set(mcpModuleId, []);
394
+ if (!this.discoveredCapabilitiesByMcpModuleId.has(mcpModuleId)) {
395
+ this.discoveredCapabilitiesByMcpModuleId.set(mcpModuleId, []);
391
396
  }
392
397
 
393
- this.discoveredToolsByMcpModuleId.get(mcpModuleId)?.push({
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.discoveredToolsByMcpModuleId.has(mcpModuleId)) {
467
- this.discoveredToolsByMcpModuleId.set(mcpModuleId, []);
471
+ if (!this.discoveredCapabilitiesByMcpModuleId.has(mcpModuleId)) {
472
+ this.discoveredCapabilitiesByMcpModuleId.set(mcpModuleId, []);
468
473
  }
469
474
 
470
- this.discoveredToolsByMcpModuleId.get(mcpModuleId)?.push({
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.discoveredToolsByMcpModuleId.keys());
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): DiscoveredTool<ToolMetadata>[] {
531
+ getTools(mcpModuleId: string): DiscoveredCapability<ToolMetadata>[] {
527
532
  return (
528
- this.discoveredToolsByMcpModuleId
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
- ): DiscoveredTool<ToolMetadata> | undefined {
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): DiscoveredTool<ResourceMetadata>[] {
554
+ getResources(mcpModuleId: string): DiscoveredCapability<ResourceMetadata>[] {
550
555
  return (
551
- this.discoveredToolsByMcpModuleId
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
- ): DiscoveredTool<ResourceMetadata> | undefined {
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
- ): DiscoveredTool<ResourceTemplateMetadata>[] {
579
+ ): DiscoveredCapability<ResourceTemplateMetadata>[] {
575
580
  return (
576
- this.discoveredToolsByMcpModuleId
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
- ): DiscoveredTool<ResourceTemplateMetadata> | undefined {
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): DiscoveredTool<PromptMetadata>[] {
602
+ getPrompts(mcpModuleId: string): DiscoveredCapability<PromptMetadata>[] {
598
603
  return (
599
- this.discoveredToolsByMcpModuleId
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
- ): DiscoveredTool<PromptMetadata> | undefined {
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: DiscoveredTool<ResourceMetadata>;
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: DiscoveredTool<ResourceTemplateMetadata>;
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 McpToolBuilder.registerTool() instead of calling this directly.
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 tool - The discovered tool object to register
796
+ * @param capability - The discovered capability object to register
792
797
  */
793
- registerDynamicTool(
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
- tool: DiscoveredTool<ToolMetadata>,
829
+ type: DiscoveredCapability<object>['type'],
830
+ identifier: string,
796
831
  ): void {
797
- if (!this.discoveredToolsByMcpModuleId.has(mcpModuleId)) {
798
- this.discoveredToolsByMcpModuleId.set(mcpModuleId, []);
832
+ const capabilities =
833
+ this.discoveredCapabilitiesByMcpModuleId.get(mcpModuleId);
834
+ if (!capabilities) {
835
+ return;
799
836
  }
800
837
 
801
838
  this.logger.debug(
802
- `Dynamic tool registered: ${tool.metadata.name} in module: ${mcpModuleId}`,
839
+ `Dynamic ${type} removed: ${identifier} in module: ${mcpModuleId}`,
803
840
  );
804
841
 
805
- this.discoveredToolsByMcpModuleId.get(mcpModuleId)?.push(tool);
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 { McpRegistryService } from './mcp-registry.service';
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: McpRegistryService,
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 { McpRegistryService } from './mcp-registry.service';
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: McpRegistryService,
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 { DiscoveredTool } from './mcp-registry.service';
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: DiscoveredTool<ToolMetadata>,
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: DiscoveredTool<ToolMetadata>,
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: DiscoveredTool<ToolMetadata>,
135
+ tool: DiscoveredCapability<ToolMetadata>,
136
136
  moduleHasGuards: boolean,
137
137
  allowUnauthenticatedAccess: boolean = false,
138
138
  ): void {