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

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,11 @@
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';
6
7
  import { McpRegistryService } from './mcp-registry.service';
7
- export declare class McpStreamableHttpService {
8
+ export declare class McpStreamableHttpService implements OnModuleDestroy {
8
9
  private readonly options;
9
10
  private readonly mcpModuleId;
10
11
  private readonly moduleRef;
@@ -12,6 +13,7 @@ export declare class McpStreamableHttpService {
12
13
  private readonly logger;
13
14
  private readonly transports;
14
15
  private readonly mcpServers;
16
+ private readonly executors;
15
17
  private readonly isStatelessMode;
16
18
  constructor(options: McpOptions, mcpModuleId: string, moduleRef: ModuleRef, toolRegistry: McpRegistryService);
17
19
  createStatelessServer(rawReq: any): Promise<{
@@ -23,7 +25,8 @@ export declare class McpStreamableHttpService {
23
25
  handleStatefulRequest(req: HttpRequest, res: HttpResponse, body: unknown): Promise<void>;
24
26
  handleGetRequest(req: any, res: any): Promise<void>;
25
27
  handleDeleteRequest(req: any, res: any): Promise<void>;
26
- isInitializeRequest(body: unknown): boolean;
27
- cleanupSession(sessionId: string): void;
28
+ private isInitializeRequest;
29
+ private cleanupSession;
30
+ onModuleDestroy(): Promise<void>;
28
31
  }
29
32
  //# 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;AAE3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,qBACa,wBAAyB,YAAW,eAAe;IAUrC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAZ/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,SAAS,CAAmD;IAC7E,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;IA6CV,qBAAqB,CACzB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;IAqIV,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAsDnD,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAyD5D,OAAO,CAAC,mBAAmB;YAqBb,cAAc;IAgCtB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAUvC"}
@@ -32,6 +32,7 @@ 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.executors = {};
35
36
  this.isStatelessMode = !!options.streamableHttp?.statelessMode;
36
37
  }
37
38
  async createStatelessServer(rawReq) {
@@ -81,7 +82,7 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
81
82
  }
82
83
  }
83
84
  async handleStatelessRequest(req, res, body) {
84
- this.logger.debug(`[Stateless] Handling stateless MCP request at ${req.url} with body: ${JSON.stringify(body)}`);
85
+ this.logger.debug(`[Stateless] Handling stateless MCP request at ${req.url}`);
85
86
  let server = null;
86
87
  let transport = null;
87
88
  try {
@@ -89,69 +90,132 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
89
90
  server = stateless.server;
90
91
  transport = stateless.transport;
91
92
  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();
93
+ res.raw.on('finish', async () => {
94
+ this.logger.debug('[Stateless] Response sent, cleaning up');
95
+ try {
96
+ if (transport)
97
+ await transport.close();
98
+ if (server)
99
+ await server.close();
100
+ }
101
+ catch (error) {
102
+ this.logger.error('[Stateless] Error cleaning up:', error);
103
+ }
96
104
  });
97
105
  }
98
106
  catch (error) {
99
107
  this.logger.error(`[Stateless] Error in stateless request handling: ${error}`);
100
- void transport?.close();
101
- void server?.close();
108
+ try {
109
+ if (transport)
110
+ await transport.close();
111
+ if (server)
112
+ await server.close();
113
+ }
114
+ catch (error) {
115
+ this.logger.error('[Stateless] Error cleaning up on error:', error);
116
+ }
102
117
  throw error;
103
118
  }
104
119
  }
105
120
  async handleStatefulRequest(req, res, body) {
106
- this.logger.debug(`[Stateful] Handling stateful MCP request at ${req.url} with body: ${JSON.stringify(body)}`);
107
121
  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)) {
122
+ this.logger.debug(`[${sessionId || 'New'}] Handling stateful MCP request`);
123
+ if (!sessionId && this.isInitializeRequest(body)) {
124
+ if (Array.isArray(body) && body.length > 1) {
125
+ res.status(400).json({
126
+ jsonrpc: '2.0',
127
+ error: {
128
+ code: -32600,
129
+ message: 'Invalid Request: Only one initialization request is allowed',
130
+ },
131
+ id: null,
132
+ });
133
+ return;
134
+ }
113
135
  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
136
  const mcpServer = new mcp_js_1.McpServer({ name: this.options.name, version: this.options.version }, {
116
137
  capabilities,
117
138
  instructions: this.options.instructions || '',
118
139
  });
119
- transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
140
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
120
141
  sessionIdGenerator: this.options.streamableHttp?.sessionIdGenerator ||
121
142
  (() => (0, crypto_1.randomUUID)()),
122
143
  enableJsonResponse: this.options.streamableHttp?.enableJsonResponse || false,
123
- onsessioninitialized: (sid) => {
124
- this.logger.debug(`[${sid}] Session initialized`);
144
+ onsessioninitialized: async (sid) => {
145
+ this.logger.debug(`[${sid}] Session initialized, storing references`);
125
146
  this.transports[sid] = transport;
126
147
  this.mcpServers[sid] = mcpServer;
148
+ const contextId = core_1.ContextIdFactory.getByRequest(req);
149
+ const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
150
+ this.executors[sid] = executor;
151
+ executor.registerRequestHandlers(mcpServer, req);
152
+ },
153
+ onsessionclosed: async (sid) => {
154
+ this.logger.debug(`[${sid}] Session closed via DELETE`);
155
+ await this.cleanupSession(sid);
127
156
  },
128
157
  });
129
- transport.onclose = () => {
130
- if (transport.sessionId) {
131
- this.cleanupSession(transport.sessionId);
132
- }
133
- };
134
158
  await mcpServer.connect(transport);
135
- const contextId = core_1.ContextIdFactory.getByRequest(req);
136
- const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
137
- executor.registerRequestHandlers(mcpServer, req);
138
159
  await transport.handleRequest(req.raw, res.raw, body);
139
- this.logger.log(`[${transport.sessionId}] Initialized new session`);
160
+ this.logger.log(`[${transport.sessionId}] New session initialized`);
140
161
  return;
141
162
  }
142
- else if (sessionId && !this.transports[sessionId]) {
143
- res.status(404).json({
163
+ if (sessionId) {
164
+ if (!this.transports[sessionId]) {
165
+ this.logger.debug(`[${sessionId}] Session not found`);
166
+ res.status(404).json({
167
+ jsonrpc: '2.0',
168
+ error: {
169
+ code: -32001,
170
+ message: 'Session not found',
171
+ },
172
+ id: null,
173
+ });
174
+ return;
175
+ }
176
+ if (this.isInitializeRequest(body)) {
177
+ res.status(400).json({
178
+ jsonrpc: '2.0',
179
+ error: {
180
+ code: -32600,
181
+ message: 'Invalid Request: Server already initialized',
182
+ },
183
+ id: null,
184
+ });
185
+ return;
186
+ }
187
+ const transport = this.transports[sessionId];
188
+ this.logger.debug(`[${sessionId}] Handling request with existing session`);
189
+ await transport.handleRequest(req.raw, res.raw, body);
190
+ return;
191
+ }
192
+ res.status(400).json({
193
+ jsonrpc: '2.0',
194
+ error: {
195
+ code: -32000,
196
+ message: 'Bad Request: Mcp-Session-Id header is required',
197
+ },
198
+ id: null,
199
+ });
200
+ }
201
+ async handleGetRequest(req, res) {
202
+ const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
203
+ const adaptedReq = adapter.adaptRequest(req);
204
+ const adaptedRes = adapter.adaptResponse(res);
205
+ if (this.isStatelessMode) {
206
+ adaptedRes.status(405).json({
144
207
  jsonrpc: '2.0',
145
208
  error: {
146
- code: -32001,
147
- message: 'Session not found',
209
+ code: -32000,
210
+ message: 'Method not allowed in stateless mode',
148
211
  },
149
212
  id: null,
150
213
  });
151
214
  return;
152
215
  }
153
- else {
154
- res.status(400).json({
216
+ const sessionId = adaptedReq.headers['mcp-session-id'];
217
+ if (!sessionId) {
218
+ adaptedRes.status(400).json({
155
219
  jsonrpc: '2.0',
156
220
  error: {
157
221
  code: -32000,
@@ -161,10 +225,9 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
161
225
  });
162
226
  return;
163
227
  }
164
- const mcpServer = this.mcpServers[sessionId];
165
- if (!mcpServer) {
166
- this.logger.debug(`[${sessionId}] Session not found`);
167
- res.status(404).json({
228
+ if (!this.transports[sessionId]) {
229
+ this.logger.debug(`[${sessionId}] GET request - session not found`);
230
+ adaptedRes.status(404).json({
168
231
  jsonrpc: '2.0',
169
232
  error: {
170
233
  code: -32001,
@@ -174,13 +237,11 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
174
237
  });
175
238
  return;
176
239
  }
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);
240
+ this.logger.debug(`[${sessionId}] Establishing SSE stream`);
241
+ const transport = this.transports[sessionId];
242
+ await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
182
243
  }
183
- async handleGetRequest(req, res) {
244
+ async handleDeleteRequest(req, res) {
184
245
  const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
185
246
  const adaptedReq = adapter.adaptRequest(req);
186
247
  const adaptedRes = adapter.adaptResponse(res);
@@ -196,39 +257,30 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
196
257
  return;
197
258
  }
198
259
  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({
260
+ if (!sessionId) {
261
+ adaptedRes.status(400).json({
216
262
  jsonrpc: '2.0',
217
263
  error: {
218
264
  code: -32000,
219
- message: 'Method not allowed in stateless mode',
265
+ message: 'Bad Request: Mcp-Session-Id header is required',
220
266
  },
221
267
  id: null,
222
268
  });
223
269
  return;
224
270
  }
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');
271
+ if (!this.transports[sessionId]) {
272
+ this.logger.debug(`[${sessionId}] DELETE request - session not found`);
273
+ adaptedRes.status(404).json({
274
+ jsonrpc: '2.0',
275
+ error: {
276
+ code: -32001,
277
+ message: 'Session not found',
278
+ },
279
+ id: null,
280
+ });
229
281
  return;
230
282
  }
231
- this.logger.debug(`[${sessionId}] Terminating session`);
283
+ this.logger.debug(`[${sessionId}] Processing DELETE request`);
232
284
  const transport = this.transports[sessionId];
233
285
  await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
234
286
  }
@@ -244,12 +296,33 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
244
296
  'method' in body &&
245
297
  body.method === 'initialize');
246
298
  }
247
- cleanupSession(sessionId) {
248
- if (sessionId) {
249
- this.logger.debug(`[${sessionId}] Cleaning up session`);
299
+ async cleanupSession(sessionId) {
300
+ if (!sessionId || !this.transports[sessionId]) {
301
+ return;
302
+ }
303
+ this.logger.debug(`[${sessionId}] Cleaning up session`);
304
+ try {
305
+ const transport = this.transports[sessionId];
306
+ if (transport) {
307
+ await transport.close();
308
+ }
309
+ const server = this.mcpServers[sessionId];
310
+ if (server) {
311
+ await server.close();
312
+ }
250
313
  delete this.transports[sessionId];
251
314
  delete this.mcpServers[sessionId];
315
+ delete this.executors[sessionId];
252
316
  }
317
+ catch (error) {
318
+ this.logger.error(`[${sessionId}] Error during cleanup:`, error);
319
+ }
320
+ }
321
+ async onModuleDestroy() {
322
+ this.logger.log('Cleaning up all MCP sessions...');
323
+ const sessionIds = Object.keys(this.transports);
324
+ await Promise.all(sessionIds.map((sessionId) => this.cleanupSession(sessionId)));
325
+ this.logger.log(`Cleaned up ${sessionIds.length} MCP sessions`);
253
326
  }
254
327
  };
255
328
  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 private readonly transports: {\n [sessionId: string]: StreamableHTTPServerTransport;\n } = {};\n private readonly mcpServers: { [sessionId: string]: McpServer } = {};\n private readonly executors: { [sessionId: string]: McpExecutorService } = {};\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}`,\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 after response is sent\n res.raw.on('finish', async () => {\n this.logger.debug('[Stateless] Response sent, cleaning up');\n try {\n if (transport) await transport.close();\n if (server) await server.close();\n } catch (error) {\n this.logger.error('[Stateless] Error cleaning up:', error);\n }\n });\n } catch (error) {\n this.logger.error(\n `[Stateless] Error in stateless request handling: ${error}`,\n );\n // Clean up on error\n try {\n if (transport) await transport.close();\n if (server) await server.close();\n } catch (error) {\n this.logger.error('[Stateless] Error cleaning up on error:', error);\n }\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 const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n this.logger.debug(`[${sessionId || 'New'}] Handling stateful MCP request`);\n\n // Case 1: New initialization request\n if (!sessionId && this.isInitializeRequest(body)) {\n // Validate it's not a batch with multiple requests\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 // Build capabilities\n const capabilities = buildMcpCapabilities(\n this.mcpModuleId,\n this.toolRegistry,\n this.options,\n );\n\n // Create MCP server\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 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: async (sid: string) => {\n this.logger.debug(`[${sid}] Session initialized, storing references`);\n // Store all session data\n this.transports[sid] = transport;\n this.mcpServers[sid] = mcpServer;\n\n // Resolve and store the executor for this session\n const contextId = ContextIdFactory.getByRequest(req);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n this.executors[sid] = executor;\n\n // Register request handlers ONCE during initialization\n executor.registerRequestHandlers(mcpServer, req);\n },\n onsessionclosed: async (sid: string) => {\n this.logger.debug(`[${sid}] Session closed via DELETE`);\n await this.cleanupSession(sid);\n },\n });\n\n // Connect transport to server\n await mcpServer.connect(transport);\n\n // Handle the initialization request\n await transport.handleRequest(req.raw, res.raw, body);\n\n this.logger.log(`[${transport.sessionId}] New session initialized`);\n return;\n }\n\n // Case 2: Request with session ID\n if (sessionId) {\n // Check if session exists\n if (!this.transports[sessionId]) {\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 // 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 - DO NOT re-register handlers\n const transport = this.transports[sessionId];\n\n this.logger.debug(\n `[${sessionId}] Handling request with existing session`,\n );\n\n // Handle the request with existing transport and handlers\n await transport.handleRequest(req.raw, res.raw, body);\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: Mcp-Session-Id header is required',\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 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 this.logger.debug(`[${sessionId}] GET request - session not found`);\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(`[${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 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 this.logger.debug(`[${sessionId}] DELETE request - session not found`);\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(`[${sessionId}] Processing DELETE request`);\n const transport = this.transports[sessionId];\n\n // Let transport handle the DELETE request\n // The onsessionclosed callback will handle cleanup\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n }\n\n /**\n * Helper function to detect initialize 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 as any).method === 'initialize'\n );\n }\n\n /**\n * Clean up session resources\n */\n private async cleanupSession(sessionId: string): Promise<void> {\n if (!sessionId || !this.transports[sessionId]) {\n return;\n }\n\n this.logger.debug(`[${sessionId}] Cleaning up session`);\n\n try {\n // Close transport if still open\n const transport = this.transports[sessionId];\n if (transport) {\n await transport.close();\n }\n\n // Close MCP server\n const server = this.mcpServers[sessionId];\n if (server) {\n await server.close();\n }\n\n // Clean up all references\n delete this.transports[sessionId];\n delete this.mcpServers[sessionId];\n delete this.executors[sessionId];\n } catch (error) {\n this.logger.error(`[${sessionId}] Error during cleanup:`, 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 const sessionIds = Object.keys(this.transports);\n\n await Promise.all(\n sessionIds.map((sessionId) => this.cleanupSession(sessionId)),\n );\n\n this.logger.log(`Cleaned up ${sessionIds.length} MCP sessions`);\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.3",
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,13 @@ 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
19
  private readonly transports: {
20
20
  [sessionId: string]: StreamableHTTPServerTransport;
21
21
  } = {};
22
22
  private readonly mcpServers: { [sessionId: string]: McpServer } = {};
23
+ private readonly executors: { [sessionId: string]: McpExecutorService } = {};
23
24
  private readonly isStatelessMode: boolean;
24
25
 
25
26
  constructor(
@@ -132,7 +133,7 @@ export class McpStreamableHttpService {
132
133
  body: unknown,
133
134
  ): Promise<void> {
134
135
  this.logger.debug(
135
- `[Stateless] Handling stateless MCP request at ${req.url} with body: ${JSON.stringify(body)}`,
136
+ `[Stateless] Handling stateless MCP request at ${req.url}`,
136
137
  );
137
138
 
138
139
  let server: McpServer | null = null;
@@ -147,19 +148,27 @@ export class McpStreamableHttpService {
147
148
  // Handle the request
148
149
  await transport.handleRequest(req.raw, res.raw, body);
149
150
 
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();
151
+ // Clean up after response is sent
152
+ res.raw.on('finish', async () => {
153
+ this.logger.debug('[Stateless] Response sent, cleaning up');
154
+ try {
155
+ if (transport) await transport.close();
156
+ if (server) await server.close();
157
+ } catch (error) {
158
+ this.logger.error('[Stateless] Error cleaning up:', error);
159
+ }
155
160
  });
156
161
  } catch (error) {
157
162
  this.logger.error(
158
163
  `[Stateless] Error in stateless request handling: ${error}`,
159
164
  );
160
165
  // Clean up on error
161
- void transport?.close();
162
- void server?.close();
166
+ try {
167
+ if (transport) await transport.close();
168
+ if (server) await server.close();
169
+ } catch (error) {
170
+ this.logger.error('[Stateless] Error cleaning up on error:', error);
171
+ }
163
172
  throw error;
164
173
  }
165
174
  }
@@ -172,28 +181,34 @@ export class McpStreamableHttpService {
172
181
  res: HttpResponse,
173
182
  body: unknown,
174
183
  ): Promise<void> {
175
- this.logger.debug(
176
- `[Stateful] Handling stateful MCP request at ${req.url} with body: ${JSON.stringify(body)}`,
177
- );
178
-
179
- // Check for existing session ID
180
184
  const sessionId = req.headers['mcp-session-id'] as string | undefined;
181
- let transport: StreamableHTTPServerTransport;
182
185
 
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
186
+ this.logger.debug(`[${sessionId || 'New'}] Handling stateful MCP request`);
187
+
188
+ // Case 1: New initialization request
189
+ if (!sessionId && this.isInitializeRequest(body)) {
190
+ // Validate it's not a batch with multiple requests
191
+ if (Array.isArray(body) && body.length > 1) {
192
+ res.status(400).json({
193
+ jsonrpc: '2.0',
194
+ error: {
195
+ code: -32600,
196
+ message:
197
+ 'Invalid Request: Only one initialization request is allowed',
198
+ },
199
+ id: null,
200
+ });
201
+ return;
202
+ }
203
+
204
+ // Build capabilities
188
205
  const capabilities = buildMcpCapabilities(
189
206
  this.mcpModuleId,
190
207
  this.toolRegistry,
191
208
  this.options,
192
209
  );
193
- this.logger.debug(
194
- `[${sessionId || 'New-Session'}] Built MCP capabilities: ${JSON.stringify(capabilities)}`,
195
- );
196
210
 
211
+ // Create MCP server
197
212
  const mcpServer = new McpServer(
198
213
  { name: this.options.name, version: this.options.version },
199
214
  {
@@ -202,107 +217,103 @@ export class McpStreamableHttpService {
202
217
  },
203
218
  );
204
219
 
205
- // Create the transport with proper session handling
206
- transport = new StreamableHTTPServerTransport({
220
+ // Create transport with session management
221
+ const transport = new StreamableHTTPServerTransport({
207
222
  sessionIdGenerator:
208
223
  this.options.streamableHttp?.sessionIdGenerator ||
209
224
  (() => randomUUID()),
210
225
  enableJsonResponse:
211
226
  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`);
227
+ onsessioninitialized: async (sid: string) => {
228
+ this.logger.debug(`[${sid}] Session initialized, storing references`);
229
+ // Store all session data
215
230
  this.transports[sid] = transport;
216
231
  this.mcpServers[sid] = mcpServer;
232
+
233
+ // Resolve and store the executor for this session
234
+ const contextId = ContextIdFactory.getByRequest(req);
235
+ const executor = await this.moduleRef.resolve(
236
+ McpExecutorService,
237
+ contextId,
238
+ { strict: true },
239
+ );
240
+ this.executors[sid] = executor;
241
+
242
+ // Register request handlers ONCE during initialization
243
+ executor.registerRequestHandlers(mcpServer, req);
244
+ },
245
+ onsessionclosed: async (sid: string) => {
246
+ this.logger.debug(`[${sid}] Session closed via DELETE`);
247
+ await this.cleanupSession(sid);
217
248
  },
218
249
  });
219
250
 
220
- // Attach onclose cleanup BEFORE handling any request
221
- transport.onclose = () => {
222
- if (transport.sessionId) {
223
- this.cleanupSession(transport.sessionId);
224
- }
225
- };
226
-
227
- // Connect the transport to the MCP server BEFORE handling the request
251
+ // Connect transport to server
228
252
  await mcpServer.connect(transport);
229
253
 
230
- // Register request handlers BEFORE the first handleRequest to avoid races
231
- const contextId = ContextIdFactory.getByRequest(req);
232
- const executor = await this.moduleRef.resolve(
233
- McpExecutorService,
234
- contextId,
235
- { strict: true },
236
- );
237
- executor.registerRequestHandlers(mcpServer, req);
238
-
239
254
  // Handle the initialization request
240
255
  await transport.handleRequest(req.raw, res.raw, body);
241
256
 
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
- });
257
+ this.logger.log(`[${transport.sessionId}] New session initialized`);
266
258
  return;
267
259
  }
268
260
 
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
- return;
282
- }
261
+ // Case 2: Request with session ID
262
+ if (sessionId) {
263
+ // Check if session exists
264
+ if (!this.transports[sessionId]) {
265
+ this.logger.debug(`[${sessionId}] Session not found`);
266
+ res.status(404).json({
267
+ jsonrpc: '2.0',
268
+ error: {
269
+ code: -32001,
270
+ message: 'Session not found',
271
+ },
272
+ id: null,
273
+ });
274
+ return;
275
+ }
283
276
 
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
- );
277
+ // Reject re-initialization attempts
278
+ if (this.isInitializeRequest(body)) {
279
+ res.status(400).json({
280
+ jsonrpc: '2.0',
281
+ error: {
282
+ code: -32600,
283
+ message: 'Invalid Request: Server already initialized',
284
+ },
285
+ id: null,
286
+ });
287
+ return;
288
+ }
291
289
 
292
- // Register request handlers with the user context from this specific request
293
- executor.registerRequestHandlers(mcpServer, req);
290
+ // Use existing transport - DO NOT re-register handlers
291
+ const transport = this.transports[sessionId];
294
292
 
295
- this.logger.debug(`[${sessionId}] Handling subsequent request`);
293
+ this.logger.debug(
294
+ `[${sessionId}] Handling request with existing session`,
295
+ );
296
296
 
297
- // Handle the request with existing transport
298
- await transport.handleRequest(req.raw, res.raw, body);
297
+ // Handle the request with existing transport and handlers
298
+ await transport.handleRequest(req.raw, res.raw, body);
299
+ return;
300
+ }
301
+
302
+ // Case 3: No session ID and not initialization
303
+ res.status(400).json({
304
+ jsonrpc: '2.0',
305
+ error: {
306
+ code: -32000,
307
+ message: 'Bad Request: Mcp-Session-Id header is required',
308
+ },
309
+ id: null,
310
+ });
299
311
  }
300
312
 
301
313
  /**
302
314
  * Handle GET requests for SSE streams
303
315
  */
304
316
  async handleGetRequest(req: any, res: any): Promise<void> {
305
- // Get the appropriate HTTP adapter for the request/response
306
317
  const adapter = HttpAdapterFactory.getAdapter(req, res);
307
318
  const adaptedReq = adapter.adaptRequest(req);
308
319
  const adaptedRes = adapter.adaptResponse(res);
@@ -323,13 +334,28 @@ export class McpStreamableHttpService {
323
334
  | string
324
335
  | undefined;
325
336
 
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');
337
+ if (!sessionId) {
338
+ adaptedRes.status(400).json({
339
+ jsonrpc: '2.0',
340
+ error: {
341
+ code: -32000,
342
+ message: 'Bad Request: Mcp-Session-Id header is required',
343
+ },
344
+ id: null,
345
+ });
346
+ return;
347
+ }
348
+
349
+ if (!this.transports[sessionId]) {
350
+ this.logger.debug(`[${sessionId}] GET request - session not found`);
351
+ adaptedRes.status(404).json({
352
+ jsonrpc: '2.0',
353
+ error: {
354
+ code: -32001,
355
+ message: 'Session not found',
356
+ },
357
+ id: null,
358
+ });
333
359
  return;
334
360
  }
335
361
 
@@ -342,7 +368,6 @@ export class McpStreamableHttpService {
342
368
  * Handle DELETE requests for terminating sessions
343
369
  */
344
370
  async handleDeleteRequest(req: any, res: any): Promise<void> {
345
- // Get the appropriate HTTP adapter for the request/response
346
371
  const adapter = HttpAdapterFactory.getAdapter(req, res);
347
372
  const adaptedReq = adapter.adaptRequest(req);
348
373
  const adaptedRes = adapter.adaptResponse(res);
@@ -363,24 +388,43 @@ export class McpStreamableHttpService {
363
388
  | string
364
389
  | undefined;
365
390
 
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');
391
+ if (!sessionId) {
392
+ adaptedRes.status(400).json({
393
+ jsonrpc: '2.0',
394
+ error: {
395
+ code: -32000,
396
+ message: 'Bad Request: Mcp-Session-Id header is required',
397
+ },
398
+ id: null,
399
+ });
371
400
  return;
372
401
  }
373
402
 
374
- this.logger.debug(`[${sessionId}] Terminating session`);
403
+ if (!this.transports[sessionId]) {
404
+ this.logger.debug(`[${sessionId}] DELETE request - session not found`);
405
+ adaptedRes.status(404).json({
406
+ jsonrpc: '2.0',
407
+ error: {
408
+ code: -32001,
409
+ message: 'Session not found',
410
+ },
411
+ id: null,
412
+ });
413
+ return;
414
+ }
415
+
416
+ this.logger.debug(`[${sessionId}] Processing DELETE request`);
375
417
  const transport = this.transports[sessionId];
376
418
 
377
- // Let the transport handle termination; onclose handler will perform cleanup.
419
+ // Let transport handle the DELETE request
420
+ // The onsessionclosed callback will handle cleanup
378
421
  await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
379
- // DO NOT call this.cleanupSession(sessionId) here to avoid double cleanup.
380
422
  }
381
423
 
382
- // Helper function to detect initialize requests
383
- isInitializeRequest(body: unknown): boolean {
424
+ /**
425
+ * Helper function to detect initialize requests
426
+ */
427
+ private isInitializeRequest(body: unknown): boolean {
384
428
  if (Array.isArray(body)) {
385
429
  return body.some(
386
430
  (msg) =>
@@ -398,12 +442,49 @@ export class McpStreamableHttpService {
398
442
  );
399
443
  }
400
444
 
401
- // Clean up session resources
402
- cleanupSession(sessionId: string): void {
403
- if (sessionId) {
404
- this.logger.debug(`[${sessionId}] Cleaning up session`);
445
+ /**
446
+ * Clean up session resources
447
+ */
448
+ private async cleanupSession(sessionId: string): Promise<void> {
449
+ if (!sessionId || !this.transports[sessionId]) {
450
+ return;
451
+ }
452
+
453
+ this.logger.debug(`[${sessionId}] Cleaning up session`);
454
+
455
+ try {
456
+ // Close transport if still open
457
+ const transport = this.transports[sessionId];
458
+ if (transport) {
459
+ await transport.close();
460
+ }
461
+
462
+ // Close MCP server
463
+ const server = this.mcpServers[sessionId];
464
+ if (server) {
465
+ await server.close();
466
+ }
467
+
468
+ // Clean up all references
405
469
  delete this.transports[sessionId];
406
470
  delete this.mcpServers[sessionId];
471
+ delete this.executors[sessionId];
472
+ } catch (error) {
473
+ this.logger.error(`[${sessionId}] Error during cleanup:`, error);
407
474
  }
408
475
  }
476
+
477
+ /**
478
+ * Clean up all sessions on module destroy
479
+ */
480
+ async onModuleDestroy(): Promise<void> {
481
+ this.logger.log('Cleaning up all MCP sessions...');
482
+ const sessionIds = Object.keys(this.transports);
483
+
484
+ await Promise.all(
485
+ sessionIds.map((sessionId) => this.cleanupSession(sessionId)),
486
+ );
487
+
488
+ this.logger.log(`Cleaned up ${sessionIds.length} MCP sessions`);
489
+ }
409
490
  }