@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.
- package/dist/mcp/services/mcp-streamable-http.service.d.ts +6 -3
- package/dist/mcp/services/mcp-streamable-http.service.d.ts.map +1 -1
- package/dist/mcp/services/mcp-streamable-http.service.js +141 -68
- package/dist/mcp/services/mcp-streamable-http.service.js.map +1 -1
- package/package.json +1 -1
- package/src/mcp/services/mcp-streamable-http.service.ts +200 -119
|
@@ -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
|
|
27
|
-
cleanupSession
|
|
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":"
|
|
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}
|
|
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
|
|
93
|
-
this.logger.debug('[Stateless]
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
109
|
-
if (sessionId && this.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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}]
|
|
160
|
+
this.logger.log(`[${transport.sessionId}] New session initialized`);
|
|
140
161
|
return;
|
|
141
162
|
}
|
|
142
|
-
|
|
143
|
-
|
|
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: -
|
|
147
|
-
message: '
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
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
|
|
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
|
|
200
|
-
|
|
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: '
|
|
265
|
+
message: 'Bad Request: Mcp-Session-Id header is required',
|
|
220
266
|
},
|
|
221
267
|
id: null,
|
|
222
268
|
});
|
|
223
269
|
return;
|
|
224
270
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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}]
|
|
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
|
-
|
|
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,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}
|
|
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
|
|
151
|
-
res.on
|
|
152
|
-
this.logger.debug('[Stateless]
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
//
|
|
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}]
|
|
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
|
-
//
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
this.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
293
|
-
|
|
290
|
+
// Use existing transport - DO NOT re-register handlers
|
|
291
|
+
const transport = this.transports[sessionId];
|
|
294
292
|
|
|
295
|
-
|
|
293
|
+
this.logger.debug(
|
|
294
|
+
`[${sessionId}] Handling request with existing session`,
|
|
295
|
+
);
|
|
296
296
|
|
|
297
|
-
|
|
298
|
-
|
|
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
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
383
|
-
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
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
|
}
|