@nestjs-mcp/server 0.1.0-alpha.9 → 0.1.1

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 (74) hide show
  1. package/README.md +454 -136
  2. package/coverage/clover.xml +507 -0
  3. package/coverage/coverage-final.json +19 -0
  4. package/coverage/lcov-report/base.css +224 -0
  5. package/coverage/lcov-report/block-navigation.js +87 -0
  6. package/coverage/lcov-report/favicon.png +0 -0
  7. package/coverage/lcov-report/index.html +206 -0
  8. package/coverage/lcov-report/prettify.css +1 -0
  9. package/coverage/lcov-report/prettify.js +2 -0
  10. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  11. package/coverage/lcov-report/sorter.js +196 -0
  12. package/coverage/lcov-report/src/controllers/sse/index.html +146 -0
  13. package/coverage/lcov-report/src/controllers/sse/index.ts.html +91 -0
  14. package/coverage/lcov-report/src/controllers/sse/sse.controller.ts.html +160 -0
  15. package/coverage/lcov-report/src/controllers/sse/sse.service.ts.html +403 -0
  16. package/coverage/lcov-report/src/controllers/streamable/index.html +146 -0
  17. package/coverage/lcov-report/src/controllers/streamable/index.ts.html +91 -0
  18. package/coverage/lcov-report/src/controllers/streamable/streamable.controller.ts.html +157 -0
  19. package/coverage/lcov-report/src/controllers/streamable/streamable.service.ts.html +655 -0
  20. package/coverage/lcov-report/src/decorators/capabilities.constants.ts.html +106 -0
  21. package/coverage/lcov-report/src/decorators/capabilities.decorators.ts.html +535 -0
  22. package/coverage/lcov-report/src/decorators/index.html +146 -0
  23. package/coverage/lcov-report/src/decorators/index.ts.html +91 -0
  24. package/coverage/lcov-report/src/index.html +131 -0
  25. package/coverage/lcov-report/src/index.ts.html +118 -0
  26. package/coverage/lcov-report/src/interfaces/capabilities.interface.ts.html +703 -0
  27. package/coverage/lcov-report/src/interfaces/index.html +131 -0
  28. package/coverage/lcov-report/src/interfaces/index.ts.html +91 -0
  29. package/coverage/lcov-report/src/mcp.module.ts.html +817 -0
  30. package/coverage/lcov-report/src/registry/discovery.service.ts.html +433 -0
  31. package/coverage/lcov-report/src/registry/index.html +161 -0
  32. package/coverage/lcov-report/src/registry/index.ts.html +91 -0
  33. package/coverage/lcov-report/src/registry/logger.service.ts.html +514 -0
  34. package/coverage/lcov-report/src/registry/registry.service.ts.html +1183 -0
  35. package/coverage/lcov-report/src/services/index.html +116 -0
  36. package/coverage/lcov-report/src/services/session.manager.ts.html +163 -0
  37. package/coverage/lcov.info +912 -0
  38. package/dist/controllers/sse/sse.controller.d.ts +1 -3
  39. package/dist/controllers/sse/sse.controller.js +2 -8
  40. package/dist/controllers/sse/sse.controller.js.map +1 -1
  41. package/dist/interfaces/capabilities.interface.d.ts +1 -1
  42. package/dist/interfaces/{context.interface.d.ts → guards.interface.d.ts} +0 -2
  43. package/dist/interfaces/{message.types.js → guards.interface.js} +1 -1
  44. package/dist/interfaces/guards.interface.js.map +1 -0
  45. package/dist/interfaces/index.d.ts +1 -1
  46. package/dist/interfaces/index.js +1 -1
  47. package/dist/interfaces/index.js.map +1 -1
  48. package/dist/interfaces/mcp-server-options.interface.d.ts +2 -2
  49. package/dist/mcp.module.js +1 -18
  50. package/dist/mcp.module.js.map +1 -1
  51. package/dist/registry/registry.service.d.ts +1 -3
  52. package/dist/registry/registry.service.js +3 -8
  53. package/dist/registry/registry.service.js.map +1 -1
  54. package/dist/tsconfig.build.tsbuildinfo +1 -1
  55. package/package.json +20 -18
  56. package/src/controllers/sse/sse.service.ts +21 -5
  57. package/src/controllers/streamable/streamable.service.ts +45 -23
  58. package/src/interfaces/capabilities.interface.ts +112 -1
  59. package/src/interfaces/context.interface.ts +21 -6
  60. package/src/mcp.module.ts +4 -10
  61. package/src/registry/registry.service.ts +94 -10
  62. package/src/services/session.manager.ts +26 -0
  63. package/dist/interceptors/message.interceptor.d.ts +0 -10
  64. package/dist/interceptors/message.interceptor.js +0 -61
  65. package/dist/interceptors/message.interceptor.js.map +0 -1
  66. package/dist/interfaces/context.interface.js +0 -3
  67. package/dist/interfaces/context.interface.js.map +0 -1
  68. package/dist/interfaces/message.types.d.ts +0 -8
  69. package/dist/interfaces/message.types.js.map +0 -1
  70. package/dist/services/message.service.d.ts +0 -7
  71. package/dist/services/message.service.js +0 -25
  72. package/dist/services/message.service.js.map +0 -1
  73. package/src/interceptors/message.interceptor.ts +0 -70
  74. package/src/services/message.service.ts +0 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nestjs-mcp/server",
3
- "version": "0.1.0-alpha.9",
3
+ "version": "0.1.1",
4
4
  "description": "Modular library for building scalable MCP servers with NestJS, providing decorators and integration patterns as a wrapper for the official MCP TypeScript SDK.",
5
5
  "author": "Adrián Darío Hidalgo Flores",
6
6
  "license": "MIT",
@@ -30,25 +30,13 @@
30
30
  "model-context-protocol",
31
31
  "module",
32
32
  "nestjs",
33
+ "npm",
34
+ "pnpm",
33
35
  "sdk",
34
36
  "server",
35
- "typescript"
37
+ "typescript",
38
+ "yarn"
36
39
  ],
37
- "scripts": {
38
- "build": "nest build",
39
- "prepare": "husky install",
40
- "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
41
- "start:example": "npx -y ts-node-dev --respawn examples/$EXAMPLE/main.ts",
42
- "start:inspector": "npx -y @modelcontextprotocol/inspector",
43
- "lint": "eslint \"{src,test,examples}/**/*.ts\" --fix",
44
- "typecheck": "tsc --noEmit",
45
- "test": "jest",
46
- "test:watch": "jest --watch",
47
- "test:cov": "jest --coverage",
48
- "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
49
- "test:e2e": "jest --config ./test/jest-e2e.json",
50
- "npm:publish": "node scripts/npm-publish.js"
51
- },
52
40
  "peerDependencies": {
53
41
  "@nestjs/common": "^11.0.1",
54
42
  "@nestjs/core": "^11.0.1",
@@ -105,5 +93,19 @@
105
93
  ],
106
94
  "coverageDirectory": "../coverage",
107
95
  "testEnvironment": "node"
96
+ },
97
+ "scripts": {
98
+ "build": "nest build",
99
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
100
+ "start:example": "npx -y ts-node-dev --respawn examples/$EXAMPLE/main.ts",
101
+ "start:inspector": "npx -y @modelcontextprotocol/inspector",
102
+ "lint": "eslint \"{src,test,examples}/**/*.ts\" --fix",
103
+ "typecheck": "tsc --noEmit",
104
+ "test": "jest",
105
+ "test:watch": "jest --watch",
106
+ "test:cov": "jest --coverage",
107
+ "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
108
+ "test:e2e": "jest --config ./test/jest-e2e.json",
109
+ "npm:publish": "node scripts/npm-publish.js"
108
110
  }
109
- }
111
+ }
@@ -6,18 +6,18 @@ import { Request, Response } from 'express';
6
6
  import { McpServerOptions } from '../../interfaces/mcp-server-options.interface';
7
7
  import { McpLoggerService } from '../../registry/logger.service';
8
8
  import { RegistryService } from '../../registry/registry.service';
9
+ import { SessionManager } from '../../services/session.manager';
9
10
 
10
11
  @Injectable()
11
12
  export class SseService implements OnModuleInit {
12
13
  private server: McpServer;
13
14
 
14
- private transports = {} as Record<string, SSEServerTransport>;
15
-
16
15
  constructor(
17
16
  @Inject('MCP_SERVER_OPTIONS')
18
17
  private readonly options: McpServerOptions,
19
18
  private readonly registry: RegistryService,
20
19
  private readonly logger: McpLoggerService,
20
+ private readonly sessionManager: SessionManager,
21
21
  ) {
22
22
  this.server = new McpServer(this.options.serverInfo, this.options.options);
23
23
  }
@@ -36,7 +36,11 @@ export class SseService implements OnModuleInit {
36
36
  async handleSse(req: Request, res: Response) {
37
37
  // Create SSE transport for legacy clients
38
38
  const transport = new SSEServerTransport('/messages', res);
39
- this.transports[transport.sessionId] = transport;
39
+
40
+ this.sessionManager.setSession(transport.sessionId, {
41
+ transport,
42
+ request: req,
43
+ });
40
44
 
41
45
  this.logger.debug(
42
46
  `Starting SSE for sessionId: ${transport.sessionId}`,
@@ -44,7 +48,7 @@ export class SseService implements OnModuleInit {
44
48
  );
45
49
 
46
50
  res.on('close', () => {
47
- delete this.transports[transport.sessionId];
51
+ this.sessionManager.deleteSession(transport.sessionId);
48
52
  });
49
53
 
50
54
  await this.server.connect(transport);
@@ -55,7 +59,12 @@ export class SseService implements OnModuleInit {
55
59
  */
56
60
  async handleMessage(req: Request, res: Response) {
57
61
  const sessionId = req.query.sessionId as string;
58
- const transport = this.transports[sessionId];
62
+ const session = this.sessionManager.getSession(sessionId);
63
+
64
+ if (!session) {
65
+ res.status(400).send('Invalid or missing sessionId');
66
+ return;
67
+ }
59
68
 
60
69
  this.logger.debug(
61
70
  `Receiving SSE message for sessionId: ${sessionId}`,
@@ -64,6 +73,13 @@ export class SseService implements OnModuleInit {
64
73
 
65
74
  this.logger.debug(`SSE message: ${JSON.stringify(req.body)}`, 'MCP_SERVER');
66
75
 
76
+ const transport = session.transport;
77
+
78
+ if (!(transport instanceof SSEServerTransport)) {
79
+ res.status(400).send('Invalid transport');
80
+ return;
81
+ }
82
+
67
83
  try {
68
84
  if (transport) {
69
85
  await transport.handlePostMessage(req, res, req.body);
@@ -11,15 +11,13 @@ import {
11
11
  } from '../../interfaces/mcp-server-options.interface';
12
12
  import { McpLoggerService } from '../../registry/logger.service';
13
13
  import { RegistryService } from '../../registry/registry.service';
14
-
14
+ import { SessionManager } from '../../services/session.manager';
15
15
  // TODO: Stateless mode should be handled here or in another service
16
16
 
17
17
  @Injectable()
18
18
  export class StreamableService implements OnModuleInit {
19
19
  private server: McpServer;
20
20
 
21
- private transports = {} as Record<string, StreamableHTTPServerTransport>;
22
-
23
21
  constructor(
24
22
  @Inject('MCP_SERVER_OPTIONS')
25
23
  private readonly options: McpServerOptions,
@@ -27,6 +25,7 @@ export class StreamableService implements OnModuleInit {
27
25
  private readonly transportOptions: McpModuleTransportOptions,
28
26
  private readonly registry: RegistryService,
29
27
  private readonly logger: McpLoggerService,
28
+ private readonly sessionManager: SessionManager,
30
29
  ) {
31
30
  this.server = new McpServer(this.options.serverInfo, this.options.options);
32
31
  }
@@ -47,23 +46,34 @@ export class StreamableService implements OnModuleInit {
47
46
  * @param req Express Request object (expects sessionId in query)
48
47
  * @param res Express Response object
49
48
  */
50
- async handlePostRequest(
51
- req: Request<any, any, any, { sessionId?: string }>,
52
- res: Response,
53
- ) {
49
+ async handlePostRequest(req: Request, res: Response) {
54
50
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
55
51
  let transport: StreamableHTTPServerTransport;
56
52
 
57
53
  const { options } = this.transportOptions?.streamable || {};
58
54
 
59
- if (sessionId && this.transports[sessionId]) {
60
- transport = this.transports[sessionId];
55
+ if (sessionId && this.sessionManager.getSession(sessionId)) {
56
+ const session = this.sessionManager.getSession(sessionId);
57
+
58
+ if (!session) {
59
+ throw new Error('Session not found');
60
+ }
61
+
62
+ if (!(session.transport instanceof StreamableHTTPServerTransport)) {
63
+ throw new Error('Invalid transport');
64
+ }
65
+
66
+ transport = session.transport;
61
67
  } else if (!sessionId && isInitializeRequest(req.body)) {
68
+ // This is called only when method is initialize
62
69
  transport = new StreamableHTTPServerTransport({
63
70
  sessionIdGenerator: () =>
64
71
  options?.sessionIdGenerator?.() || randomUUID(),
65
72
  onsessioninitialized: (sessionId) => {
66
- this.transports[sessionId] = transport;
73
+ this.sessionManager.setSession(sessionId, {
74
+ transport,
75
+ request: req,
76
+ });
67
77
  },
68
78
  enableJsonResponse: options?.enableJsonResponse,
69
79
  eventStore: options?.eventStore,
@@ -71,9 +81,11 @@ export class StreamableService implements OnModuleInit {
71
81
 
72
82
  transport.onclose = () => {
73
83
  if (transport.sessionId) {
74
- delete this.transports[transport.sessionId];
84
+ this.sessionManager.deleteSession(transport.sessionId);
75
85
  }
76
86
  };
87
+
88
+ await this.server.connect(transport);
77
89
  } else {
78
90
  // Invalid request
79
91
  res.status(400).json({
@@ -99,18 +111,25 @@ export class StreamableService implements OnModuleInit {
99
111
  * @param req Express Request object (expects sessionId in query)
100
112
  * @param res Express Response object
101
113
  */
102
- async handleGetRequest(
103
- req: Request<any, any, any, { sessionId?: string }>,
104
- res: Response,
105
- ) {
114
+ async handleGetRequest(req: Request, res: Response) {
106
115
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
107
116
 
108
- if (!sessionId || !this.transports[sessionId]) {
117
+ if (!sessionId || !this.sessionManager.getSession(sessionId)) {
109
118
  res.status(400).send('Invalid or missing session ID');
110
119
  return;
111
120
  }
112
121
 
113
- const transport = this.transports[sessionId];
122
+ const session = this.sessionManager.getSession(sessionId);
123
+
124
+ if (!session) {
125
+ throw new Error('Session not found');
126
+ }
127
+
128
+ const { transport } = session;
129
+
130
+ if (!(transport instanceof StreamableHTTPServerTransport)) {
131
+ throw new Error('Invalid transport');
132
+ }
114
133
 
115
134
  await transport.handleRequest(req, res);
116
135
  }
@@ -125,10 +144,7 @@ export class StreamableService implements OnModuleInit {
125
144
  * @param req Express Request object
126
145
  * @param res Express Response object
127
146
  */
128
- async handleDeleteRequest(
129
- req: Request<any, any, any, { sessionId?: string }>,
130
- res: Response,
131
- ) {
147
+ async handleDeleteRequest(req: Request, res: Response) {
132
148
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
133
149
 
134
150
  if (!sessionId) {
@@ -136,7 +152,13 @@ export class StreamableService implements OnModuleInit {
136
152
  return;
137
153
  }
138
154
 
139
- const transport = this.transports[sessionId];
155
+ const session = this.sessionManager.getSession(sessionId);
156
+
157
+ if (!session) {
158
+ throw new Error('Session not found');
159
+ }
160
+
161
+ const { transport } = session;
140
162
 
141
163
  if (transport) {
142
164
  this.logger.debug(
@@ -154,7 +176,7 @@ export class StreamableService implements OnModuleInit {
154
176
  return;
155
177
  }
156
178
 
157
- delete this.transports[sessionId];
179
+ this.sessionManager.deleteSession(sessionId);
158
180
 
159
181
  res.status(200).json({ success: true, sessionId });
160
182
  } else {
@@ -2,7 +2,19 @@ import {
2
2
  CompleteResourceTemplateCallback,
3
3
  ListResourcesCallback,
4
4
  } from '@modelcontextprotocol/sdk/server/mcp.js';
5
- import { ZodOptional, ZodRawShape, ZodType, ZodTypeDef } from 'zod';
5
+ import { RequestHandlerExtra as SdkRequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol.js';
6
+ import {
7
+ ServerNotification,
8
+ ServerRequest,
9
+ } from '@modelcontextprotocol/sdk/types.js';
10
+ import {
11
+ z,
12
+ ZodOptional,
13
+ ZodRawShape,
14
+ ZodType,
15
+ ZodTypeAny,
16
+ ZodTypeDef,
17
+ } from 'zod';
6
18
 
7
19
  export interface ResourceBaseOptions {
8
20
  name: string;
@@ -93,3 +105,102 @@ export interface TemplateCallbacks {
93
105
  [variable: string]: CompleteResourceTemplateCallback;
94
106
  };
95
107
  }
108
+
109
+ export type RequestHandlerExtra = SdkRequestHandlerExtra<
110
+ ServerRequest,
111
+ ServerNotification
112
+ >;
113
+
114
+ export class ResourceUriHandlerParams {
115
+ public readonly uri: URL;
116
+ public readonly extra: RequestHandlerExtra;
117
+
118
+ private constructor(uri: URL, extra: RequestHandlerExtra) {
119
+ this.uri = uri;
120
+ this.extra = extra;
121
+ }
122
+
123
+ static from(uri: URL, extra: RequestHandlerExtra): ResourceUriHandlerParams {
124
+ return new ResourceUriHandlerParams(uri, extra);
125
+ }
126
+ }
127
+
128
+ export class ResourceTemplateHandlerParams {
129
+ public readonly uri: URL;
130
+ public readonly variables?: Record<string, string>;
131
+ public readonly extra: RequestHandlerExtra;
132
+
133
+ private constructor(
134
+ uri: URL,
135
+ extra: RequestHandlerExtra,
136
+ variables?: Record<string, string>,
137
+ ) {
138
+ this.uri = uri;
139
+ this.extra = extra;
140
+ this.variables = variables;
141
+ }
142
+
143
+ static from(
144
+ uri: URL,
145
+ extra: RequestHandlerExtra,
146
+ variables?: Record<string, string>,
147
+ ): ResourceTemplateHandlerParams {
148
+ return new ResourceTemplateHandlerParams(uri, extra, variables);
149
+ }
150
+ }
151
+
152
+ export class PromptHandlerParams<
153
+ Args extends PromptArgsRawShape | undefined = undefined,
154
+ > {
155
+ public readonly args?: Args extends PromptArgsRawShape
156
+ ? z.objectOutputType<Args, ZodTypeAny>
157
+ : undefined;
158
+ public readonly extra: RequestHandlerExtra;
159
+
160
+ private constructor(
161
+ extra: RequestHandlerExtra,
162
+ args?: Args extends PromptArgsRawShape
163
+ ? z.objectOutputType<Args, ZodTypeAny>
164
+ : undefined,
165
+ ) {
166
+ this.extra = extra;
167
+ this.args = args;
168
+ }
169
+
170
+ static from<Args extends PromptArgsRawShape | undefined>(
171
+ extra: RequestHandlerExtra,
172
+ args?: Args extends PromptArgsRawShape
173
+ ? z.objectOutputType<Args, ZodTypeAny>
174
+ : undefined,
175
+ ): PromptHandlerParams<Args> {
176
+ return new PromptHandlerParams<Args>(extra, args);
177
+ }
178
+ }
179
+
180
+ export class ToolHandlerParams<
181
+ Args extends ZodRawShape | undefined = undefined,
182
+ > {
183
+ public readonly args?: Args extends ZodRawShape
184
+ ? z.objectOutputType<Args, ZodTypeAny>
185
+ : undefined;
186
+ public readonly extra: RequestHandlerExtra;
187
+
188
+ private constructor(
189
+ extra: RequestHandlerExtra,
190
+ args?: Args extends ZodRawShape
191
+ ? z.objectOutputType<Args, ZodTypeAny>
192
+ : undefined,
193
+ ) {
194
+ this.extra = extra;
195
+ this.args = args;
196
+ }
197
+
198
+ static from<Args extends ZodRawShape | undefined>(
199
+ extra: RequestHandlerExtra,
200
+ args?: Args extends ZodRawShape
201
+ ? z.objectOutputType<Args, ZodTypeAny>
202
+ : undefined,
203
+ ): ToolHandlerParams<Args> {
204
+ return new ToolHandlerParams<Args>(extra, args);
205
+ }
206
+ }
@@ -1,6 +1,11 @@
1
1
  import { ExecutionContext } from '@nestjs/common';
2
2
 
3
- import { McpMessage } from './message.types';
3
+ import {
4
+ PromptHandlerParams,
5
+ ResourceTemplateHandlerParams,
6
+ ResourceUriHandlerParams,
7
+ ToolHandlerParams,
8
+ } from './capabilities.interface';
4
9
  /**
5
10
  * Custom execution context for MCP guards.
6
11
  * Extends NestJS ExecutionContext and adds args for MCP method arguments.
@@ -8,11 +13,21 @@ import { McpMessage } from './message.types';
8
13
  * @property args - The arguments passed to the MCP method
9
14
  * @property message - The current message from the request
10
15
  */
11
- // TODO: Type Args correctly
16
+ // TODO: Remove extends ExecutionContext we don't need it
12
17
  export interface McpExecutionContext extends ExecutionContext {
13
- /** The arguments passed to the MCP method */
14
- args: unknown[];
18
+ // TODO: Remove this once the getArgs method implementation is complete.
19
+ args:
20
+ | ResourceUriHandlerParams
21
+ | ResourceTemplateHandlerParams
22
+ | PromptHandlerParams
23
+ | ToolHandlerParams;
15
24
 
16
- /** The current message from the request */
17
- message: McpMessage | undefined;
25
+ // TODO: Uncomment this once the getArgs type is fixed
26
+ // getArgs: () =>
27
+ // | ResourceUriHandlerParams
28
+ // | ResourceTemplateHandlerParams
29
+ // | PromptHandlerParams
30
+ // | ToolHandlerParams;
31
+
32
+ getSessionId: () => string;
18
33
  }
package/src/mcp.module.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Implementation } from '@modelcontextprotocol/sdk/types.js';
2
2
  import { DynamicModule, Module, Provider, Type } from '@nestjs/common';
3
- import { APP_INTERCEPTOR, DiscoveryModule } from '@nestjs/core';
3
+ import { DiscoveryModule } from '@nestjs/core';
4
4
  import { AsyncLocalStorage } from 'async_hooks';
5
5
 
6
6
  import { SseController, SseService } from './controllers/sse';
@@ -8,7 +8,6 @@ import {
8
8
  StreamableController,
9
9
  StreamableService,
10
10
  } from './controllers/streamable';
11
- import { RequestContextInterceptor } from './interceptors/message.interceptor';
12
11
  import {
13
12
  McpFeatureOptions,
14
13
  McpLoggingOptions,
@@ -19,8 +18,7 @@ import {
19
18
  import { DiscoveryService } from './registry/discovery.service';
20
19
  import { McpLoggerService } from './registry/logger.service';
21
20
  import { RegistryService } from './registry/registry.service';
22
- import { MessageService } from './services/message.service';
23
-
21
+ import { SessionManager } from './services/session.manager';
24
22
  @Module({
25
23
  imports: [DiscoveryModule],
26
24
  providers: [
@@ -31,13 +29,9 @@ import { MessageService } from './services/message.service';
31
29
  useValue: new AsyncLocalStorage(),
32
30
  },
33
31
  McpLoggerService,
34
- MessageService,
35
- {
36
- provide: APP_INTERCEPTOR,
37
- useClass: RequestContextInterceptor,
38
- },
32
+ SessionManager,
39
33
  ],
40
- exports: [MessageService],
34
+ exports: [SessionManager],
41
35
  })
42
36
  export class McpModule {
43
37
  /**
@@ -4,6 +4,7 @@ import {
4
4
  } from '@modelcontextprotocol/sdk/server/mcp.js';
5
5
  import type { CanActivate, Type } from '@nestjs/common';
6
6
  import { Injectable } from '@nestjs/common';
7
+ import { Reflector } from '@nestjs/core';
7
8
 
8
9
  import { MCP_RESOLVER } from '../decorators';
9
10
  import {
@@ -13,20 +14,27 @@ import {
13
14
  } from '../decorators/capabilities.constants';
14
15
  import { MCP_GUARDS } from '../decorators/capabilities.decorators';
15
16
  import {
17
+ PromptHandlerParams,
16
18
  PromptOptions,
19
+ RequestHandlerExtra,
17
20
  ResourceOptions,
21
+ ResourceTemplateHandlerParams,
22
+ ResourceUriHandlerParams,
23
+ ToolHandlerParams,
18
24
  ToolOptions,
19
25
  } from '../interfaces/capabilities.interface';
20
26
  import { McpExecutionContext } from '../interfaces/context.interface';
21
- import { MessageService } from '../services/message.service';
27
+ import { SessionManager } from '../services/session.manager';
22
28
  import { DiscoveryService } from './discovery.service';
23
29
  import { McpLoggerService } from './logger.service';
30
+
24
31
  @Injectable()
25
32
  export class RegistryService {
26
33
  constructor(
27
34
  private readonly discoveryService: DiscoveryService,
28
35
  private readonly logger: McpLoggerService,
29
- private readonly messageService: MessageService,
36
+ private readonly reflector: Reflector,
37
+ private readonly sessionManager: SessionManager,
30
38
  ) {}
31
39
 
32
40
  async registerAll(server: McpServer): Promise<void> {
@@ -42,6 +50,57 @@ export class RegistryService {
42
50
  ]);
43
51
  }
44
52
 
53
+ private getDecoratorType(method: Type<any> | undefined): string | null {
54
+ if (!method) return null;
55
+
56
+ if (this.reflector.get(MCP_TOOL, method)) return 'TOOL';
57
+ if (this.reflector.get(MCP_PROMPT, method)) return 'PROMPT';
58
+ if (this.reflector.get(MCP_RESOURCE, method)) return 'RESOURCE';
59
+
60
+ return null;
61
+ }
62
+
63
+ private getHandlerArgs(
64
+ method: Type<any> | undefined,
65
+ args: unknown[],
66
+ ):
67
+ | ResourceUriHandlerParams
68
+ | ResourceTemplateHandlerParams
69
+ | PromptHandlerParams
70
+ | ToolHandlerParams {
71
+ if (!method) throw new Error('Method not found');
72
+
73
+ switch (this.getDecoratorType(method)) {
74
+ case 'RESOURCE':
75
+ return args[0] instanceof URL
76
+ ? ResourceUriHandlerParams.from(
77
+ args[0],
78
+ args[1] as RequestHandlerExtra,
79
+ )
80
+ : ResourceTemplateHandlerParams.from(
81
+ args[0] as any,
82
+ args[2] as RequestHandlerExtra,
83
+ args[1] as Record<string, string>,
84
+ );
85
+ case 'PROMPT':
86
+ return args.length === 1
87
+ ? PromptHandlerParams.from(args[0] as RequestHandlerExtra)
88
+ : PromptHandlerParams.from(
89
+ args[1] as RequestHandlerExtra,
90
+ args[0] as any,
91
+ );
92
+ case 'TOOL':
93
+ return args.length === 1
94
+ ? ToolHandlerParams.from(args[0] as RequestHandlerExtra)
95
+ : ToolHandlerParams.from(
96
+ args[1] as RequestHandlerExtra,
97
+ args[0] as any,
98
+ );
99
+ default:
100
+ throw new Error(`Unknown decorator type for method ${method.name}`);
101
+ }
102
+ }
103
+
45
104
  /**
46
105
  * Executes all guards attached to the resolver class and method.
47
106
  * Throws an error if any guard denies access.
@@ -58,6 +117,7 @@ export class RegistryService {
58
117
  ): Promise<void> {
59
118
  // Retrieve class-level guards
60
119
  const classConstructor = instance.constructor;
120
+
61
121
  const classGuards: (CanActivate | { new (): CanActivate })[] =
62
122
  (Reflect.getMetadata(MCP_GUARDS, classConstructor) as (
63
123
  | CanActivate
@@ -69,7 +129,8 @@ export class RegistryService {
69
129
  string,
70
130
  unknown
71
131
  >;
72
- const methodKey = prototype[methodName] as object | undefined;
132
+
133
+ const methodKey = prototype[methodName] as Type<any> | undefined;
73
134
 
74
135
  const methodGuards: (CanActivate | { new (): CanActivate })[] =
75
136
  (methodKey &&
@@ -83,16 +144,39 @@ export class RegistryService {
83
144
  const allGuards = [...classGuards, ...methodGuards];
84
145
 
85
146
  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
147
 
91
- // @ts-expect-error: Default types are 'http' | 'ws' | 'rpc' but in our case
92
- // we are using 'mcp'
148
+ const handlerArgs = this.getHandlerArgs(methodKey, args);
149
+
150
+ const { sessionId } = handlerArgs.extra;
151
+
152
+ if (!sessionId) {
153
+ throw new Error('Session not found');
154
+ }
155
+
156
+ const session = this.sessionManager.getSession(sessionId);
157
+
158
+ if (!session) {
159
+ throw new Error('Session not found');
160
+ }
161
+
162
+ const context: McpExecutionContext = {
163
+ args: handlerArgs,
164
+ // @ts-expect-error: Default types are 'http' | 'ws' | 'rpc' but in our case, we are using 'mcp'
93
165
  getType: () => 'mcp',
94
166
  getClass: () => instance.constructor as Type<any>,
95
- getArgs: <T extends Array<any>>() => args as T,
167
+ getArgs: <T = any>() => args as T,
168
+ getArgByIndex: <T = any>(index: number) => args[index] as T,
169
+ getSessionId: () => sessionId,
170
+ getHandler: () => methodKey as unknown as Type<any>,
171
+ switchToHttp: () => ({
172
+ getRequest: <R = Request>() => session.request as R,
173
+ getResponse: () => {
174
+ throw new Error('Response not available in MCP context');
175
+ },
176
+ getNext: () => {
177
+ throw new Error('Next not available in MCP context');
178
+ },
179
+ }),
96
180
  };
97
181
 
98
182
  return (async () => {
@@ -0,0 +1,26 @@
1
+ import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse';
2
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3
+ import { Injectable } from '@nestjs/common';
4
+ import { Request } from 'express';
5
+
6
+ type Session = {
7
+ transport: StreamableHTTPServerTransport | SSEServerTransport;
8
+ request: Request;
9
+ };
10
+
11
+ @Injectable()
12
+ export class SessionManager {
13
+ private sessions = new Map<string, Session>();
14
+
15
+ public getSession(sessionId: string): Session | undefined {
16
+ return this.sessions.get(sessionId);
17
+ }
18
+
19
+ public setSession(sessionId: string, session: Session): void {
20
+ this.sessions.set(sessionId, session);
21
+ }
22
+
23
+ public deleteSession(sessionId: string): void {
24
+ this.sessions.delete(sessionId);
25
+ }
26
+ }
@@ -1,10 +0,0 @@
1
- import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
2
- import { Observable } from 'rxjs';
3
- import { McpLoggerService } from '../registry/logger.service';
4
- import { MessageService } from '../services/message.service';
5
- export declare class RequestContextInterceptor implements NestInterceptor {
6
- private readonly logger;
7
- private readonly messageService;
8
- constructor(logger: McpLoggerService, messageService: MessageService);
9
- intercept(context: ExecutionContext, next: CallHandler): Observable<any>;
10
- }