@rekog/mcp-nest 1.8.2-alpha.1 → 1.8.2-alpha.2

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.
@@ -1,10 +1,12 @@
1
+ import { OnModuleDestroy } from '@nestjs/common';
1
2
  import { ModuleRef } from '@nestjs/core';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
4
5
  import { HttpRequest, HttpResponse } from '../interfaces/http-adapter.interface';
5
6
  import { McpOptions } from '../interfaces';
7
+ import { McpExecutorService } from './mcp-executor.service';
6
8
  import { McpRegistryService } from './mcp-registry.service';
7
- export declare class McpStreamableHttpService {
9
+ export declare class McpStreamableHttpService implements OnModuleDestroy {
8
10
  private readonly options;
9
11
  private readonly mcpModuleId;
10
12
  private readonly moduleRef;
@@ -12,18 +14,23 @@ export declare class McpStreamableHttpService {
12
14
  private readonly logger;
13
15
  private readonly transports;
14
16
  private readonly mcpServers;
17
+ private readonly sessionExecutors;
15
18
  private readonly isStatelessMode;
16
19
  constructor(options: McpOptions, mcpModuleId: string, moduleRef: ModuleRef, toolRegistry: McpRegistryService);
20
+ private createMcpServer;
17
21
  createStatelessServer(rawReq: any): Promise<{
18
22
  server: McpServer;
19
23
  transport: StreamableHTTPServerTransport;
24
+ executor: McpExecutorService;
20
25
  }>;
21
26
  handlePostRequest(req: any, res: any, body: unknown): Promise<void>;
22
- handleStatelessRequest(req: any, res: HttpResponse, body: unknown): Promise<void>;
27
+ handleStatelessRequest(req: HttpRequest, res: HttpResponse, body: unknown): Promise<void>;
28
+ private cleanupStatelessResources;
23
29
  handleStatefulRequest(req: HttpRequest, res: HttpResponse, body: unknown): Promise<void>;
24
30
  handleGetRequest(req: any, res: any): Promise<void>;
25
31
  handleDeleteRequest(req: any, res: any): Promise<void>;
26
- isInitializeRequest(body: unknown): boolean;
27
- cleanupSession(sessionId: string): void;
32
+ private isInitializeRequest;
33
+ private cleanupSession;
34
+ onModuleDestroy(): Promise<void>;
28
35
  }
29
36
  //# sourceMappingURL=mcp-streamable-http.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-streamable-http.service.d.ts","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":"AACA,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAEnG,OAAO,EACL,WAAW,EACX,YAAY,EACb,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,qBACa,wBAAwB;IASV,OAAO,CAAC,QAAQ,CAAC,OAAO;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAX/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6C;IACpE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAEpB;IACP,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IACrE,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;gBAGA,OAAO,EAAE,UAAU,EACjB,WAAW,EAAE,MAAM,EAC5C,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,kBAAkB;IAS7C,qBAAqB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC;QAChD,MAAM,EAAE,SAAS,CAAC;QAClB,SAAS,EAAE,6BAA6B,CAAC;KAC1C,CAAC;IAiDI,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAuCnE,sBAAsB,CAC1B,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;IAqCV,qBAAqB,CACzB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;IAkIV,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAwCnD,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAuC5D,mBAAmB,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO;IAmB3C,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAOxC"}
1
+ {"version":3,"file":"mcp-streamable-http.service.d.ts","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAEnG,OAAO,EACL,WAAW,EACX,YAAY,EACb,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,qBACa,wBAAyB,YAAW,eAAe;IAerC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAjB/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6C;IAGpE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAEpB;IACP,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IACrE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAE1B;IAEP,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;gBAGA,OAAO,EAAE,UAAU,EACjB,WAAW,EAAE,MAAM,EAC5C,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,kBAAkB;IAQnD,OAAO,CAAC,eAAe;IAqBjB,qBAAqB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC;QAChD,MAAM,EAAE,SAAS,CAAC;QAClB,SAAS,EAAE,6BAA6B,CAAC;QACzC,QAAQ,EAAE,kBAAkB,CAAC;KAC9B,CAAC;IA+BI,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCnE,sBAAsB,CAC1B,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;YA8BF,yBAAyB;IAmBjC,qBAAqB,CACzB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;IAyHV,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDnD,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAwD5D,OAAO,CAAC,mBAAmB;YAqBb,cAAc;IAgCtB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAUvC"}
@@ -32,32 +32,34 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
32
32
  this.logger = new common_1.Logger(McpStreamableHttpService_1.name);
33
33
  this.transports = {};
34
34
  this.mcpServers = {};
35
+ this.sessionExecutors = {};
35
36
  this.isStatelessMode = !!options.streamableHttp?.statelessMode;
36
37
  }
38
+ createMcpServer() {
39
+ const capabilities = (0, capabilities_builder_1.buildMcpCapabilities)(this.mcpModuleId, this.toolRegistry, this.options);
40
+ this.logger.debug('Building MCP server with capabilities:', capabilities);
41
+ return new mcp_js_1.McpServer({ name: this.options.name, version: this.options.version }, {
42
+ capabilities,
43
+ instructions: this.options.instructions || '',
44
+ });
45
+ }
37
46
  async createStatelessServer(rawReq) {
38
47
  const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
39
48
  sessionIdGenerator: undefined,
40
49
  enableJsonResponse: this.options.streamableHttp?.enableJsonResponse || false,
41
50
  });
42
- const capabilities = (0, capabilities_builder_1.buildMcpCapabilities)(this.mcpModuleId, this.toolRegistry, this.options);
43
- this.logger.debug(`[Stateless] Built MCP capabilities: ${JSON.stringify(capabilities)}`);
44
- const server = new mcp_js_1.McpServer({ name: this.options.name, version: this.options.version }, {
45
- capabilities: capabilities,
46
- instructions: this.options.instructions || '',
47
- });
51
+ const server = this.createMcpServer();
48
52
  await server.connect(transport);
49
53
  const contextId = core_1.ContextIdFactory.getByRequest(rawReq);
50
54
  const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
51
- this.logger.debug('[Stateless] Registering request handlers for stateless MCP server');
52
55
  executor.registerRequestHandlers(server, rawReq);
53
- return { server, transport };
56
+ return { server, transport, executor };
54
57
  }
55
58
  async handlePostRequest(req, res, body) {
59
+ this.logger.debug('Received MCP POST request');
56
60
  const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
57
61
  const adaptedReq = adapter.adaptRequest(req);
58
62
  const adaptedRes = adapter.adaptResponse(res);
59
- const sessionId = adaptedReq.headers['mcp-session-id'];
60
- this.logger.debug(`[${sessionId || 'No-Session'}] Received MCP request: ${JSON.stringify(body)}`);
61
63
  try {
62
64
  if (this.isStatelessMode) {
63
65
  return this.handleStatelessRequest(adaptedReq, adaptedRes, body);
@@ -67,7 +69,7 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
67
69
  }
68
70
  }
69
71
  catch (error) {
70
- this.logger.error(`[${sessionId || 'No-Session'}] Error handling MCP request: ${error}`);
72
+ this.logger.error('Error handling MCP request:', error);
71
73
  if (!adaptedRes.headersSent) {
72
74
  adaptedRes.status(500).json({
73
75
  jsonrpc: '2.0',
@@ -81,7 +83,7 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
81
83
  }
82
84
  }
83
85
  async handleStatelessRequest(req, res, body) {
84
- this.logger.debug(`[Stateless] Handling stateless MCP request at ${req.url} with body: ${JSON.stringify(body)}`);
86
+ this.logger.debug('Handling stateless MCP request');
85
87
  let server = null;
86
88
  let transport = null;
87
89
  try {
@@ -89,69 +91,125 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
89
91
  server = stateless.server;
90
92
  transport = stateless.transport;
91
93
  await transport.handleRequest(req.raw, res.raw, body);
92
- res.on?.('close', () => {
93
- this.logger.debug('[Stateless] Request closed, cleaning up');
94
- void transport?.close();
95
- void server?.close();
94
+ res.on?.('finish', async () => {
95
+ this.logger.debug('Stateless response finished, cleaning up');
96
+ await this.cleanupStatelessResources(server, transport);
96
97
  });
97
98
  }
98
99
  catch (error) {
99
- this.logger.error(`[Stateless] Error in stateless request handling: ${error}`);
100
- void transport?.close();
101
- void server?.close();
100
+ this.logger.error('Error in stateless request handling:', error);
101
+ await this.cleanupStatelessResources(server, transport);
102
102
  throw error;
103
103
  }
104
104
  }
105
+ async cleanupStatelessResources(server, transport) {
106
+ try {
107
+ if (transport) {
108
+ await transport.close();
109
+ }
110
+ if (server) {
111
+ await server.close();
112
+ }
113
+ }
114
+ catch (error) {
115
+ this.logger.error('Error cleaning up stateless resources:', error);
116
+ }
117
+ }
105
118
  async handleStatefulRequest(req, res, body) {
106
- this.logger.debug(`[Stateful] Handling stateful MCP request at ${req.url} with body: ${JSON.stringify(body)}`);
119
+ this.logger.debug('Handling stateful MCP request');
107
120
  const sessionId = req.headers['mcp-session-id'];
108
- let transport;
109
- if (sessionId && this.transports[sessionId]) {
110
- transport = this.transports[sessionId];
111
- }
112
- else if (!sessionId && this.isInitializeRequest(body)) {
113
- const capabilities = (0, capabilities_builder_1.buildMcpCapabilities)(this.mcpModuleId, this.toolRegistry, this.options);
114
- this.logger.debug(`[${sessionId || 'New-Session'}] Built MCP capabilities: ${JSON.stringify(capabilities)}`);
115
- const mcpServer = new mcp_js_1.McpServer({ name: this.options.name, version: this.options.version }, {
116
- capabilities,
117
- instructions: this.options.instructions || '',
118
- });
119
- transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
121
+ if (!sessionId && this.isInitializeRequest(body)) {
122
+ if (Array.isArray(body) && body.length > 1) {
123
+ res.status(400).json({
124
+ jsonrpc: '2.0',
125
+ error: {
126
+ code: -32600,
127
+ message: 'Invalid Request: Only one initialization request is allowed',
128
+ },
129
+ id: null,
130
+ });
131
+ return;
132
+ }
133
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
120
134
  sessionIdGenerator: this.options.streamableHttp?.sessionIdGenerator ||
121
135
  (() => (0, crypto_1.randomUUID)()),
122
136
  enableJsonResponse: this.options.streamableHttp?.enableJsonResponse || false,
123
- onsessioninitialized: (sid) => {
124
- this.logger.debug(`[${sid}] Session initialized`);
125
- this.transports[sid] = transport;
126
- this.mcpServers[sid] = mcpServer;
137
+ onsessioninitialized: (sessionId) => {
138
+ this.logger.log(`Session initialized: ${sessionId}`);
139
+ this.transports[sessionId] = transport;
140
+ },
141
+ onsessionclosed: async (sessionId) => {
142
+ this.logger.log(`Session termination requested: ${sessionId}`);
143
+ await this.cleanupSession(sessionId);
127
144
  },
128
145
  });
129
- transport.onclose = () => {
130
- if (transport.sessionId) {
131
- this.cleanupSession(transport.sessionId);
132
- }
133
- };
146
+ const mcpServer = this.createMcpServer();
134
147
  await mcpServer.connect(transport);
135
148
  const contextId = core_1.ContextIdFactory.getByRequest(req);
136
149
  const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
137
150
  executor.registerRequestHandlers(mcpServer, req);
138
151
  await transport.handleRequest(req.raw, res.raw, body);
139
- this.logger.log(`[${transport.sessionId}] Initialized new session`);
152
+ if (transport.sessionId) {
153
+ this.mcpServers[transport.sessionId] = mcpServer;
154
+ this.sessionExecutors[transport.sessionId] = executor;
155
+ this.logger.log(`Session fully initialized: ${transport.sessionId}`);
156
+ }
157
+ return;
158
+ }
159
+ if (sessionId) {
160
+ if (!this.transports[sessionId]) {
161
+ res.status(404).json({
162
+ jsonrpc: '2.0',
163
+ error: {
164
+ code: -32001,
165
+ message: 'Session not found',
166
+ },
167
+ id: null,
168
+ });
169
+ return;
170
+ }
171
+ if (this.isInitializeRequest(body)) {
172
+ res.status(400).json({
173
+ jsonrpc: '2.0',
174
+ error: {
175
+ code: -32600,
176
+ message: 'Invalid Request: Server already initialized',
177
+ },
178
+ id: null,
179
+ });
180
+ return;
181
+ }
182
+ const transport = this.transports[sessionId];
183
+ await transport.handleRequest(req.raw, res.raw, body);
140
184
  return;
141
185
  }
142
- else if (sessionId && !this.transports[sessionId]) {
143
- res.status(404).json({
186
+ res.status(400).json({
187
+ jsonrpc: '2.0',
188
+ error: {
189
+ code: -32000,
190
+ message: 'Bad Request: No valid session ID provided',
191
+ },
192
+ id: null,
193
+ });
194
+ }
195
+ async handleGetRequest(req, res) {
196
+ const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
197
+ const adaptedReq = adapter.adaptRequest(req);
198
+ const adaptedRes = adapter.adaptResponse(res);
199
+ if (this.isStatelessMode) {
200
+ adaptedRes.status(405).json({
144
201
  jsonrpc: '2.0',
145
202
  error: {
146
- code: -32001,
147
- message: 'Session not found',
203
+ code: -32000,
204
+ message: 'Method not allowed in stateless mode',
148
205
  },
149
206
  id: null,
150
207
  });
151
208
  return;
152
209
  }
153
- else {
154
- res.status(400).json({
210
+ const sessionId = adaptedReq.headers['mcp-session-id'];
211
+ if (!sessionId) {
212
+ adaptedRes.status(400).json({
155
213
  jsonrpc: '2.0',
156
214
  error: {
157
215
  code: -32000,
@@ -161,10 +219,8 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
161
219
  });
162
220
  return;
163
221
  }
164
- const mcpServer = this.mcpServers[sessionId];
165
- if (!mcpServer) {
166
- this.logger.debug(`[${sessionId}] Session not found`);
167
- res.status(404).json({
222
+ if (!this.transports[sessionId]) {
223
+ adaptedRes.status(404).json({
168
224
  jsonrpc: '2.0',
169
225
  error: {
170
226
  code: -32001,
@@ -174,13 +230,11 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
174
230
  });
175
231
  return;
176
232
  }
177
- const contextId = core_1.ContextIdFactory.getByRequest(req);
178
- const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
179
- executor.registerRequestHandlers(mcpServer, req);
180
- this.logger.debug(`[${sessionId}] Handling subsequent request`);
181
- await transport.handleRequest(req.raw, res.raw, body);
233
+ this.logger.debug(`Establishing SSE stream for session ${sessionId}`);
234
+ const transport = this.transports[sessionId];
235
+ await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
182
236
  }
183
- async handleGetRequest(req, res) {
237
+ async handleDeleteRequest(req, res) {
184
238
  const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
185
239
  const adaptedReq = adapter.adaptRequest(req);
186
240
  const adaptedRes = adapter.adaptResponse(res);
@@ -196,39 +250,29 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
196
250
  return;
197
251
  }
198
252
  const sessionId = adaptedReq.headers['mcp-session-id'];
199
- if (!sessionId || !this.transports[sessionId]) {
200
- this.logger.debug(`[${sessionId || 'No-Session'}] GET request failed - session not found`);
201
- adaptedRes
202
- .status(400)
203
- .send('Bad Request: Mcp-Session-Id header is required');
204
- return;
205
- }
206
- this.logger.debug(`[${sessionId}] Establishing SSE stream`);
207
- const transport = this.transports[sessionId];
208
- await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
209
- }
210
- async handleDeleteRequest(req, res) {
211
- const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
212
- const adaptedReq = adapter.adaptRequest(req);
213
- const adaptedRes = adapter.adaptResponse(res);
214
- if (this.isStatelessMode) {
215
- adaptedRes.status(405).json({
253
+ if (!sessionId) {
254
+ adaptedRes.status(400).json({
216
255
  jsonrpc: '2.0',
217
256
  error: {
218
257
  code: -32000,
219
- message: 'Method not allowed in stateless mode',
258
+ message: 'Bad Request: Mcp-Session-Id header is required',
220
259
  },
221
260
  id: null,
222
261
  });
223
262
  return;
224
263
  }
225
- const sessionId = adaptedReq.headers['mcp-session-id'];
226
- if (!sessionId || !this.transports[sessionId]) {
227
- this.logger.debug(`[${sessionId || 'No-Session'}] DELETE request failed - session not found`);
228
- adaptedRes.status(400).send('Invalid or missing session ID');
264
+ if (!this.transports[sessionId]) {
265
+ adaptedRes.status(404).json({
266
+ jsonrpc: '2.0',
267
+ error: {
268
+ code: -32001,
269
+ message: 'Session not found',
270
+ },
271
+ id: null,
272
+ });
229
273
  return;
230
274
  }
231
- this.logger.debug(`[${sessionId}] Terminating session`);
275
+ this.logger.debug(`Processing DELETE request for session ${sessionId}`);
232
276
  const transport = this.transports[sessionId];
233
277
  await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
234
278
  }
@@ -244,12 +288,33 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
244
288
  'method' in body &&
245
289
  body.method === 'initialize');
246
290
  }
247
- cleanupSession(sessionId) {
248
- if (sessionId) {
249
- this.logger.debug(`[${sessionId}] Cleaning up session`);
291
+ async cleanupSession(sessionId) {
292
+ if (!sessionId)
293
+ return;
294
+ this.logger.debug(`Cleaning up session: ${sessionId}`);
295
+ try {
296
+ const transport = this.transports[sessionId];
297
+ if (transport) {
298
+ await transport.close();
299
+ }
250
300
  delete this.transports[sessionId];
301
+ const server = this.mcpServers[sessionId];
302
+ if (server) {
303
+ await server.close();
304
+ }
251
305
  delete this.mcpServers[sessionId];
306
+ delete this.sessionExecutors[sessionId];
307
+ this.logger.log(`Session cleanup complete: ${sessionId}`);
252
308
  }
309
+ catch (error) {
310
+ this.logger.error(`Error cleaning up session ${sessionId}:`, error);
311
+ }
312
+ }
313
+ async onModuleDestroy() {
314
+ this.logger.log('Cleaning up all MCP sessions...');
315
+ const sessionIds = Object.keys(this.transports);
316
+ await Promise.all(sessionIds.map((sessionId) => this.cleanupSession(sessionId)));
317
+ this.logger.log('All MCP sessions cleaned up');
253
318
  }
254
319
  };
255
320
  exports.McpStreamableHttpService = McpStreamableHttpService;
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-streamable-http.service.js","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Inject, Injectable, Logger } from '@nestjs/common';\nimport { ContextIdFactory, ModuleRef } from '@nestjs/core';\nimport { randomUUID } from 'crypto';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { HttpAdapterFactory } from '../adapters/http-adapter.factory';\nimport {\n HttpRequest,\n HttpResponse,\n} from '../interfaces/http-adapter.interface';\nimport { McpOptions } from '../interfaces';\nimport { McpExecutorService } from './mcp-executor.service';\nimport { McpRegistryService } from './mcp-registry.service';\nimport { buildMcpCapabilities } from '../utils/capabilities-builder';\n\n@Injectable()\nexport class McpStreamableHttpService {\n private readonly logger = new Logger(McpStreamableHttpService.name);\n private readonly transports: {\n [sessionId: string]: StreamableHTTPServerTransport;\n } = {};\n private readonly mcpServers: { [sessionId: string]: McpServer } = {};\n private readonly isStatelessMode: boolean;\n\n constructor(\n @Inject('MCP_OPTIONS') private readonly options: McpOptions,\n @Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,\n private readonly moduleRef: ModuleRef,\n private readonly toolRegistry: McpRegistryService,\n ) {\n // Determine if we're in stateless mode\n this.isStatelessMode = !!options.streamableHttp?.statelessMode;\n }\n\n /**\n * Create a new MCP server instance for stateless requests\n */\n async createStatelessServer(rawReq: any): Promise<{\n server: McpServer;\n transport: StreamableHTTPServerTransport;\n }> {\n // Create a new transport for this request (stateless = no session management)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse:\n this.options.streamableHttp?.enableJsonResponse || false,\n });\n\n // Create a new MCP server instance with dynamic capabilities\n const capabilities = buildMcpCapabilities(\n this.mcpModuleId,\n this.toolRegistry,\n this.options,\n );\n this.logger.debug(\n `[Stateless] Built MCP capabilities: ${JSON.stringify(capabilities)}`,\n );\n\n const server = new McpServer(\n { name: this.options.name, version: this.options.version },\n {\n capabilities: capabilities,\n instructions: this.options.instructions || '',\n },\n );\n\n // Connect the transport to the MCP server first\n await server.connect(transport);\n\n // Now resolve the request-scoped tool executor service\n const contextId = ContextIdFactory.getByRequest(rawReq);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n\n // Register request handlers after connection\n this.logger.debug(\n '[Stateless] Registering request handlers for stateless MCP server',\n );\n executor.registerRequestHandlers(server, rawReq);\n\n return { server, transport };\n }\n\n /**\n * Handle POST requests\n */\n async handlePostRequest(req: any, res: any, body: unknown): Promise<void> {\n // Get the appropriate HTTP adapter for the request/response\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n this.logger.debug(\n `[${sessionId || 'No-Session'}] Received MCP request: ${JSON.stringify(body)}`,\n );\n\n try {\n if (this.isStatelessMode) {\n return this.handleStatelessRequest(adaptedReq, adaptedRes, body);\n } else {\n return this.handleStatefulRequest(adaptedReq, adaptedRes, body);\n }\n } catch (error) {\n this.logger.error(\n `[${sessionId || 'No-Session'}] Error handling MCP request: ${error}`,\n );\n if (!adaptedRes.headersSent) {\n adaptedRes.status(500).json({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: 'Internal server error',\n },\n id: null,\n });\n }\n }\n }\n\n /**\n * Handle requests in stateless mode\n */\n async handleStatelessRequest(\n req: any,\n res: HttpResponse,\n body: unknown,\n ): Promise<void> {\n this.logger.debug(\n `[Stateless] Handling stateless MCP request at ${req.url} with body: ${JSON.stringify(body)}`,\n );\n\n let server: McpServer | null = null;\n let transport: StreamableHTTPServerTransport | null = null;\n\n try {\n // Create a new server and transport for each request\n const stateless = await this.createStatelessServer(req);\n server = stateless.server;\n transport = stateless.transport;\n\n // Handle the request\n await transport.handleRequest(req.raw, res.raw, body);\n\n // Clean up when the response closes\n res.on?.('close', () => {\n this.logger.debug('[Stateless] Request closed, cleaning up');\n void transport?.close();\n void server?.close();\n });\n } catch (error) {\n this.logger.error(\n `[Stateless] Error in stateless request handling: ${error}`,\n );\n // Clean up on error\n void transport?.close();\n void server?.close();\n throw error;\n }\n }\n\n /**\n * Handle requests in stateful mode\n */\n async handleStatefulRequest(\n req: HttpRequest,\n res: HttpResponse,\n body: unknown,\n ): Promise<void> {\n this.logger.debug(\n `[Stateful] Handling stateful MCP request at ${req.url} with body: ${JSON.stringify(body)}`,\n );\n\n // Check for existing session ID\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n let transport: StreamableHTTPServerTransport;\n\n if (sessionId && this.transports[sessionId]) {\n // Reuse existing transport\n transport = this.transports[sessionId];\n } else if (!sessionId && this.isInitializeRequest(body)) {\n // Build capabilities and create the MCP server first so the init callback can capture it\n const capabilities = buildMcpCapabilities(\n this.mcpModuleId,\n this.toolRegistry,\n this.options,\n );\n this.logger.debug(\n `[${sessionId || 'New-Session'}] Built MCP capabilities: ${JSON.stringify(capabilities)}`,\n );\n\n const mcpServer = new McpServer(\n { name: this.options.name, version: this.options.version },\n {\n capabilities,\n instructions: this.options.instructions || '',\n },\n );\n\n // Create the transport with proper session handling\n transport = new StreamableHTTPServerTransport({\n sessionIdGenerator:\n this.options.streamableHttp?.sessionIdGenerator ||\n (() => randomUUID()),\n enableJsonResponse:\n this.options.streamableHttp?.enableJsonResponse || false,\n // Single source of truth for session registration to avoid races/duplication\n onsessioninitialized: (sid: string) => {\n this.logger.debug(`[${sid}] Session initialized`);\n this.transports[sid] = transport;\n this.mcpServers[sid] = mcpServer;\n },\n });\n\n // Attach onclose cleanup BEFORE handling any request\n transport.onclose = () => {\n if (transport.sessionId) {\n this.cleanupSession(transport.sessionId);\n }\n };\n\n // Connect the transport to the MCP server BEFORE handling the request\n await mcpServer.connect(transport);\n\n // Register request handlers BEFORE the first handleRequest to avoid races\n const contextId = ContextIdFactory.getByRequest(req);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n executor.registerRequestHandlers(mcpServer, req);\n\n // Handle the initialization request\n await transport.handleRequest(req.raw, res.raw, body);\n\n this.logger.log(`[${transport.sessionId}] Initialized new session`);\n return;\n } else if (sessionId && !this.transports[sessionId]) {\n // Provided session ID but no matching session exists → align with SDK error shape/code\n res.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n } else {\n // Invalid request - no session ID or not initialization request\n // Match SDK wording for missing session id after init\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: Mcp-Session-Id header is required',\n },\n id: null,\n });\n return;\n }\n\n // ---- Subsequent requests to an existing session ----\n const mcpServer = this.mcpServers[sessionId];\n if (!mcpServer) {\n this.logger.debug(`[${sessionId}] Session not found`);\n res.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n // Resolve the request-scoped tool executor service\n const contextId = ContextIdFactory.getByRequest(req);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n\n // Register request handlers with the user context from this specific request\n executor.registerRequestHandlers(mcpServer, req);\n\n this.logger.debug(`[${sessionId}] Handling subsequent request`);\n\n // Handle the request with existing transport\n await transport.handleRequest(req.raw, res.raw, body);\n }\n\n /**\n * Handle GET requests for SSE streams\n */\n async handleGetRequest(req: any, res: any): Promise<void> {\n // Get the appropriate HTTP adapter for the request/response\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n if (this.isStatelessMode) {\n adaptedRes.status(405).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed in stateless mode',\n },\n id: null,\n });\n return;\n }\n\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n if (!sessionId || !this.transports[sessionId]) {\n this.logger.debug(\n `[${sessionId || 'No-Session'}] GET request failed - session not found`,\n );\n adaptedRes\n .status(400)\n .send('Bad Request: Mcp-Session-Id header is required');\n return;\n }\n\n this.logger.debug(`[${sessionId}] Establishing SSE stream`);\n const transport = this.transports[sessionId];\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n }\n\n /**\n * Handle DELETE requests for terminating sessions\n */\n async handleDeleteRequest(req: any, res: any): Promise<void> {\n // Get the appropriate HTTP adapter for the request/response\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n if (this.isStatelessMode) {\n adaptedRes.status(405).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed in stateless mode',\n },\n id: null,\n });\n return;\n }\n\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n if (!sessionId || !this.transports[sessionId]) {\n this.logger.debug(\n `[${sessionId || 'No-Session'}] DELETE request failed - session not found`,\n );\n adaptedRes.status(400).send('Invalid or missing session ID');\n return;\n }\n\n this.logger.debug(`[${sessionId}] Terminating session`);\n const transport = this.transports[sessionId];\n\n // Let the transport handle termination; onclose handler will perform cleanup.\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n // DO NOT call this.cleanupSession(sessionId) here to avoid double cleanup.\n }\n\n // Helper function to detect initialize requests\n isInitializeRequest(body: unknown): boolean {\n if (Array.isArray(body)) {\n return body.some(\n (msg) =>\n typeof msg === 'object' &&\n msg !== null &&\n 'method' in msg &&\n msg.method === 'initialize',\n );\n }\n return (\n typeof body === 'object' &&\n body !== null &&\n 'method' in body &&\n (body as any).method === 'initialize'\n );\n }\n\n // Clean up session resources\n cleanupSession(sessionId: string): void {\n if (sessionId) {\n this.logger.debug(`[${sessionId}] Cleaning up session`);\n delete this.transports[sessionId];\n delete this.mcpServers[sessionId];\n }\n }\n}\n"]}
1
+ {"version":3,"file":"mcp-streamable-http.service.js","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":"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","sourcesContent":["import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';\nimport { ContextIdFactory, ModuleRef } from '@nestjs/core';\nimport { randomUUID } from 'crypto';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { HttpAdapterFactory } from '../adapters/http-adapter.factory';\nimport {\n HttpRequest,\n HttpResponse,\n} from '../interfaces/http-adapter.interface';\nimport { McpOptions } from '../interfaces';\nimport { McpExecutorService } from './mcp-executor.service';\nimport { McpRegistryService } from './mcp-registry.service';\nimport { buildMcpCapabilities } from '../utils/capabilities-builder';\n\n@Injectable()\nexport class McpStreamableHttpService implements OnModuleDestroy {\n private readonly logger = new Logger(McpStreamableHttpService.name);\n\n // Session management for stateful mode\n private readonly transports: {\n [sessionId: string]: StreamableHTTPServerTransport;\n } = {};\n private readonly mcpServers: { [sessionId: string]: McpServer } = {};\n private readonly sessionExecutors: {\n [sessionId: string]: McpExecutorService;\n } = {};\n\n private readonly isStatelessMode: boolean;\n\n constructor(\n @Inject('MCP_OPTIONS') private readonly options: McpOptions,\n @Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,\n private readonly moduleRef: ModuleRef,\n private readonly toolRegistry: McpRegistryService,\n ) {\n this.isStatelessMode = !!options.streamableHttp?.statelessMode;\n }\n\n /**\n * Creates a new MCP server instance with the shared registry\n */\n private createMcpServer(): McpServer {\n const capabilities = buildMcpCapabilities(\n this.mcpModuleId,\n this.toolRegistry,\n this.options,\n );\n\n this.logger.debug('Building MCP server with capabilities:', capabilities);\n\n return new McpServer(\n { name: this.options.name, version: this.options.version },\n {\n capabilities,\n instructions: this.options.instructions || '',\n },\n );\n }\n\n /**\n * Create a new MCP server instance for stateless requests\n */\n async createStatelessServer(rawReq: any): Promise<{\n server: McpServer;\n transport: StreamableHTTPServerTransport;\n executor: McpExecutorService;\n }> {\n // Create transport without session ID for true stateless mode\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse:\n this.options.streamableHttp?.enableJsonResponse || false,\n });\n\n // Create new MCP server instance\n const server = this.createMcpServer();\n\n // Connect transport to server\n await server.connect(transport);\n\n // Resolve request-scoped executor\n const contextId = ContextIdFactory.getByRequest(rawReq);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n\n // Register handlers for this request\n executor.registerRequestHandlers(server, rawReq);\n\n return { server, transport, executor };\n }\n\n /**\n * Handle POST requests\n */\n async handlePostRequest(req: any, res: any, body: unknown): Promise<void> {\n this.logger.debug('Received MCP POST request');\n\n // Get the appropriate HTTP adapter\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n try {\n if (this.isStatelessMode) {\n return this.handleStatelessRequest(adaptedReq, adaptedRes, body);\n } else {\n return this.handleStatefulRequest(adaptedReq, adaptedRes, body);\n }\n } catch (error) {\n this.logger.error('Error handling MCP request:', error);\n if (!adaptedRes.headersSent) {\n adaptedRes.status(500).json({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: 'Internal server error',\n },\n id: null,\n });\n }\n }\n }\n\n /**\n * Handle requests in stateless mode\n */\n async handleStatelessRequest(\n req: HttpRequest,\n res: HttpResponse,\n body: unknown,\n ): Promise<void> {\n this.logger.debug('Handling stateless MCP request');\n\n let server: McpServer | null = null;\n let transport: StreamableHTTPServerTransport | null = null;\n\n try {\n // Create new server and transport for each request\n const stateless = await this.createStatelessServer(req);\n server = stateless.server;\n transport = stateless.transport;\n\n // Handle the request\n await transport.handleRequest(req.raw, res.raw, body);\n\n // Clean up after response is sent\n res.on?.('finish', async () => {\n this.logger.debug('Stateless response finished, cleaning up');\n await this.cleanupStatelessResources(server, transport);\n });\n } catch (error) {\n this.logger.error('Error in stateless request handling:', error);\n await this.cleanupStatelessResources(server, transport);\n throw error;\n }\n }\n\n /**\n * Clean up stateless resources\n */\n private async cleanupStatelessResources(\n server: McpServer | null,\n transport: StreamableHTTPServerTransport | null,\n ): Promise<void> {\n try {\n if (transport) {\n await transport.close();\n }\n if (server) {\n await server.close();\n }\n } catch (error) {\n this.logger.error('Error cleaning up stateless resources:', error);\n }\n }\n\n /**\n * Handle requests in stateful mode\n */\n async handleStatefulRequest(\n req: HttpRequest,\n res: HttpResponse,\n body: unknown,\n ): Promise<void> {\n this.logger.debug('Handling stateful MCP request');\n\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n // Case 1: New initialization request\n if (!sessionId && this.isInitializeRequest(body)) {\n // Validate single initialization request\n if (Array.isArray(body) && body.length > 1) {\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32600,\n message:\n 'Invalid Request: Only one initialization request is allowed',\n },\n id: null,\n });\n return;\n }\n\n // Create new transport with session management\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator:\n this.options.streamableHttp?.sessionIdGenerator ||\n (() => randomUUID()),\n enableJsonResponse:\n this.options.streamableHttp?.enableJsonResponse || false,\n onsessioninitialized: (sessionId: string) => {\n this.logger.log(`Session initialized: ${sessionId}`);\n // Store transport when session is initialized\n this.transports[sessionId] = transport;\n },\n onsessionclosed: async (sessionId: string) => {\n // Called by DELETE requests\n this.logger.log(`Session termination requested: ${sessionId}`);\n await this.cleanupSession(sessionId);\n },\n });\n\n // Create new MCP server for this session\n const mcpServer = this.createMcpServer();\n\n // Connect transport to server BEFORE handling request\n await mcpServer.connect(transport);\n\n // Resolve request-scoped executor\n const contextId = ContextIdFactory.getByRequest(req);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n\n // Register handlers ONCE during initialization\n executor.registerRequestHandlers(mcpServer, req);\n\n // Handle the initialization request\n await transport.handleRequest(req.raw, res.raw, body);\n\n // Store server and executor after successful initialization\n if (transport.sessionId) {\n this.mcpServers[transport.sessionId] = mcpServer;\n this.sessionExecutors[transport.sessionId] = executor;\n this.logger.log(`Session fully initialized: ${transport.sessionId}`);\n }\n\n return;\n }\n\n // Case 2: Existing session with session ID\n if (sessionId) {\n // Validate session exists\n if (!this.transports[sessionId]) {\n res.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n // Reject re-initialization attempts\n if (this.isInitializeRequest(body)) {\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32600,\n message: 'Invalid Request: Server already initialized',\n },\n id: null,\n });\n return;\n }\n\n // Use existing transport\n const transport = this.transports[sessionId];\n\n // Handle the request with existing transport\n await transport.handleRequest(req.raw, res.raw, body);\n\n return;\n }\n\n // Case 3: No session ID and not initialization\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: No valid session ID provided',\n },\n id: null,\n });\n }\n\n /**\n * Handle GET requests for SSE streams\n */\n async handleGetRequest(req: any, res: any): Promise<void> {\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n if (this.isStatelessMode) {\n // Stateless mode doesn't support SSE\n adaptedRes.status(405).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed in stateless mode',\n },\n id: null,\n });\n return;\n }\n\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n if (!sessionId) {\n adaptedRes.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: Mcp-Session-Id header is required',\n },\n id: null,\n });\n return;\n }\n\n if (!this.transports[sessionId]) {\n adaptedRes.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n this.logger.debug(`Establishing SSE stream for session ${sessionId}`);\n const transport = this.transports[sessionId];\n\n // Let transport handle the GET request for SSE\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n }\n\n /**\n * Handle DELETE requests for terminating sessions\n */\n async handleDeleteRequest(req: any, res: any): Promise<void> {\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n if (this.isStatelessMode) {\n adaptedRes.status(405).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed in stateless mode',\n },\n id: null,\n });\n return;\n }\n\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n if (!sessionId) {\n adaptedRes.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: Mcp-Session-Id header is required',\n },\n id: null,\n });\n return;\n }\n\n if (!this.transports[sessionId]) {\n adaptedRes.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n this.logger.debug(`Processing DELETE request for session ${sessionId}`);\n const transport = this.transports[sessionId];\n\n // Let transport handle the DELETE request\n // The onsessionclosed callback will trigger cleanup\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n }\n\n /**\n * Helper to detect initialization requests\n */\n private isInitializeRequest(body: unknown): boolean {\n if (Array.isArray(body)) {\n return body.some(\n (msg) =>\n typeof msg === 'object' &&\n msg !== null &&\n 'method' in msg &&\n msg.method === 'initialize',\n );\n }\n return (\n typeof body === 'object' &&\n body !== null &&\n 'method' in body &&\n body.method === 'initialize'\n );\n }\n\n /**\n * Clean up session resources\n */\n private async cleanupSession(sessionId: string): Promise<void> {\n if (!sessionId) return;\n\n this.logger.debug(`Cleaning up session: ${sessionId}`);\n\n try {\n // Close transport if still open\n const transport = this.transports[sessionId];\n if (transport) {\n await transport.close();\n }\n delete this.transports[sessionId];\n\n // Close MCP server\n const server = this.mcpServers[sessionId];\n if (server) {\n await server.close();\n }\n delete this.mcpServers[sessionId];\n\n // Clean up executor\n delete this.sessionExecutors[sessionId];\n\n this.logger.log(`Session cleanup complete: ${sessionId}`);\n } catch (error) {\n this.logger.error(`Error cleaning up session ${sessionId}:`, error);\n }\n }\n\n /**\n * Clean up all sessions on module destroy\n */\n async onModuleDestroy(): Promise<void> {\n this.logger.log('Cleaning up all MCP sessions...');\n\n const sessionIds = Object.keys(this.transports);\n await Promise.all(\n sessionIds.map((sessionId) => this.cleanupSession(sessionId)),\n );\n\n this.logger.log('All MCP sessions cleaned up');\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rekog/mcp-nest",
3
- "version": "1.8.2-alpha.1",
3
+ "version": "1.8.2-alpha.2",
4
4
  "description": "NestJS module for creating Model Context Protocol (MCP) servers",
5
5
  "main": "dist/index.js",
6
6
  "license": "MIT",
@@ -1,4 +1,4 @@
1
- import { Inject, Injectable, Logger } from '@nestjs/common';
1
+ import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
2
2
  import { ContextIdFactory, ModuleRef } from '@nestjs/core';
3
3
  import { randomUUID } from 'crypto';
4
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
@@ -14,12 +14,18 @@ import { McpRegistryService } from './mcp-registry.service';
14
14
  import { buildMcpCapabilities } from '../utils/capabilities-builder';
15
15
 
16
16
  @Injectable()
17
- export class McpStreamableHttpService {
17
+ export class McpStreamableHttpService implements OnModuleDestroy {
18
18
  private readonly logger = new Logger(McpStreamableHttpService.name);
19
+
20
+ // Session management for stateful mode
19
21
  private readonly transports: {
20
22
  [sessionId: string]: StreamableHTTPServerTransport;
21
23
  } = {};
22
24
  private readonly mcpServers: { [sessionId: string]: McpServer } = {};
25
+ private readonly sessionExecutors: {
26
+ [sessionId: string]: McpExecutorService;
27
+ } = {};
28
+
23
29
  private readonly isStatelessMode: boolean;
24
30
 
25
31
  constructor(
@@ -28,46 +34,52 @@ export class McpStreamableHttpService {
28
34
  private readonly moduleRef: ModuleRef,
29
35
  private readonly toolRegistry: McpRegistryService,
30
36
  ) {
31
- // Determine if we're in stateless mode
32
37
  this.isStatelessMode = !!options.streamableHttp?.statelessMode;
33
38
  }
34
39
 
35
40
  /**
36
- * Create a new MCP server instance for stateless requests
41
+ * Creates a new MCP server instance with the shared registry
37
42
  */
38
- async createStatelessServer(rawReq: any): Promise<{
39
- server: McpServer;
40
- transport: StreamableHTTPServerTransport;
41
- }> {
42
- // Create a new transport for this request (stateless = no session management)
43
- const transport = new StreamableHTTPServerTransport({
44
- sessionIdGenerator: undefined,
45
- enableJsonResponse:
46
- this.options.streamableHttp?.enableJsonResponse || false,
47
- });
48
-
49
- // Create a new MCP server instance with dynamic capabilities
43
+ private createMcpServer(): McpServer {
50
44
  const capabilities = buildMcpCapabilities(
51
45
  this.mcpModuleId,
52
46
  this.toolRegistry,
53
47
  this.options,
54
48
  );
55
- this.logger.debug(
56
- `[Stateless] Built MCP capabilities: ${JSON.stringify(capabilities)}`,
57
- );
58
49
 
59
- const server = new McpServer(
50
+ this.logger.debug('Building MCP server with capabilities:', capabilities);
51
+
52
+ return new McpServer(
60
53
  { name: this.options.name, version: this.options.version },
61
54
  {
62
- capabilities: capabilities,
55
+ capabilities,
63
56
  instructions: this.options.instructions || '',
64
57
  },
65
58
  );
59
+ }
60
+
61
+ /**
62
+ * Create a new MCP server instance for stateless requests
63
+ */
64
+ async createStatelessServer(rawReq: any): Promise<{
65
+ server: McpServer;
66
+ transport: StreamableHTTPServerTransport;
67
+ executor: McpExecutorService;
68
+ }> {
69
+ // Create transport without session ID for true stateless mode
70
+ const transport = new StreamableHTTPServerTransport({
71
+ sessionIdGenerator: undefined,
72
+ enableJsonResponse:
73
+ this.options.streamableHttp?.enableJsonResponse || false,
74
+ });
75
+
76
+ // Create new MCP server instance
77
+ const server = this.createMcpServer();
66
78
 
67
- // Connect the transport to the MCP server first
79
+ // Connect transport to server
68
80
  await server.connect(transport);
69
81
 
70
- // Now resolve the request-scoped tool executor service
82
+ // Resolve request-scoped executor
71
83
  const contextId = ContextIdFactory.getByRequest(rawReq);
72
84
  const executor = await this.moduleRef.resolve(
73
85
  McpExecutorService,
@@ -75,30 +87,22 @@ export class McpStreamableHttpService {
75
87
  { strict: true },
76
88
  );
77
89
 
78
- // Register request handlers after connection
79
- this.logger.debug(
80
- '[Stateless] Registering request handlers for stateless MCP server',
81
- );
90
+ // Register handlers for this request
82
91
  executor.registerRequestHandlers(server, rawReq);
83
92
 
84
- return { server, transport };
93
+ return { server, transport, executor };
85
94
  }
86
95
 
87
96
  /**
88
97
  * Handle POST requests
89
98
  */
90
99
  async handlePostRequest(req: any, res: any, body: unknown): Promise<void> {
91
- // Get the appropriate HTTP adapter for the request/response
100
+ this.logger.debug('Received MCP POST request');
101
+
102
+ // Get the appropriate HTTP adapter
92
103
  const adapter = HttpAdapterFactory.getAdapter(req, res);
93
104
  const adaptedReq = adapter.adaptRequest(req);
94
105
  const adaptedRes = adapter.adaptResponse(res);
95
- const sessionId = adaptedReq.headers['mcp-session-id'] as
96
- | string
97
- | undefined;
98
-
99
- this.logger.debug(
100
- `[${sessionId || 'No-Session'}] Received MCP request: ${JSON.stringify(body)}`,
101
- );
102
106
 
103
107
  try {
104
108
  if (this.isStatelessMode) {
@@ -107,9 +111,7 @@ export class McpStreamableHttpService {
107
111
  return this.handleStatefulRequest(adaptedReq, adaptedRes, body);
108
112
  }
109
113
  } catch (error) {
110
- this.logger.error(
111
- `[${sessionId || 'No-Session'}] Error handling MCP request: ${error}`,
112
- );
114
+ this.logger.error('Error handling MCP request:', error);
113
115
  if (!adaptedRes.headersSent) {
114
116
  adaptedRes.status(500).json({
115
117
  jsonrpc: '2.0',
@@ -127,19 +129,17 @@ export class McpStreamableHttpService {
127
129
  * Handle requests in stateless mode
128
130
  */
129
131
  async handleStatelessRequest(
130
- req: any,
132
+ req: HttpRequest,
131
133
  res: HttpResponse,
132
134
  body: unknown,
133
135
  ): Promise<void> {
134
- this.logger.debug(
135
- `[Stateless] Handling stateless MCP request at ${req.url} with body: ${JSON.stringify(body)}`,
136
- );
136
+ this.logger.debug('Handling stateless MCP request');
137
137
 
138
138
  let server: McpServer | null = null;
139
139
  let transport: StreamableHTTPServerTransport | null = null;
140
140
 
141
141
  try {
142
- // Create a new server and transport for each request
142
+ // Create new server and transport for each request
143
143
  const stateless = await this.createStatelessServer(req);
144
144
  server = stateless.server;
145
145
  transport = stateless.transport;
@@ -147,23 +147,37 @@ export class McpStreamableHttpService {
147
147
  // Handle the request
148
148
  await transport.handleRequest(req.raw, res.raw, body);
149
149
 
150
- // Clean up when the response closes
151
- res.on?.('close', () => {
152
- this.logger.debug('[Stateless] Request closed, cleaning up');
153
- void transport?.close();
154
- void server?.close();
150
+ // Clean up after response is sent
151
+ res.on?.('finish', async () => {
152
+ this.logger.debug('Stateless response finished, cleaning up');
153
+ await this.cleanupStatelessResources(server, transport);
155
154
  });
156
155
  } catch (error) {
157
- this.logger.error(
158
- `[Stateless] Error in stateless request handling: ${error}`,
159
- );
160
- // Clean up on error
161
- void transport?.close();
162
- void server?.close();
156
+ this.logger.error('Error in stateless request handling:', error);
157
+ await this.cleanupStatelessResources(server, transport);
163
158
  throw error;
164
159
  }
165
160
  }
166
161
 
162
+ /**
163
+ * Clean up stateless resources
164
+ */
165
+ private async cleanupStatelessResources(
166
+ server: McpServer | null,
167
+ transport: StreamableHTTPServerTransport | null,
168
+ ): Promise<void> {
169
+ try {
170
+ if (transport) {
171
+ await transport.close();
172
+ }
173
+ if (server) {
174
+ await server.close();
175
+ }
176
+ } catch (error) {
177
+ this.logger.error('Error cleaning up stateless resources:', error);
178
+ }
179
+ }
180
+
167
181
  /**
168
182
  * Handle requests in stateful mode
169
183
  */
@@ -172,142 +186,133 @@ export class McpStreamableHttpService {
172
186
  res: HttpResponse,
173
187
  body: unknown,
174
188
  ): Promise<void> {
175
- this.logger.debug(
176
- `[Stateful] Handling stateful MCP request at ${req.url} with body: ${JSON.stringify(body)}`,
177
- );
189
+ this.logger.debug('Handling stateful MCP request');
178
190
 
179
- // Check for existing session ID
180
191
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
181
- let transport: StreamableHTTPServerTransport;
182
-
183
- if (sessionId && this.transports[sessionId]) {
184
- // Reuse existing transport
185
- transport = this.transports[sessionId];
186
- } else if (!sessionId && this.isInitializeRequest(body)) {
187
- // Build capabilities and create the MCP server first so the init callback can capture it
188
- const capabilities = buildMcpCapabilities(
189
- this.mcpModuleId,
190
- this.toolRegistry,
191
- this.options,
192
- );
193
- this.logger.debug(
194
- `[${sessionId || 'New-Session'}] Built MCP capabilities: ${JSON.stringify(capabilities)}`,
195
- );
196
192
 
197
- const mcpServer = new McpServer(
198
- { name: this.options.name, version: this.options.version },
199
- {
200
- capabilities,
201
- instructions: this.options.instructions || '',
202
- },
203
- );
193
+ // Case 1: New initialization request
194
+ if (!sessionId && this.isInitializeRequest(body)) {
195
+ // Validate single initialization request
196
+ if (Array.isArray(body) && body.length > 1) {
197
+ res.status(400).json({
198
+ jsonrpc: '2.0',
199
+ error: {
200
+ code: -32600,
201
+ message:
202
+ 'Invalid Request: Only one initialization request is allowed',
203
+ },
204
+ id: null,
205
+ });
206
+ return;
207
+ }
204
208
 
205
- // Create the transport with proper session handling
206
- transport = new StreamableHTTPServerTransport({
209
+ // Create new transport with session management
210
+ const transport = new StreamableHTTPServerTransport({
207
211
  sessionIdGenerator:
208
212
  this.options.streamableHttp?.sessionIdGenerator ||
209
213
  (() => randomUUID()),
210
214
  enableJsonResponse:
211
215
  this.options.streamableHttp?.enableJsonResponse || false,
212
- // Single source of truth for session registration to avoid races/duplication
213
- onsessioninitialized: (sid: string) => {
214
- this.logger.debug(`[${sid}] Session initialized`);
215
- this.transports[sid] = transport;
216
- this.mcpServers[sid] = mcpServer;
216
+ onsessioninitialized: (sessionId: string) => {
217
+ this.logger.log(`Session initialized: ${sessionId}`);
218
+ // Store transport when session is initialized
219
+ this.transports[sessionId] = transport;
220
+ },
221
+ onsessionclosed: async (sessionId: string) => {
222
+ // Called by DELETE requests
223
+ this.logger.log(`Session termination requested: ${sessionId}`);
224
+ await this.cleanupSession(sessionId);
217
225
  },
218
226
  });
219
227
 
220
- // Attach onclose cleanup BEFORE handling any request
221
- transport.onclose = () => {
222
- if (transport.sessionId) {
223
- this.cleanupSession(transport.sessionId);
224
- }
225
- };
228
+ // Create new MCP server for this session
229
+ const mcpServer = this.createMcpServer();
226
230
 
227
- // Connect the transport to the MCP server BEFORE handling the request
231
+ // Connect transport to server BEFORE handling request
228
232
  await mcpServer.connect(transport);
229
233
 
230
- // Register request handlers BEFORE the first handleRequest to avoid races
234
+ // Resolve request-scoped executor
231
235
  const contextId = ContextIdFactory.getByRequest(req);
232
236
  const executor = await this.moduleRef.resolve(
233
237
  McpExecutorService,
234
238
  contextId,
235
239
  { strict: true },
236
240
  );
241
+
242
+ // Register handlers ONCE during initialization
237
243
  executor.registerRequestHandlers(mcpServer, req);
238
244
 
239
245
  // Handle the initialization request
240
246
  await transport.handleRequest(req.raw, res.raw, body);
241
247
 
242
- this.logger.log(`[${transport.sessionId}] Initialized new session`);
243
- return;
244
- } else if (sessionId && !this.transports[sessionId]) {
245
- // Provided session ID but no matching session exists → align with SDK error shape/code
246
- res.status(404).json({
247
- jsonrpc: '2.0',
248
- error: {
249
- code: -32001,
250
- message: 'Session not found',
251
- },
252
- id: null,
253
- });
254
- return;
255
- } else {
256
- // Invalid request - no session ID or not initialization request
257
- // Match SDK wording for missing session id after init
258
- res.status(400).json({
259
- jsonrpc: '2.0',
260
- error: {
261
- code: -32000,
262
- message: 'Bad Request: Mcp-Session-Id header is required',
263
- },
264
- id: null,
265
- });
266
- return;
267
- }
248
+ // Store server and executor after successful initialization
249
+ if (transport.sessionId) {
250
+ this.mcpServers[transport.sessionId] = mcpServer;
251
+ this.sessionExecutors[transport.sessionId] = executor;
252
+ this.logger.log(`Session fully initialized: ${transport.sessionId}`);
253
+ }
268
254
 
269
- // ---- Subsequent requests to an existing session ----
270
- const mcpServer = this.mcpServers[sessionId];
271
- if (!mcpServer) {
272
- this.logger.debug(`[${sessionId}] Session not found`);
273
- res.status(404).json({
274
- jsonrpc: '2.0',
275
- error: {
276
- code: -32001,
277
- message: 'Session not found',
278
- },
279
- id: null,
280
- });
281
255
  return;
282
256
  }
283
257
 
284
- // Resolve the request-scoped tool executor service
285
- const contextId = ContextIdFactory.getByRequest(req);
286
- const executor = await this.moduleRef.resolve(
287
- McpExecutorService,
288
- contextId,
289
- { strict: true },
290
- );
258
+ // Case 2: Existing session with session ID
259
+ if (sessionId) {
260
+ // Validate session exists
261
+ if (!this.transports[sessionId]) {
262
+ res.status(404).json({
263
+ jsonrpc: '2.0',
264
+ error: {
265
+ code: -32001,
266
+ message: 'Session not found',
267
+ },
268
+ id: null,
269
+ });
270
+ return;
271
+ }
272
+
273
+ // Reject re-initialization attempts
274
+ if (this.isInitializeRequest(body)) {
275
+ res.status(400).json({
276
+ jsonrpc: '2.0',
277
+ error: {
278
+ code: -32600,
279
+ message: 'Invalid Request: Server already initialized',
280
+ },
281
+ id: null,
282
+ });
283
+ return;
284
+ }
285
+
286
+ // Use existing transport
287
+ const transport = this.transports[sessionId];
291
288
 
292
- // Register request handlers with the user context from this specific request
293
- executor.registerRequestHandlers(mcpServer, req);
289
+ // Handle the request with existing transport
290
+ await transport.handleRequest(req.raw, res.raw, body);
294
291
 
295
- this.logger.debug(`[${sessionId}] Handling subsequent request`);
292
+ return;
293
+ }
296
294
 
297
- // Handle the request with existing transport
298
- await transport.handleRequest(req.raw, res.raw, body);
295
+ // Case 3: No session ID and not initialization
296
+ res.status(400).json({
297
+ jsonrpc: '2.0',
298
+ error: {
299
+ code: -32000,
300
+ message: 'Bad Request: No valid session ID provided',
301
+ },
302
+ id: null,
303
+ });
299
304
  }
300
305
 
301
306
  /**
302
307
  * Handle GET requests for SSE streams
303
308
  */
304
309
  async handleGetRequest(req: any, res: any): Promise<void> {
305
- // Get the appropriate HTTP adapter for the request/response
306
310
  const adapter = HttpAdapterFactory.getAdapter(req, res);
307
311
  const adaptedReq = adapter.adaptRequest(req);
308
312
  const adaptedRes = adapter.adaptResponse(res);
309
313
 
310
314
  if (this.isStatelessMode) {
315
+ // Stateless mode doesn't support SSE
311
316
  adaptedRes.status(405).json({
312
317
  jsonrpc: '2.0',
313
318
  error: {
@@ -323,18 +328,34 @@ export class McpStreamableHttpService {
323
328
  | string
324
329
  | undefined;
325
330
 
326
- if (!sessionId || !this.transports[sessionId]) {
327
- this.logger.debug(
328
- `[${sessionId || 'No-Session'}] GET request failed - session not found`,
329
- );
330
- adaptedRes
331
- .status(400)
332
- .send('Bad Request: Mcp-Session-Id header is required');
331
+ if (!sessionId) {
332
+ adaptedRes.status(400).json({
333
+ jsonrpc: '2.0',
334
+ error: {
335
+ code: -32000,
336
+ message: 'Bad Request: Mcp-Session-Id header is required',
337
+ },
338
+ id: null,
339
+ });
333
340
  return;
334
341
  }
335
342
 
336
- this.logger.debug(`[${sessionId}] Establishing SSE stream`);
343
+ if (!this.transports[sessionId]) {
344
+ adaptedRes.status(404).json({
345
+ jsonrpc: '2.0',
346
+ error: {
347
+ code: -32001,
348
+ message: 'Session not found',
349
+ },
350
+ id: null,
351
+ });
352
+ return;
353
+ }
354
+
355
+ this.logger.debug(`Establishing SSE stream for session ${sessionId}`);
337
356
  const transport = this.transports[sessionId];
357
+
358
+ // Let transport handle the GET request for SSE
338
359
  await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
339
360
  }
340
361
 
@@ -342,7 +363,6 @@ export class McpStreamableHttpService {
342
363
  * Handle DELETE requests for terminating sessions
343
364
  */
344
365
  async handleDeleteRequest(req: any, res: any): Promise<void> {
345
- // Get the appropriate HTTP adapter for the request/response
346
366
  const adapter = HttpAdapterFactory.getAdapter(req, res);
347
367
  const adaptedReq = adapter.adaptRequest(req);
348
368
  const adaptedRes = adapter.adaptResponse(res);
@@ -363,24 +383,42 @@ export class McpStreamableHttpService {
363
383
  | string
364
384
  | undefined;
365
385
 
366
- if (!sessionId || !this.transports[sessionId]) {
367
- this.logger.debug(
368
- `[${sessionId || 'No-Session'}] DELETE request failed - session not found`,
369
- );
370
- adaptedRes.status(400).send('Invalid or missing session ID');
386
+ if (!sessionId) {
387
+ adaptedRes.status(400).json({
388
+ jsonrpc: '2.0',
389
+ error: {
390
+ code: -32000,
391
+ message: 'Bad Request: Mcp-Session-Id header is required',
392
+ },
393
+ id: null,
394
+ });
395
+ return;
396
+ }
397
+
398
+ if (!this.transports[sessionId]) {
399
+ adaptedRes.status(404).json({
400
+ jsonrpc: '2.0',
401
+ error: {
402
+ code: -32001,
403
+ message: 'Session not found',
404
+ },
405
+ id: null,
406
+ });
371
407
  return;
372
408
  }
373
409
 
374
- this.logger.debug(`[${sessionId}] Terminating session`);
410
+ this.logger.debug(`Processing DELETE request for session ${sessionId}`);
375
411
  const transport = this.transports[sessionId];
376
412
 
377
- // Let the transport handle termination; onclose handler will perform cleanup.
413
+ // Let transport handle the DELETE request
414
+ // The onsessionclosed callback will trigger cleanup
378
415
  await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
379
- // DO NOT call this.cleanupSession(sessionId) here to avoid double cleanup.
380
416
  }
381
417
 
382
- // Helper function to detect initialize requests
383
- isInitializeRequest(body: unknown): boolean {
418
+ /**
419
+ * Helper to detect initialization requests
420
+ */
421
+ private isInitializeRequest(body: unknown): boolean {
384
422
  if (Array.isArray(body)) {
385
423
  return body.some(
386
424
  (msg) =>
@@ -394,16 +432,53 @@ export class McpStreamableHttpService {
394
432
  typeof body === 'object' &&
395
433
  body !== null &&
396
434
  'method' in body &&
397
- (body as any).method === 'initialize'
435
+ body.method === 'initialize'
398
436
  );
399
437
  }
400
438
 
401
- // Clean up session resources
402
- cleanupSession(sessionId: string): void {
403
- if (sessionId) {
404
- this.logger.debug(`[${sessionId}] Cleaning up session`);
439
+ /**
440
+ * Clean up session resources
441
+ */
442
+ private async cleanupSession(sessionId: string): Promise<void> {
443
+ if (!sessionId) return;
444
+
445
+ this.logger.debug(`Cleaning up session: ${sessionId}`);
446
+
447
+ try {
448
+ // Close transport if still open
449
+ const transport = this.transports[sessionId];
450
+ if (transport) {
451
+ await transport.close();
452
+ }
405
453
  delete this.transports[sessionId];
454
+
455
+ // Close MCP server
456
+ const server = this.mcpServers[sessionId];
457
+ if (server) {
458
+ await server.close();
459
+ }
406
460
  delete this.mcpServers[sessionId];
461
+
462
+ // Clean up executor
463
+ delete this.sessionExecutors[sessionId];
464
+
465
+ this.logger.log(`Session cleanup complete: ${sessionId}`);
466
+ } catch (error) {
467
+ this.logger.error(`Error cleaning up session ${sessionId}:`, error);
407
468
  }
408
469
  }
470
+
471
+ /**
472
+ * Clean up all sessions on module destroy
473
+ */
474
+ async onModuleDestroy(): Promise<void> {
475
+ this.logger.log('Cleaning up all MCP sessions...');
476
+
477
+ const sessionIds = Object.keys(this.transports);
478
+ await Promise.all(
479
+ sessionIds.map((sessionId) => this.cleanupSession(sessionId)),
480
+ );
481
+
482
+ this.logger.log('All MCP sessions cleaned up');
483
+ }
409
484
  }