@nestjs-mcp/server 0.1.0-alpha.10

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 (100) hide show
  1. package/.copilotignore +38 -0
  2. package/.devcontainer/Dockerfile.dev +28 -0
  3. package/.devcontainer/devcontainer.json +56 -0
  4. package/.devcontainer/docker-compose.yml +15 -0
  5. package/.dockerignore +37 -0
  6. package/.prettierrc +4 -0
  7. package/LICENSE +21 -0
  8. package/README.md +540 -0
  9. package/dist/controllers/sse/index.d.ts +2 -0
  10. package/dist/controllers/sse/index.js +19 -0
  11. package/dist/controllers/sse/index.js.map +1 -0
  12. package/dist/controllers/sse/sse.controller.d.ts +10 -0
  13. package/dist/controllers/sse/sse.controller.js +57 -0
  14. package/dist/controllers/sse/sse.controller.js.map +1 -0
  15. package/dist/controllers/sse/sse.service.d.ts +16 -0
  16. package/dist/controllers/sse/sse.service.js +78 -0
  17. package/dist/controllers/sse/sse.service.js.map +1 -0
  18. package/dist/controllers/streamable/index.d.ts +2 -0
  19. package/dist/controllers/streamable/index.js +19 -0
  20. package/dist/controllers/streamable/index.js.map +1 -0
  21. package/dist/controllers/streamable/streamable.controller.d.ts +9 -0
  22. package/dist/controllers/streamable/streamable.controller.js +62 -0
  23. package/dist/controllers/streamable/streamable.controller.js.map +1 -0
  24. package/dist/controllers/streamable/streamable.service.d.ts +24 -0
  25. package/dist/controllers/streamable/streamable.service.js +118 -0
  26. package/dist/controllers/streamable/streamable.service.js.map +1 -0
  27. package/dist/decorators/capabilities.constants.d.ts +4 -0
  28. package/dist/decorators/capabilities.constants.js +8 -0
  29. package/dist/decorators/capabilities.constants.js.map +1 -0
  30. package/dist/decorators/capabilities.decorators.d.ts +8 -0
  31. package/dist/decorators/capabilities.decorators.js +49 -0
  32. package/dist/decorators/capabilities.decorators.js.map +1 -0
  33. package/dist/decorators/index.d.ts +2 -0
  34. package/dist/decorators/index.js +19 -0
  35. package/dist/decorators/index.js.map +1 -0
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.js +21 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/interceptors/message.interceptor.d.ts +10 -0
  40. package/dist/interceptors/message.interceptor.js +61 -0
  41. package/dist/interceptors/message.interceptor.js.map +1 -0
  42. package/dist/interfaces/capabilities.interface.d.ts +52 -0
  43. package/dist/interfaces/capabilities.interface.js +3 -0
  44. package/dist/interfaces/capabilities.interface.js.map +1 -0
  45. package/dist/interfaces/context.interface.d.ts +6 -0
  46. package/dist/interfaces/context.interface.js +3 -0
  47. package/dist/interfaces/context.interface.js.map +1 -0
  48. package/dist/interfaces/index.d.ts +2 -0
  49. package/dist/interfaces/index.js +19 -0
  50. package/dist/interfaces/index.js.map +1 -0
  51. package/dist/interfaces/mcp-server-options.interface.d.ts +42 -0
  52. package/dist/interfaces/mcp-server-options.interface.js +3 -0
  53. package/dist/interfaces/mcp-server-options.interface.js.map +1 -0
  54. package/dist/interfaces/message.types.d.ts +8 -0
  55. package/dist/interfaces/message.types.js +3 -0
  56. package/dist/interfaces/message.types.js.map +1 -0
  57. package/dist/mcp.module.d.ts +13 -0
  58. package/dist/mcp.module.js +193 -0
  59. package/dist/mcp.module.js.map +1 -0
  60. package/dist/registry/discovery.service.d.ts +16 -0
  61. package/dist/registry/discovery.service.js +85 -0
  62. package/dist/registry/discovery.service.js.map +1 -0
  63. package/dist/registry/index.d.ts +2 -0
  64. package/dist/registry/index.js +19 -0
  65. package/dist/registry/index.js.map +1 -0
  66. package/dist/registry/logger.service.d.ts +16 -0
  67. package/dist/registry/logger.service.js +97 -0
  68. package/dist/registry/logger.service.js.map +1 -0
  69. package/dist/registry/registry.service.d.ts +16 -0
  70. package/dist/registry/registry.service.js +170 -0
  71. package/dist/registry/registry.service.js.map +1 -0
  72. package/dist/services/message.service.d.ts +7 -0
  73. package/dist/services/message.service.js +25 -0
  74. package/dist/services/message.service.js.map +1 -0
  75. package/dist/tsconfig.build.tsbuildinfo +1 -0
  76. package/eslint.config.mjs +40 -0
  77. package/package.json +109 -0
  78. package/src/controllers/sse/index.ts +2 -0
  79. package/src/controllers/sse/sse.controller.ts +25 -0
  80. package/src/controllers/sse/sse.service.ts +90 -0
  81. package/src/controllers/streamable/index.ts +2 -0
  82. package/src/controllers/streamable/streamable.controller.ts +24 -0
  83. package/src/controllers/streamable/streamable.service.ts +169 -0
  84. package/src/decorators/capabilities.constants.ts +7 -0
  85. package/src/decorators/capabilities.decorators.ts +150 -0
  86. package/src/decorators/index.ts +2 -0
  87. package/src/index.ts +11 -0
  88. package/src/interceptors/message.interceptor.ts +70 -0
  89. package/src/interfaces/capabilities.interface.ts +95 -0
  90. package/src/interfaces/context.interface.ts +18 -0
  91. package/src/interfaces/index.ts +2 -0
  92. package/src/interfaces/mcp-server-options.interface.ts +105 -0
  93. package/src/interfaces/message.types.ts +13 -0
  94. package/src/mcp.module.ts +250 -0
  95. package/src/mcp.service.spec.ts +28 -0
  96. package/src/registry/discovery.service.ts +116 -0
  97. package/src/registry/index.ts +2 -0
  98. package/src/registry/logger.service.ts +143 -0
  99. package/src/registry/registry.service.ts +282 -0
  100. package/src/services/message.service.ts +18 -0
@@ -0,0 +1,143 @@
1
+ import {
2
+ Inject,
3
+ Injectable,
4
+ Logger,
5
+ LoggerService,
6
+ Optional,
7
+ } from '@nestjs/common';
8
+
9
+ import { McpLoggingOptions } from '../interfaces/mcp-server-options.interface';
10
+
11
+ /**
12
+ * Servicio especializado de logging para el servidor MCP
13
+ */
14
+ @Injectable()
15
+ export class McpLoggerService implements LoggerService {
16
+ private readonly logger: Logger;
17
+ private readonly options: Required<McpLoggingOptions>;
18
+
19
+ constructor(
20
+ @Optional() @Inject('MCP_LOGGING_OPTIONS') options?: McpLoggingOptions,
21
+ ) {
22
+ this.options = {
23
+ enabled: options?.enabled !== false, // Habilitado por defecto
24
+ level: options?.level || 'verbose',
25
+ };
26
+
27
+ this.logger = new Logger('MCP');
28
+ }
29
+
30
+ /**
31
+ * Registra un mensaje de nivel debug
32
+ */
33
+ debug(message: string, context?: string): void {
34
+ if (
35
+ !this.options.enabled ||
36
+ this.getLevelValue(this.options.level) > this.getLevelValue('debug')
37
+ ) {
38
+ return;
39
+ }
40
+
41
+ const formattedContext = this.formatContext(context);
42
+ this.logger.debug(message, formattedContext);
43
+ }
44
+
45
+ /**
46
+ * Registra un mensaje de nivel verbose (detallado)
47
+ */
48
+ verbose(message: string, context?: string): void {
49
+ if (
50
+ !this.options.enabled ||
51
+ this.getLevelValue(this.options.level) > this.getLevelValue('verbose')
52
+ ) {
53
+ return;
54
+ }
55
+
56
+ const formattedContext = this.formatContext(context);
57
+ this.logger.verbose(message, formattedContext);
58
+ }
59
+
60
+ /**
61
+ * Registra un mensaje de nivel log (información)
62
+ */
63
+ log(message: string, context?: string): void {
64
+ if (
65
+ !this.options.enabled ||
66
+ this.getLevelValue(this.options.level) > this.getLevelValue('log')
67
+ ) {
68
+ return;
69
+ }
70
+
71
+ const formattedContext = this.formatContext(context);
72
+ this.logger.log(message, formattedContext);
73
+ }
74
+
75
+ /**
76
+ * Registra un mensaje de nivel warn (advertencia)
77
+ */
78
+ warn(message: string, context?: string): void {
79
+ if (
80
+ !this.options.enabled ||
81
+ this.getLevelValue(this.options.level) > this.getLevelValue('warn')
82
+ ) {
83
+ return;
84
+ }
85
+
86
+ const formattedContext = this.formatContext(context);
87
+ this.logger.warn(message, formattedContext);
88
+ }
89
+
90
+ /**
91
+ * Registra un mensaje de nivel error
92
+ */
93
+ error(message: string, trace?: string, context?: string): void {
94
+ if (
95
+ !this.options.enabled ||
96
+ this.getLevelValue(this.options.level) > this.getLevelValue('error')
97
+ ) {
98
+ return;
99
+ }
100
+
101
+ const formattedContext = this.formatContext(context);
102
+ this.logger.error(message, trace, formattedContext);
103
+ }
104
+
105
+ /**
106
+ * Método para determinar si el logging está habilitado
107
+ */
108
+ isEnabled(): boolean {
109
+ return this.options.enabled;
110
+ }
111
+
112
+ /**
113
+ * Método para obtener el nivel actual de logging
114
+ */
115
+ getLevel(): string {
116
+ return this.options.level;
117
+ }
118
+
119
+ /**
120
+ * Formatea el contexto para añadirle el prefijo '@mcp'
121
+ */
122
+ private formatContext(context?: string): string {
123
+ if (!context) {
124
+ return '@mcp';
125
+ }
126
+ return `@mcp:${context}`;
127
+ }
128
+
129
+ /**
130
+ * Convierte un nivel de log a un valor numérico para comparaciones
131
+ */
132
+ private getLevelValue(level: string): number {
133
+ const levels: Record<string, number> = {
134
+ debug: 0,
135
+ verbose: 1,
136
+ log: 2,
137
+ warn: 3,
138
+ error: 4,
139
+ };
140
+
141
+ return levels[level] ?? 2; // Por defecto, nivel 'log'
142
+ }
143
+ }
@@ -0,0 +1,282 @@
1
+ import {
2
+ McpServer,
3
+ ResourceTemplate,
4
+ } from '@modelcontextprotocol/sdk/server/mcp.js';
5
+ import type { CanActivate, Type } from '@nestjs/common';
6
+ import { Injectable } from '@nestjs/common';
7
+
8
+ import { MCP_RESOLVER } from '../decorators';
9
+ import {
10
+ MCP_PROMPT,
11
+ MCP_RESOURCE,
12
+ MCP_TOOL,
13
+ } from '../decorators/capabilities.constants';
14
+ import { MCP_GUARDS } from '../decorators/capabilities.decorators';
15
+ import {
16
+ PromptOptions,
17
+ ResourceOptions,
18
+ ToolOptions,
19
+ } from '../interfaces/capabilities.interface';
20
+ import { McpExecutionContext } from '../interfaces/context.interface';
21
+ import { MessageService } from '../services/message.service';
22
+ import { DiscoveryService } from './discovery.service';
23
+ import { McpLoggerService } from './logger.service';
24
+ @Injectable()
25
+ export class RegistryService {
26
+ constructor(
27
+ private readonly discoveryService: DiscoveryService,
28
+ private readonly logger: McpLoggerService,
29
+ private readonly messageService: MessageService,
30
+ ) {}
31
+
32
+ async registerAll(server: McpServer): Promise<void> {
33
+ this.logger.log(
34
+ 'Starting registration of all MCP capabilities...',
35
+ 'registry',
36
+ );
37
+
38
+ await Promise.all([
39
+ this.registerResources(server),
40
+ this.registerPrompts(server),
41
+ this.registerTools(server),
42
+ ]);
43
+ }
44
+
45
+ /**
46
+ * Executes all guards attached to the resolver class and method.
47
+ * Throws an error if any guard denies access.
48
+ *
49
+ * @param instance The resolver instance
50
+ * @param methodName The method name being invoked
51
+ * @param args The arguments passed to the method
52
+ * @throws Error if any guard denies access
53
+ */
54
+ private runGuards(
55
+ instance: object,
56
+ methodName: string,
57
+ args: unknown[],
58
+ ): Promise<void> {
59
+ // Retrieve class-level guards
60
+ const classConstructor = instance.constructor;
61
+ const classGuards: (CanActivate | { new (): CanActivate })[] =
62
+ (Reflect.getMetadata(MCP_GUARDS, classConstructor) as (
63
+ | CanActivate
64
+ | { new (): CanActivate }
65
+ )[]) || [];
66
+
67
+ // Retrieve method-level guards
68
+ const prototype = Object.getPrototypeOf(instance) as Record<
69
+ string,
70
+ unknown
71
+ >;
72
+ const methodKey = prototype[methodName] as object | undefined;
73
+
74
+ const methodGuards: (CanActivate | { new (): CanActivate })[] =
75
+ (methodKey &&
76
+ (Reflect.getMetadata(MCP_GUARDS, methodKey) as (
77
+ | CanActivate
78
+ | { new (): CanActivate }
79
+ )[])) ||
80
+ [];
81
+
82
+ // Combine guards: class-level first, then method-level
83
+ const allGuards = [...classGuards, ...methodGuards];
84
+
85
+ if (!allGuards.length) return Promise.resolve();
86
+ // Build a minimal context (customize as needed)
87
+ const context: McpExecutionContext = {
88
+ args,
89
+ message: this.messageService.get(),
90
+
91
+ // @ts-expect-error: Default types are 'http' | 'ws' | 'rpc' but in our case
92
+ // we are using 'mcp'
93
+ getType: () => 'mcp',
94
+ getClass: () => instance.constructor as Type<any>,
95
+ getArgs: <T extends Array<any>>() => args as T,
96
+ };
97
+
98
+ return (async () => {
99
+ for (const Guard of allGuards) {
100
+ const guardInstance: CanActivate =
101
+ typeof Guard === 'function' ? new Guard() : Guard;
102
+ const allowed = await guardInstance.canActivate(context);
103
+
104
+ if (!allowed)
105
+ throw new Error(`Access denied by guard on ${methodName}`);
106
+ }
107
+ })();
108
+ }
109
+
110
+ private async wrappedHandler<TArgs extends unknown[], TResult>(
111
+ instance: object,
112
+ handler: (...args: TArgs) => TResult,
113
+ args: unknown[],
114
+ ) {
115
+ const isResolver = Reflect.hasMetadata(MCP_RESOLVER, instance.constructor);
116
+
117
+ if (!isResolver) {
118
+ throw new Error(
119
+ `Class "${instance.constructor.name}" must be decorated with @Resolver to use @Prompt, @Tool, or @Resource.`,
120
+ );
121
+ }
122
+
123
+ const methodName = handler.name;
124
+
125
+ await this.runGuards(instance, methodName, args);
126
+
127
+ return handler(...(args as TArgs));
128
+ }
129
+
130
+ private registerResources(server: McpServer) {
131
+ const resourceMethods =
132
+ this.discoveryService.getAllMethodsWithMetadata<ResourceOptions>(
133
+ MCP_RESOURCE,
134
+ );
135
+ for (const method of resourceMethods) {
136
+ const { metadata, handler, instance } = method;
137
+
138
+ this.logger.log(
139
+ `Resource "${metadata?.name || 'unnamed'}" found.`,
140
+ 'resources',
141
+ );
142
+
143
+ const wrappedHandler = (...args: unknown[]) =>
144
+ this.wrappedHandler(instance, handler, args);
145
+
146
+ try {
147
+ if ('template' in metadata) {
148
+ if ('metadata' in metadata) {
149
+ server.resource(
150
+ metadata.name,
151
+ new ResourceTemplate(metadata.template, { list: undefined }),
152
+ metadata.metadata,
153
+ wrappedHandler,
154
+ );
155
+ } else {
156
+ server.resource(
157
+ metadata.name,
158
+ new ResourceTemplate(metadata.template, { list: undefined }),
159
+ wrappedHandler,
160
+ );
161
+ }
162
+ } else if ('uri' in metadata) {
163
+ if ('metadata' in metadata) {
164
+ server.resource(
165
+ metadata.name,
166
+ metadata.uri,
167
+ metadata.metadata,
168
+ wrappedHandler,
169
+ );
170
+ } else {
171
+ server.resource(metadata.name, metadata.uri, wrappedHandler);
172
+ }
173
+ }
174
+ } catch (error) {
175
+ this.logger.error(
176
+ `Error registering resource ${metadata.name}: ${error}`,
177
+ undefined,
178
+ 'resources',
179
+ );
180
+ if (error && typeof error === 'object' && 'stack' in error) {
181
+ this.logger.error(
182
+ `Error stack: ${(error as Error).stack}`,
183
+ undefined,
184
+ 'resources',
185
+ );
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ private registerPrompts(server: McpServer) {
192
+ const promptMethods =
193
+ this.discoveryService.getAllMethodsWithMetadata<PromptOptions>(
194
+ MCP_PROMPT,
195
+ );
196
+ for (const method of promptMethods) {
197
+ const { metadata, handler, instance } = method;
198
+
199
+ this.logger.log(
200
+ `Prompt "${metadata?.name || 'unnamed'}" found.`,
201
+ 'prompts',
202
+ );
203
+
204
+ const wrappedHandler = (...args: unknown[]) =>
205
+ this.wrappedHandler(instance, handler, args);
206
+
207
+ try {
208
+ if ('description' in metadata && 'argsSchema' in metadata) {
209
+ server.prompt(
210
+ metadata.name,
211
+ metadata.description,
212
+ metadata.argsSchema,
213
+ wrappedHandler,
214
+ );
215
+ } else if ('argsSchema' in metadata) {
216
+ server.prompt(metadata.name, metadata.argsSchema, wrappedHandler);
217
+ } else if ('description' in metadata) {
218
+ server.prompt(metadata.name, metadata.description, wrappedHandler);
219
+ } else {
220
+ server.prompt(metadata.name, wrappedHandler);
221
+ }
222
+ } catch (error) {
223
+ this.logger.error(
224
+ `Error registering prompt ${metadata.name}: ${error}`,
225
+ undefined,
226
+ 'prompts',
227
+ );
228
+ if (error && typeof error === 'object' && 'stack' in error) {
229
+ this.logger.error(
230
+ `Error stack: ${(error as Error).stack}`,
231
+ undefined,
232
+ 'prompts',
233
+ );
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ private registerTools(server: McpServer) {
240
+ const toolMethods =
241
+ this.discoveryService.getAllMethodsWithMetadata<ToolOptions>(MCP_TOOL);
242
+
243
+ for (const method of toolMethods) {
244
+ const { metadata, handler, instance } = method;
245
+
246
+ this.logger.log(`Tool "${metadata?.name || 'unnamed'}" found.`, 'tools');
247
+
248
+ const wrappedHandler = (...args: unknown[]) =>
249
+ this.wrappedHandler(instance, handler, args);
250
+
251
+ try {
252
+ if ('description' in metadata && 'paramSchema' in metadata) {
253
+ server.tool(
254
+ metadata.name,
255
+ metadata.description,
256
+ metadata.paramSchema,
257
+ wrappedHandler,
258
+ );
259
+ } else if ('paramSchema' in metadata) {
260
+ server.tool(metadata.name, metadata.paramSchema, wrappedHandler);
261
+ } else if ('description' in metadata) {
262
+ server.tool(metadata.name, metadata.description, wrappedHandler);
263
+ } else {
264
+ server.tool(metadata.name, wrappedHandler);
265
+ }
266
+ } catch (error) {
267
+ this.logger.error(
268
+ `Error registering tool ${metadata.name}: ${error}`,
269
+ undefined,
270
+ 'tools',
271
+ );
272
+ if (error && typeof error === 'object' && 'stack' in error) {
273
+ this.logger.error(
274
+ `Stack trace: ${(error as Error).stack}`,
275
+ undefined,
276
+ 'tools',
277
+ );
278
+ }
279
+ }
280
+ }
281
+ }
282
+ }
@@ -0,0 +1,18 @@
1
+ import { Injectable } from '@nestjs/common';
2
+
3
+ import { AsyncLocalStorage } from 'async_hooks';
4
+
5
+ import { McpMessage } from '../interfaces/message.types';
6
+
7
+ export const requestContext = new AsyncLocalStorage<McpMessage>();
8
+
9
+ @Injectable()
10
+ export class MessageService {
11
+ public set(context: McpMessage): void {
12
+ requestContext.enterWith(context);
13
+ }
14
+
15
+ public get(): McpMessage | undefined {
16
+ return requestContext.getStore();
17
+ }
18
+ }