@rekog/mcp-nest 1.8.2-alpha.1 → 1.8.2-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp/services/mcp-streamable-http.service.d.ts +11 -4
- package/dist/mcp/services/mcp-streamable-http.service.d.ts.map +1 -1
- package/dist/mcp/services/mcp-streamable-http.service.js +149 -84
- 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 +246 -171
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { OnModuleDestroy } from '@nestjs/common';
|
|
1
2
|
import { ModuleRef } from '@nestjs/core';
|
|
2
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
5
|
import { HttpRequest, HttpResponse } from '../interfaces/http-adapter.interface';
|
|
5
6
|
import { McpOptions } from '../interfaces';
|
|
7
|
+
import { McpExecutorService } from './mcp-executor.service';
|
|
6
8
|
import { McpRegistryService } from './mcp-registry.service';
|
|
7
|
-
export declare class McpStreamableHttpService {
|
|
9
|
+
export declare class McpStreamableHttpService implements OnModuleDestroy {
|
|
8
10
|
private readonly options;
|
|
9
11
|
private readonly mcpModuleId;
|
|
10
12
|
private readonly moduleRef;
|
|
@@ -12,18 +14,23 @@ export declare class McpStreamableHttpService {
|
|
|
12
14
|
private readonly logger;
|
|
13
15
|
private readonly transports;
|
|
14
16
|
private readonly mcpServers;
|
|
17
|
+
private readonly sessionExecutors;
|
|
15
18
|
private readonly isStatelessMode;
|
|
16
19
|
constructor(options: McpOptions, mcpModuleId: string, moduleRef: ModuleRef, toolRegistry: McpRegistryService);
|
|
20
|
+
private createMcpServer;
|
|
17
21
|
createStatelessServer(rawReq: any): Promise<{
|
|
18
22
|
server: McpServer;
|
|
19
23
|
transport: StreamableHTTPServerTransport;
|
|
24
|
+
executor: McpExecutorService;
|
|
20
25
|
}>;
|
|
21
26
|
handlePostRequest(req: any, res: any, body: unknown): Promise<void>;
|
|
22
|
-
handleStatelessRequest(req:
|
|
27
|
+
handleStatelessRequest(req: HttpRequest, res: HttpResponse, body: unknown): Promise<void>;
|
|
28
|
+
private cleanupStatelessResources;
|
|
23
29
|
handleStatefulRequest(req: HttpRequest, res: HttpResponse, body: unknown): Promise<void>;
|
|
24
30
|
handleGetRequest(req: any, res: any): Promise<void>;
|
|
25
31
|
handleDeleteRequest(req: any, res: any): Promise<void>;
|
|
26
|
-
isInitializeRequest
|
|
27
|
-
cleanupSession
|
|
32
|
+
private isInitializeRequest;
|
|
33
|
+
private cleanupSession;
|
|
34
|
+
onModuleDestroy(): Promise<void>;
|
|
28
35
|
}
|
|
29
36
|
//# sourceMappingURL=mcp-streamable-http.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-streamable-http.service.d.ts","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"mcp-streamable-http.service.d.ts","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":"AAAA,OAAO,EAA8B,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC7E,OAAO,EAAoB,SAAS,EAAE,MAAM,cAAc,CAAC;AAE3D,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAEnG,OAAO,EACL,WAAW,EACX,YAAY,EACb,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,qBACa,wBAAyB,YAAW,eAAe;IAerC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;IACrD,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,YAAY;IAjB/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6C;IAGpE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAEpB;IACP,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA0C;IACrE,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAE1B;IAEP,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;gBAGA,OAAO,EAAE,UAAU,EACjB,WAAW,EAAE,MAAM,EAC5C,SAAS,EAAE,SAAS,EACpB,YAAY,EAAE,kBAAkB;IAQnD,OAAO,CAAC,eAAe;IAqBjB,qBAAqB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC;QAChD,MAAM,EAAE,SAAS,CAAC;QAClB,SAAS,EAAE,6BAA6B,CAAC;QACzC,QAAQ,EAAE,kBAAkB,CAAC;KAC9B,CAAC;IA+BI,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAgCnE,sBAAsB,CAC1B,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;YA8BF,yBAAyB;IAmBjC,qBAAqB,CACzB,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,YAAY,EACjB,IAAI,EAAE,OAAO,GACZ,OAAO,CAAC,IAAI,CAAC;IAyHV,gBAAgB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAwDnD,mBAAmB,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAwD5D,OAAO,CAAC,mBAAmB;YAqBb,cAAc;IAgCtB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;CAUvC"}
|
|
@@ -32,32 +32,34 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
32
32
|
this.logger = new common_1.Logger(McpStreamableHttpService_1.name);
|
|
33
33
|
this.transports = {};
|
|
34
34
|
this.mcpServers = {};
|
|
35
|
+
this.sessionExecutors = {};
|
|
35
36
|
this.isStatelessMode = !!options.streamableHttp?.statelessMode;
|
|
36
37
|
}
|
|
38
|
+
createMcpServer() {
|
|
39
|
+
const capabilities = (0, capabilities_builder_1.buildMcpCapabilities)(this.mcpModuleId, this.toolRegistry, this.options);
|
|
40
|
+
this.logger.debug('Building MCP server with capabilities:', capabilities);
|
|
41
|
+
return new mcp_js_1.McpServer({ name: this.options.name, version: this.options.version }, {
|
|
42
|
+
capabilities,
|
|
43
|
+
instructions: this.options.instructions || '',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
37
46
|
async createStatelessServer(rawReq) {
|
|
38
47
|
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
39
48
|
sessionIdGenerator: undefined,
|
|
40
49
|
enableJsonResponse: this.options.streamableHttp?.enableJsonResponse || false,
|
|
41
50
|
});
|
|
42
|
-
const
|
|
43
|
-
this.logger.debug(`[Stateless] Built MCP capabilities: ${JSON.stringify(capabilities)}`);
|
|
44
|
-
const server = new mcp_js_1.McpServer({ name: this.options.name, version: this.options.version }, {
|
|
45
|
-
capabilities: capabilities,
|
|
46
|
-
instructions: this.options.instructions || '',
|
|
47
|
-
});
|
|
51
|
+
const server = this.createMcpServer();
|
|
48
52
|
await server.connect(transport);
|
|
49
53
|
const contextId = core_1.ContextIdFactory.getByRequest(rawReq);
|
|
50
54
|
const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
|
|
51
|
-
this.logger.debug('[Stateless] Registering request handlers for stateless MCP server');
|
|
52
55
|
executor.registerRequestHandlers(server, rawReq);
|
|
53
|
-
return { server, transport };
|
|
56
|
+
return { server, transport, executor };
|
|
54
57
|
}
|
|
55
58
|
async handlePostRequest(req, res, body) {
|
|
59
|
+
this.logger.debug('Received MCP POST request');
|
|
56
60
|
const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
|
|
57
61
|
const adaptedReq = adapter.adaptRequest(req);
|
|
58
62
|
const adaptedRes = adapter.adaptResponse(res);
|
|
59
|
-
const sessionId = adaptedReq.headers['mcp-session-id'];
|
|
60
|
-
this.logger.debug(`[${sessionId || 'No-Session'}] Received MCP request: ${JSON.stringify(body)}`);
|
|
61
63
|
try {
|
|
62
64
|
if (this.isStatelessMode) {
|
|
63
65
|
return this.handleStatelessRequest(adaptedReq, adaptedRes, body);
|
|
@@ -67,7 +69,7 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
catch (error) {
|
|
70
|
-
this.logger.error(
|
|
72
|
+
this.logger.error('Error handling MCP request:', error);
|
|
71
73
|
if (!adaptedRes.headersSent) {
|
|
72
74
|
adaptedRes.status(500).json({
|
|
73
75
|
jsonrpc: '2.0',
|
|
@@ -81,7 +83,7 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
81
83
|
}
|
|
82
84
|
}
|
|
83
85
|
async handleStatelessRequest(req, res, body) {
|
|
84
|
-
this.logger.debug(
|
|
86
|
+
this.logger.debug('Handling stateless MCP request');
|
|
85
87
|
let server = null;
|
|
86
88
|
let transport = null;
|
|
87
89
|
try {
|
|
@@ -89,69 +91,125 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
89
91
|
server = stateless.server;
|
|
90
92
|
transport = stateless.transport;
|
|
91
93
|
await transport.handleRequest(req.raw, res.raw, body);
|
|
92
|
-
res.on?.('
|
|
93
|
-
this.logger.debug('
|
|
94
|
-
|
|
95
|
-
void server?.close();
|
|
94
|
+
res.on?.('finish', async () => {
|
|
95
|
+
this.logger.debug('Stateless response finished, cleaning up');
|
|
96
|
+
await this.cleanupStatelessResources(server, transport);
|
|
96
97
|
});
|
|
97
98
|
}
|
|
98
99
|
catch (error) {
|
|
99
|
-
this.logger.error(
|
|
100
|
-
|
|
101
|
-
void server?.close();
|
|
100
|
+
this.logger.error('Error in stateless request handling:', error);
|
|
101
|
+
await this.cleanupStatelessResources(server, transport);
|
|
102
102
|
throw error;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
async cleanupStatelessResources(server, transport) {
|
|
106
|
+
try {
|
|
107
|
+
if (transport) {
|
|
108
|
+
await transport.close();
|
|
109
|
+
}
|
|
110
|
+
if (server) {
|
|
111
|
+
await server.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
this.logger.error('Error cleaning up stateless resources:', error);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
105
118
|
async handleStatefulRequest(req, res, body) {
|
|
106
|
-
this.logger.debug(
|
|
119
|
+
this.logger.debug('Handling stateful MCP request');
|
|
107
120
|
const sessionId = req.headers['mcp-session-id'];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
if (!sessionId && this.isInitializeRequest(body)) {
|
|
122
|
+
if (Array.isArray(body) && body.length > 1) {
|
|
123
|
+
res.status(400).json({
|
|
124
|
+
jsonrpc: '2.0',
|
|
125
|
+
error: {
|
|
126
|
+
code: -32600,
|
|
127
|
+
message: 'Invalid Request: Only one initialization request is allowed',
|
|
128
|
+
},
|
|
129
|
+
id: null,
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
|
|
120
134
|
sessionIdGenerator: this.options.streamableHttp?.sessionIdGenerator ||
|
|
121
135
|
(() => (0, crypto_1.randomUUID)()),
|
|
122
136
|
enableJsonResponse: this.options.streamableHttp?.enableJsonResponse || false,
|
|
123
|
-
onsessioninitialized: (
|
|
124
|
-
this.logger.
|
|
125
|
-
this.transports[
|
|
126
|
-
|
|
137
|
+
onsessioninitialized: (sessionId) => {
|
|
138
|
+
this.logger.log(`Session initialized: ${sessionId}`);
|
|
139
|
+
this.transports[sessionId] = transport;
|
|
140
|
+
},
|
|
141
|
+
onsessionclosed: async (sessionId) => {
|
|
142
|
+
this.logger.log(`Session termination requested: ${sessionId}`);
|
|
143
|
+
await this.cleanupSession(sessionId);
|
|
127
144
|
},
|
|
128
145
|
});
|
|
129
|
-
|
|
130
|
-
if (transport.sessionId) {
|
|
131
|
-
this.cleanupSession(transport.sessionId);
|
|
132
|
-
}
|
|
133
|
-
};
|
|
146
|
+
const mcpServer = this.createMcpServer();
|
|
134
147
|
await mcpServer.connect(transport);
|
|
135
148
|
const contextId = core_1.ContextIdFactory.getByRequest(req);
|
|
136
149
|
const executor = await this.moduleRef.resolve(mcp_executor_service_1.McpExecutorService, contextId, { strict: true });
|
|
137
150
|
executor.registerRequestHandlers(mcpServer, req);
|
|
138
151
|
await transport.handleRequest(req.raw, res.raw, body);
|
|
139
|
-
|
|
152
|
+
if (transport.sessionId) {
|
|
153
|
+
this.mcpServers[transport.sessionId] = mcpServer;
|
|
154
|
+
this.sessionExecutors[transport.sessionId] = executor;
|
|
155
|
+
this.logger.log(`Session fully initialized: ${transport.sessionId}`);
|
|
156
|
+
}
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (sessionId) {
|
|
160
|
+
if (!this.transports[sessionId]) {
|
|
161
|
+
res.status(404).json({
|
|
162
|
+
jsonrpc: '2.0',
|
|
163
|
+
error: {
|
|
164
|
+
code: -32001,
|
|
165
|
+
message: 'Session not found',
|
|
166
|
+
},
|
|
167
|
+
id: null,
|
|
168
|
+
});
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
if (this.isInitializeRequest(body)) {
|
|
172
|
+
res.status(400).json({
|
|
173
|
+
jsonrpc: '2.0',
|
|
174
|
+
error: {
|
|
175
|
+
code: -32600,
|
|
176
|
+
message: 'Invalid Request: Server already initialized',
|
|
177
|
+
},
|
|
178
|
+
id: null,
|
|
179
|
+
});
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const transport = this.transports[sessionId];
|
|
183
|
+
await transport.handleRequest(req.raw, res.raw, body);
|
|
140
184
|
return;
|
|
141
185
|
}
|
|
142
|
-
|
|
143
|
-
|
|
186
|
+
res.status(400).json({
|
|
187
|
+
jsonrpc: '2.0',
|
|
188
|
+
error: {
|
|
189
|
+
code: -32000,
|
|
190
|
+
message: 'Bad Request: No valid session ID provided',
|
|
191
|
+
},
|
|
192
|
+
id: null,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async handleGetRequest(req, res) {
|
|
196
|
+
const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
|
|
197
|
+
const adaptedReq = adapter.adaptRequest(req);
|
|
198
|
+
const adaptedRes = adapter.adaptResponse(res);
|
|
199
|
+
if (this.isStatelessMode) {
|
|
200
|
+
adaptedRes.status(405).json({
|
|
144
201
|
jsonrpc: '2.0',
|
|
145
202
|
error: {
|
|
146
|
-
code: -
|
|
147
|
-
message: '
|
|
203
|
+
code: -32000,
|
|
204
|
+
message: 'Method not allowed in stateless mode',
|
|
148
205
|
},
|
|
149
206
|
id: null,
|
|
150
207
|
});
|
|
151
208
|
return;
|
|
152
209
|
}
|
|
153
|
-
|
|
154
|
-
|
|
210
|
+
const sessionId = adaptedReq.headers['mcp-session-id'];
|
|
211
|
+
if (!sessionId) {
|
|
212
|
+
adaptedRes.status(400).json({
|
|
155
213
|
jsonrpc: '2.0',
|
|
156
214
|
error: {
|
|
157
215
|
code: -32000,
|
|
@@ -161,10 +219,8 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
161
219
|
});
|
|
162
220
|
return;
|
|
163
221
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
this.logger.debug(`[${sessionId}] Session not found`);
|
|
167
|
-
res.status(404).json({
|
|
222
|
+
if (!this.transports[sessionId]) {
|
|
223
|
+
adaptedRes.status(404).json({
|
|
168
224
|
jsonrpc: '2.0',
|
|
169
225
|
error: {
|
|
170
226
|
code: -32001,
|
|
@@ -174,13 +230,11 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
174
230
|
});
|
|
175
231
|
return;
|
|
176
232
|
}
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
this.logger.debug(`[${sessionId}] Handling subsequent request`);
|
|
181
|
-
await transport.handleRequest(req.raw, res.raw, body);
|
|
233
|
+
this.logger.debug(`Establishing SSE stream for session ${sessionId}`);
|
|
234
|
+
const transport = this.transports[sessionId];
|
|
235
|
+
await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
|
|
182
236
|
}
|
|
183
|
-
async
|
|
237
|
+
async handleDeleteRequest(req, res) {
|
|
184
238
|
const adapter = http_adapter_factory_1.HttpAdapterFactory.getAdapter(req, res);
|
|
185
239
|
const adaptedReq = adapter.adaptRequest(req);
|
|
186
240
|
const adaptedRes = adapter.adaptResponse(res);
|
|
@@ -196,39 +250,29 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
196
250
|
return;
|
|
197
251
|
}
|
|
198
252
|
const sessionId = adaptedReq.headers['mcp-session-id'];
|
|
199
|
-
if (!sessionId
|
|
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({
|
|
253
|
+
if (!sessionId) {
|
|
254
|
+
adaptedRes.status(400).json({
|
|
216
255
|
jsonrpc: '2.0',
|
|
217
256
|
error: {
|
|
218
257
|
code: -32000,
|
|
219
|
-
message: '
|
|
258
|
+
message: 'Bad Request: Mcp-Session-Id header is required',
|
|
220
259
|
},
|
|
221
260
|
id: null,
|
|
222
261
|
});
|
|
223
262
|
return;
|
|
224
263
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
264
|
+
if (!this.transports[sessionId]) {
|
|
265
|
+
adaptedRes.status(404).json({
|
|
266
|
+
jsonrpc: '2.0',
|
|
267
|
+
error: {
|
|
268
|
+
code: -32001,
|
|
269
|
+
message: 'Session not found',
|
|
270
|
+
},
|
|
271
|
+
id: null,
|
|
272
|
+
});
|
|
229
273
|
return;
|
|
230
274
|
}
|
|
231
|
-
this.logger.debug(`
|
|
275
|
+
this.logger.debug(`Processing DELETE request for session ${sessionId}`);
|
|
232
276
|
const transport = this.transports[sessionId];
|
|
233
277
|
await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
|
|
234
278
|
}
|
|
@@ -244,12 +288,33 @@ let McpStreamableHttpService = McpStreamableHttpService_1 = class McpStreamableH
|
|
|
244
288
|
'method' in body &&
|
|
245
289
|
body.method === 'initialize');
|
|
246
290
|
}
|
|
247
|
-
cleanupSession(sessionId) {
|
|
248
|
-
if (sessionId)
|
|
249
|
-
|
|
291
|
+
async cleanupSession(sessionId) {
|
|
292
|
+
if (!sessionId)
|
|
293
|
+
return;
|
|
294
|
+
this.logger.debug(`Cleaning up session: ${sessionId}`);
|
|
295
|
+
try {
|
|
296
|
+
const transport = this.transports[sessionId];
|
|
297
|
+
if (transport) {
|
|
298
|
+
await transport.close();
|
|
299
|
+
}
|
|
250
300
|
delete this.transports[sessionId];
|
|
301
|
+
const server = this.mcpServers[sessionId];
|
|
302
|
+
if (server) {
|
|
303
|
+
await server.close();
|
|
304
|
+
}
|
|
251
305
|
delete this.mcpServers[sessionId];
|
|
306
|
+
delete this.sessionExecutors[sessionId];
|
|
307
|
+
this.logger.log(`Session cleanup complete: ${sessionId}`);
|
|
252
308
|
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
this.logger.error(`Error cleaning up session ${sessionId}:`, error);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
async onModuleDestroy() {
|
|
314
|
+
this.logger.log('Cleaning up all MCP sessions...');
|
|
315
|
+
const sessionIds = Object.keys(this.transports);
|
|
316
|
+
await Promise.all(sessionIds.map((sessionId) => this.cleanupSession(sessionId)));
|
|
317
|
+
this.logger.log('All MCP sessions cleaned up');
|
|
253
318
|
}
|
|
254
319
|
};
|
|
255
320
|
exports.McpStreamableHttpService = McpStreamableHttpService;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-streamable-http.service.js","sourceRoot":"","sources":["../../../src/mcp/services/mcp-streamable-http.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,2CAA4D;AAC5D,uCAA2D;AAC3D,mCAAoC;AACpC,oEAAoE;AACpE,0FAAmG;AACnG,2EAAsE;AAMtE,iEAA4D;AAC5D,iEAA4D;AAC5D,wEAAqE;AAG9D,IAAM,wBAAwB,gCAA9B,MAAM,wBAAwB;IAQnC,YACyB,OAAoC,EAClC,WAAoC,EAC5C,SAAoB,EACpB,YAAgC;QAHT,YAAO,GAAP,OAAO,CAAY;QACjB,gBAAW,GAAX,WAAW,CAAQ;QAC5C,cAAS,GAAT,SAAS,CAAW;QACpB,iBAAY,GAAZ,YAAY,CAAoB;QAXlC,WAAM,GAAG,IAAI,eAAM,CAAC,0BAAwB,CAAC,IAAI,CAAC,CAAC;QACnD,eAAU,GAEvB,EAAE,CAAC;QACU,eAAU,GAAuC,EAAE,CAAC;QAUnE,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,aAAa,CAAC;IACjE,CAAC;IAKD,KAAK,CAAC,qBAAqB,CAAC,MAAW;QAKrC,MAAM,SAAS,GAAG,IAAI,iDAA6B,CAAC;YAClD,kBAAkB,EAAE,SAAS;YAC7B,kBAAkB,EAChB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,kBAAkB,IAAI,KAAK;SAC3D,CAAC,CAAC;QAGH,MAAM,YAAY,GAAG,IAAA,2CAAoB,EACvC,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,OAAO,CACb,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,uCAAuC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CACtE,CAAC;QAEF,MAAM,MAAM,GAAG,IAAI,kBAAS,CAC1B,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAC1D;YACE,YAAY,EAAE,YAAY;YAC1B,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE;SAC9C,CACF,CAAC;QAGF,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAGhC,MAAM,SAAS,GAAG,uBAAgB,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAC3C,yCAAkB,EAClB,SAAS,EACT,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;QAGF,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,mEAAmE,CACpE,CAAC;QACF,QAAQ,CAAC,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAEjD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAC/B,CAAC;IAKD,KAAK,CAAC,iBAAiB,CAAC,GAAQ,EAAE,GAAQ,EAAE,IAAa;QAEvD,MAAM,OAAO,GAAG,yCAAkB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAC9C,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAExC,CAAC;QAEd,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,IAAI,SAAS,IAAI,YAAY,2BAA2B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAC/E,CAAC;QAEF,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;gBACzB,OAAO,IAAI,CAAC,sBAAsB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;YACnE,CAAC;iBAAM,CAAC;gBACN,OAAO,IAAI,CAAC,qBAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,IAAI,SAAS,IAAI,YAAY,iCAAiC,KAAK,EAAE,CACtE,CAAC;YACF,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;gBAC5B,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC1B,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE;wBACL,IAAI,EAAE,CAAC,KAAK;wBACZ,OAAO,EAAE,uBAAuB;qBACjC;oBACD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAKD,KAAK,CAAC,sBAAsB,CAC1B,GAAQ,EACR,GAAiB,EACjB,IAAa;QAEb,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,iDAAiD,GAAG,CAAC,GAAG,eAAe,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAC9F,CAAC;QAEF,IAAI,MAAM,GAAqB,IAAI,CAAC;QACpC,IAAI,SAAS,GAAyC,IAAI,CAAC;QAE3D,IAAI,CAAC;YAEH,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC;YACxD,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;YAC1B,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC;YAGhC,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAGtD,GAAG,CAAC,EAAE,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;gBACrB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,CAAC,CAAC;gBAC7D,KAAK,SAAS,EAAE,KAAK,EAAE,CAAC;gBACxB,KAAK,MAAM,EAAE,KAAK,EAAE,CAAC;YACvB,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,oDAAoD,KAAK,EAAE,CAC5D,CAAC;YAEF,KAAK,SAAS,EAAE,KAAK,EAAE,CAAC;YACxB,KAAK,MAAM,EAAE,KAAK,EAAE,CAAC;YACrB,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAKD,KAAK,CAAC,qBAAqB,CACzB,GAAgB,EAChB,GAAiB,EACjB,IAAa;QAEb,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,+CAA+C,GAAG,CAAC,GAAG,eAAe,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAC5F,CAAC;QAGF,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAC;QACtE,IAAI,SAAwC,CAAC;QAE7C,IAAI,SAAS,IAAI,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAE5C,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QACzC,CAAC;aAAM,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;YAExD,MAAM,YAAY,GAAG,IAAA,2CAAoB,EACvC,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,OAAO,CACb,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,IAAI,SAAS,IAAI,aAAa,6BAA6B,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,CAC1F,CAAC;YAEF,MAAM,SAAS,GAAG,IAAI,kBAAS,CAC7B,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAC1D;gBACE,YAAY;gBACZ,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE;aAC9C,CACF,CAAC;YAGF,SAAS,GAAG,IAAI,iDAA6B,CAAC;gBAC5C,kBAAkB,EAChB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,kBAAkB;oBAC/C,CAAC,GAAG,EAAE,CAAC,IAAA,mBAAU,GAAE,CAAC;gBACtB,kBAAkB,EAChB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,kBAAkB,IAAI,KAAK;gBAE1D,oBAAoB,EAAE,CAAC,GAAW,EAAE,EAAE;oBACpC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,uBAAuB,CAAC,CAAC;oBAClD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;oBACjC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC;gBACnC,CAAC;aACF,CAAC,CAAC;YAGH,SAAS,CAAC,OAAO,GAAG,GAAG,EAAE;gBACvB,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;oBACxB,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;gBAC3C,CAAC;YACH,CAAC,CAAC;YAGF,MAAM,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAGnC,MAAM,SAAS,GAAG,uBAAgB,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;YACrD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAC3C,yCAAkB,EAClB,SAAS,EACT,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;YACF,QAAQ,CAAC,uBAAuB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YAGjD,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAEtD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,SAAS,CAAC,SAAS,2BAA2B,CAAC,CAAC;YACpE,OAAO;QACT,CAAC;aAAM,IAAI,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAEpD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,mBAAmB;iBAC7B;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;aAAM,CAAC;YAGN,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,gDAAgD;iBAC1D;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAGD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAC7C,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,qBAAqB,CAAC,CAAC;YACtD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,mBAAmB;iBAC7B;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAGD,MAAM,SAAS,GAAG,uBAAgB,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QACrD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAC3C,yCAAkB,EAClB,SAAS,EACT,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;QAGF,QAAQ,CAAC,uBAAuB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAEjD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,+BAA+B,CAAC,CAAC;QAGhE,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IACxD,CAAC;IAKD,KAAK,CAAC,gBAAgB,CAAC,GAAQ,EAAE,GAAQ;QAEvC,MAAM,OAAO,GAAG,yCAAkB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAE9C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,sCAAsC;iBAChD;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAExC,CAAC;QAEd,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,IAAI,SAAS,IAAI,YAAY,0CAA0C,CACxE,CAAC;YACF,UAAU;iBACP,MAAM,CAAC,GAAG,CAAC;iBACX,IAAI,CAAC,gDAAgD,CAAC,CAAC;YAC1D,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,2BAA2B,CAAC,CAAC;QAC5D,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,SAAS,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAChE,CAAC;IAKD,KAAK,CAAC,mBAAmB,CAAC,GAAQ,EAAE,GAAQ;QAE1C,MAAM,OAAO,GAAG,yCAAkB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAE9C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,sCAAsC;iBAChD;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAExC,CAAC;QAEd,IAAI,CAAC,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,IAAI,SAAS,IAAI,YAAY,6CAA6C,CAC3E,CAAC;YACF,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,+BAA+B,CAAC,CAAC;YAC7D,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,uBAAuB,CAAC,CAAC;QACxD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAG7C,MAAM,SAAS,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAEhE,CAAC;IAGD,mBAAmB,CAAC,IAAa;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,IAAI,CACd,CAAC,GAAG,EAAE,EAAE,CACN,OAAO,GAAG,KAAK,QAAQ;gBACvB,GAAG,KAAK,IAAI;gBACZ,QAAQ,IAAI,GAAG;gBACf,GAAG,CAAC,MAAM,KAAK,YAAY,CAC9B,CAAC;QACJ,CAAC;QACD,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;YACxB,IAAI,KAAK,IAAI;YACb,QAAQ,IAAI,IAAI;YACf,IAAY,CAAC,MAAM,KAAK,YAAY,CACtC,CAAC;IACJ,CAAC;IAGD,cAAc,CAAC,SAAiB;QAC9B,IAAI,SAAS,EAAE,CAAC;YACd,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,SAAS,uBAAuB,CAAC,CAAC;YACxD,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAClC,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;CACF,CAAA;AAxYY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,mBAAU,GAAE;IAUR,WAAA,IAAA,eAAM,EAAC,aAAa,CAAC,CAAA;IACrB,WAAA,IAAA,eAAM,EAAC,eAAe,CAAC,CAAA;qDACI,gBAAS;QACN,yCAAkB;GAZxC,wBAAwB,CAwYpC","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":";;;;;;;;;;;;;;;;AAAA,2CAA6E;AAC7E,uCAA2D;AAC3D,mCAAoC;AACpC,oEAAoE;AACpE,0FAAmG;AACnG,2EAAsE;AAMtE,iEAA4D;AAC5D,iEAA4D;AAC5D,wEAAqE;AAG9D,IAAM,wBAAwB,gCAA9B,MAAM,wBAAwB;IAcnC,YACyB,OAAoC,EAClC,WAAoC,EAC5C,SAAoB,EACpB,YAAgC;QAHT,YAAO,GAAP,OAAO,CAAY;QACjB,gBAAW,GAAX,WAAW,CAAQ;QAC5C,cAAS,GAAT,SAAS,CAAW;QACpB,iBAAY,GAAZ,YAAY,CAAoB;QAjBlC,WAAM,GAAG,IAAI,eAAM,CAAC,0BAAwB,CAAC,IAAI,CAAC,CAAC;QAGnD,eAAU,GAEvB,EAAE,CAAC;QACU,eAAU,GAAuC,EAAE,CAAC;QACpD,qBAAgB,GAE7B,EAAE,CAAC;QAUL,IAAI,CAAC,eAAe,GAAG,CAAC,CAAC,OAAO,CAAC,cAAc,EAAE,aAAa,CAAC;IACjE,CAAC;IAKO,eAAe;QACrB,MAAM,YAAY,GAAG,IAAA,2CAAoB,EACvC,IAAI,CAAC,WAAW,EAChB,IAAI,CAAC,YAAY,EACjB,IAAI,CAAC,OAAO,CACb,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE,YAAY,CAAC,CAAC;QAE1E,OAAO,IAAI,kBAAS,CAClB,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,EAC1D;YACE,YAAY;YACZ,YAAY,EAAE,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE;SAC9C,CACF,CAAC;IACJ,CAAC;IAKD,KAAK,CAAC,qBAAqB,CAAC,MAAW;QAMrC,MAAM,SAAS,GAAG,IAAI,iDAA6B,CAAC;YAClD,kBAAkB,EAAE,SAAS;YAC7B,kBAAkB,EAChB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,kBAAkB,IAAI,KAAK;SAC3D,CAAC,CAAC;QAGH,MAAM,MAAM,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;QAGtC,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAGhC,MAAM,SAAS,GAAG,uBAAgB,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QACxD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAC3C,yCAAkB,EAClB,SAAS,EACT,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;QAGF,QAAQ,CAAC,uBAAuB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAEjD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IACzC,CAAC;IAKD,KAAK,CAAC,iBAAiB,CAAC,GAAQ,EAAE,GAAQ,EAAE,IAAa;QACvD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAG/C,MAAM,OAAO,GAAG,yCAAkB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAE9C,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;gBACzB,OAAO,IAAI,CAAC,sBAAsB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;YACnE,CAAC;iBAAM,CAAC;gBACN,OAAO,IAAI,CAAC,qBAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;YACxD,IAAI,CAAC,UAAU,CAAC,WAAW,EAAE,CAAC;gBAC5B,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC1B,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE;wBACL,IAAI,EAAE,CAAC,KAAK;wBACZ,OAAO,EAAE,uBAAuB;qBACjC;oBACD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IAKD,KAAK,CAAC,sBAAsB,CAC1B,GAAgB,EAChB,GAAiB,EACjB,IAAa;QAEb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,gCAAgC,CAAC,CAAC;QAEpD,IAAI,MAAM,GAAqB,IAAI,CAAC;QACpC,IAAI,SAAS,GAAyC,IAAI,CAAC;QAE3D,IAAI,CAAC;YAEH,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,CAAC;YACxD,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;YAC1B,SAAS,GAAG,SAAS,CAAC,SAAS,CAAC;YAGhC,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAGtD,GAAG,CAAC,EAAE,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE;gBAC5B,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,0CAA0C,CAAC,CAAC;gBAC9D,MAAM,IAAI,CAAC,yBAAyB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAC1D,CAAC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,sCAAsC,EAAE,KAAK,CAAC,CAAC;YACjE,MAAM,IAAI,CAAC,yBAAyB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YACxD,MAAM,KAAK,CAAC;QACd,CAAC;IACH,CAAC;IAKO,KAAK,CAAC,yBAAyB,CACrC,MAAwB,EACxB,SAA+C;QAE/C,IAAI,CAAC;YACH,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;YAC1B,CAAC;YACD,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wCAAwC,EAAE,KAAK,CAAC,CAAC;QACrE,CAAC;IACH,CAAC;IAKD,KAAK,CAAC,qBAAqB,CACzB,GAAgB,EAChB,GAAiB,EACjB,IAAa;QAEb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,+BAA+B,CAAC,CAAC;QAEnD,MAAM,SAAS,GAAG,GAAG,CAAC,OAAO,CAAC,gBAAgB,CAAuB,CAAC;QAGtE,IAAI,CAAC,SAAS,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;YAEjD,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE;wBACL,IAAI,EAAE,CAAC,KAAK;wBACZ,OAAO,EACL,6DAA6D;qBAChE;oBACD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAGD,MAAM,SAAS,GAAG,IAAI,iDAA6B,CAAC;gBAClD,kBAAkB,EAChB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,kBAAkB;oBAC/C,CAAC,GAAG,EAAE,CAAC,IAAA,mBAAU,GAAE,CAAC;gBACtB,kBAAkB,EAChB,IAAI,CAAC,OAAO,CAAC,cAAc,EAAE,kBAAkB,IAAI,KAAK;gBAC1D,oBAAoB,EAAE,CAAC,SAAiB,EAAE,EAAE;oBAC1C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,wBAAwB,SAAS,EAAE,CAAC,CAAC;oBAErD,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;gBACzC,CAAC;gBACD,eAAe,EAAE,KAAK,EAAE,SAAiB,EAAE,EAAE;oBAE3C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,kCAAkC,SAAS,EAAE,CAAC,CAAC;oBAC/D,MAAM,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC;gBACvC,CAAC;aACF,CAAC,CAAC;YAGH,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,EAAE,CAAC;YAGzC,MAAM,SAAS,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAGnC,MAAM,SAAS,GAAG,uBAAgB,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;YACrD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,OAAO,CAC3C,yCAAkB,EAClB,SAAS,EACT,EAAE,MAAM,EAAE,IAAI,EAAE,CACjB,CAAC;YAGF,QAAQ,CAAC,uBAAuB,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;YAGjD,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAGtD,IAAI,SAAS,CAAC,SAAS,EAAE,CAAC;gBACxB,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,SAAS,CAAC;gBACjD,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,SAAS,CAAC,GAAG,QAAQ,CAAC;gBACtD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,8BAA8B,SAAS,CAAC,SAAS,EAAE,CAAC,CAAC;YACvE,CAAC;YAED,OAAO;QACT,CAAC;QAGD,IAAI,SAAS,EAAE,CAAC;YAEd,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAChC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE;wBACL,IAAI,EAAE,CAAC,KAAK;wBACZ,OAAO,EAAE,mBAAmB;qBAC7B;oBACD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAGD,IAAI,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;gBACnC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE;wBACL,IAAI,EAAE,CAAC,KAAK;wBACZ,OAAO,EAAE,6CAA6C;qBACvD;oBACD,EAAE,EAAE,IAAI;iBACT,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAGD,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAG7C,MAAM,SAAS,CAAC,aAAa,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YAEtD,OAAO;QACT,CAAC;QAGD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE;gBACL,IAAI,EAAE,CAAC,KAAK;gBACZ,OAAO,EAAE,2CAA2C;aACrD;YACD,EAAE,EAAE,IAAI;SACT,CAAC,CAAC;IACL,CAAC;IAKD,KAAK,CAAC,gBAAgB,CAAC,GAAQ,EAAE,GAAQ;QACvC,MAAM,OAAO,GAAG,yCAAkB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAE9C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAEzB,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,sCAAsC;iBAChD;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAExC,CAAC;QAEd,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,gDAAgD;iBAC1D;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,mBAAmB;iBAC7B;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,SAAS,EAAE,CAAC,CAAC;QACtE,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAG7C,MAAM,SAAS,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAChE,CAAC;IAKD,KAAK,CAAC,mBAAmB,CAAC,GAAQ,EAAE,GAAQ;QAC1C,MAAM,OAAO,GAAG,yCAAkB,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACxD,MAAM,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;QAC7C,MAAM,UAAU,GAAG,OAAO,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;QAE9C,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YACzB,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,sCAAsC;iBAChD;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,OAAO,CAAC,gBAAgB,CAExC,CAAC;QAEd,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,gDAAgD;iBAC1D;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBAC1B,OAAO,EAAE,KAAK;gBACd,KAAK,EAAE;oBACL,IAAI,EAAE,CAAC,KAAK;oBACZ,OAAO,EAAE,mBAAmB;iBAC7B;gBACD,EAAE,EAAE,IAAI;aACT,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,yCAAyC,SAAS,EAAE,CAAC,CAAC;QACxE,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;QAI7C,MAAM,SAAS,CAAC,aAAa,CAAC,UAAU,CAAC,GAAG,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC;IAChE,CAAC;IAKO,mBAAmB,CAAC,IAAa;QACvC,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;YACxB,OAAO,IAAI,CAAC,IAAI,CACd,CAAC,GAAG,EAAE,EAAE,CACN,OAAO,GAAG,KAAK,QAAQ;gBACvB,GAAG,KAAK,IAAI;gBACZ,QAAQ,IAAI,GAAG;gBACf,GAAG,CAAC,MAAM,KAAK,YAAY,CAC9B,CAAC;QACJ,CAAC;QACD,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;YACxB,IAAI,KAAK,IAAI;YACb,QAAQ,IAAI,IAAI;YAChB,IAAI,CAAC,MAAM,KAAK,YAAY,CAC7B,CAAC;IACJ,CAAC;IAKO,KAAK,CAAC,cAAc,CAAC,SAAiB;QAC5C,IAAI,CAAC,SAAS;YAAE,OAAO;QAEvB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,wBAAwB,SAAS,EAAE,CAAC,CAAC;QAEvD,IAAI,CAAC;YAEH,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAC7C,IAAI,SAAS,EAAE,CAAC;gBACd,MAAM,SAAS,CAAC,KAAK,EAAE,CAAC;YAC1B,CAAC;YACD,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAGlC,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAC1C,IAAI,MAAM,EAAE,CAAC;gBACX,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;YACvB,CAAC;YACD,OAAO,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;YAGlC,OAAO,IAAI,CAAC,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAExC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,6BAA6B,SAAS,EAAE,CAAC,CAAC;QAC5D,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,6BAA6B,SAAS,GAAG,EAAE,KAAK,CAAC,CAAC;QACtE,CAAC;IACH,CAAC;IAKD,KAAK,CAAC,eAAe;QACnB,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,iCAAiC,CAAC,CAAC;QAEnD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChD,MAAM,OAAO,CAAC,GAAG,CACf,UAAU,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAC9D,CAAC;QAEF,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;CACF,CAAA;AAndY,4DAAwB;mCAAxB,wBAAwB;IADpC,IAAA,mBAAU,GAAE;IAgBR,WAAA,IAAA,eAAM,EAAC,aAAa,CAAC,CAAA;IACrB,WAAA,IAAA,eAAM,EAAC,eAAe,CAAC,CAAA;qDACI,gBAAS;QACN,yCAAkB;GAlBxC,wBAAwB,CAmdpC","sourcesContent":["import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';\nimport { ContextIdFactory, ModuleRef } from '@nestjs/core';\nimport { randomUUID } from 'crypto';\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';\nimport { HttpAdapterFactory } from '../adapters/http-adapter.factory';\nimport {\n HttpRequest,\n HttpResponse,\n} from '../interfaces/http-adapter.interface';\nimport { McpOptions } from '../interfaces';\nimport { McpExecutorService } from './mcp-executor.service';\nimport { McpRegistryService } from './mcp-registry.service';\nimport { buildMcpCapabilities } from '../utils/capabilities-builder';\n\n@Injectable()\nexport class McpStreamableHttpService implements OnModuleDestroy {\n private readonly logger = new Logger(McpStreamableHttpService.name);\n\n // Session management for stateful mode\n private readonly transports: {\n [sessionId: string]: StreamableHTTPServerTransport;\n } = {};\n private readonly mcpServers: { [sessionId: string]: McpServer } = {};\n private readonly sessionExecutors: {\n [sessionId: string]: McpExecutorService;\n } = {};\n\n private readonly isStatelessMode: boolean;\n\n constructor(\n @Inject('MCP_OPTIONS') private readonly options: McpOptions,\n @Inject('MCP_MODULE_ID') private readonly mcpModuleId: string,\n private readonly moduleRef: ModuleRef,\n private readonly toolRegistry: McpRegistryService,\n ) {\n this.isStatelessMode = !!options.streamableHttp?.statelessMode;\n }\n\n /**\n * Creates a new MCP server instance with the shared registry\n */\n private createMcpServer(): McpServer {\n const capabilities = buildMcpCapabilities(\n this.mcpModuleId,\n this.toolRegistry,\n this.options,\n );\n\n this.logger.debug('Building MCP server with capabilities:', capabilities);\n\n return new McpServer(\n { name: this.options.name, version: this.options.version },\n {\n capabilities,\n instructions: this.options.instructions || '',\n },\n );\n }\n\n /**\n * Create a new MCP server instance for stateless requests\n */\n async createStatelessServer(rawReq: any): Promise<{\n server: McpServer;\n transport: StreamableHTTPServerTransport;\n executor: McpExecutorService;\n }> {\n // Create transport without session ID for true stateless mode\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse:\n this.options.streamableHttp?.enableJsonResponse || false,\n });\n\n // Create new MCP server instance\n const server = this.createMcpServer();\n\n // Connect transport to server\n await server.connect(transport);\n\n // Resolve request-scoped executor\n const contextId = ContextIdFactory.getByRequest(rawReq);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n\n // Register handlers for this request\n executor.registerRequestHandlers(server, rawReq);\n\n return { server, transport, executor };\n }\n\n /**\n * Handle POST requests\n */\n async handlePostRequest(req: any, res: any, body: unknown): Promise<void> {\n this.logger.debug('Received MCP POST request');\n\n // Get the appropriate HTTP adapter\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n try {\n if (this.isStatelessMode) {\n return this.handleStatelessRequest(adaptedReq, adaptedRes, body);\n } else {\n return this.handleStatefulRequest(adaptedReq, adaptedRes, body);\n }\n } catch (error) {\n this.logger.error('Error handling MCP request:', error);\n if (!adaptedRes.headersSent) {\n adaptedRes.status(500).json({\n jsonrpc: '2.0',\n error: {\n code: -32603,\n message: 'Internal server error',\n },\n id: null,\n });\n }\n }\n }\n\n /**\n * Handle requests in stateless mode\n */\n async handleStatelessRequest(\n req: HttpRequest,\n res: HttpResponse,\n body: unknown,\n ): Promise<void> {\n this.logger.debug('Handling stateless MCP request');\n\n let server: McpServer | null = null;\n let transport: StreamableHTTPServerTransport | null = null;\n\n try {\n // Create new server and transport for each request\n const stateless = await this.createStatelessServer(req);\n server = stateless.server;\n transport = stateless.transport;\n\n // Handle the request\n await transport.handleRequest(req.raw, res.raw, body);\n\n // Clean up after response is sent\n res.on?.('finish', async () => {\n this.logger.debug('Stateless response finished, cleaning up');\n await this.cleanupStatelessResources(server, transport);\n });\n } catch (error) {\n this.logger.error('Error in stateless request handling:', error);\n await this.cleanupStatelessResources(server, transport);\n throw error;\n }\n }\n\n /**\n * Clean up stateless resources\n */\n private async cleanupStatelessResources(\n server: McpServer | null,\n transport: StreamableHTTPServerTransport | null,\n ): Promise<void> {\n try {\n if (transport) {\n await transport.close();\n }\n if (server) {\n await server.close();\n }\n } catch (error) {\n this.logger.error('Error cleaning up stateless resources:', error);\n }\n }\n\n /**\n * Handle requests in stateful mode\n */\n async handleStatefulRequest(\n req: HttpRequest,\n res: HttpResponse,\n body: unknown,\n ): Promise<void> {\n this.logger.debug('Handling stateful MCP request');\n\n const sessionId = req.headers['mcp-session-id'] as string | undefined;\n\n // Case 1: New initialization request\n if (!sessionId && this.isInitializeRequest(body)) {\n // Validate single initialization request\n if (Array.isArray(body) && body.length > 1) {\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32600,\n message:\n 'Invalid Request: Only one initialization request is allowed',\n },\n id: null,\n });\n return;\n }\n\n // Create new transport with session management\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator:\n this.options.streamableHttp?.sessionIdGenerator ||\n (() => randomUUID()),\n enableJsonResponse:\n this.options.streamableHttp?.enableJsonResponse || false,\n onsessioninitialized: (sessionId: string) => {\n this.logger.log(`Session initialized: ${sessionId}`);\n // Store transport when session is initialized\n this.transports[sessionId] = transport;\n },\n onsessionclosed: async (sessionId: string) => {\n // Called by DELETE requests\n this.logger.log(`Session termination requested: ${sessionId}`);\n await this.cleanupSession(sessionId);\n },\n });\n\n // Create new MCP server for this session\n const mcpServer = this.createMcpServer();\n\n // Connect transport to server BEFORE handling request\n await mcpServer.connect(transport);\n\n // Resolve request-scoped executor\n const contextId = ContextIdFactory.getByRequest(req);\n const executor = await this.moduleRef.resolve(\n McpExecutorService,\n contextId,\n { strict: true },\n );\n\n // Register handlers ONCE during initialization\n executor.registerRequestHandlers(mcpServer, req);\n\n // Handle the initialization request\n await transport.handleRequest(req.raw, res.raw, body);\n\n // Store server and executor after successful initialization\n if (transport.sessionId) {\n this.mcpServers[transport.sessionId] = mcpServer;\n this.sessionExecutors[transport.sessionId] = executor;\n this.logger.log(`Session fully initialized: ${transport.sessionId}`);\n }\n\n return;\n }\n\n // Case 2: Existing session with session ID\n if (sessionId) {\n // Validate session exists\n if (!this.transports[sessionId]) {\n res.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n // Reject re-initialization attempts\n if (this.isInitializeRequest(body)) {\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32600,\n message: 'Invalid Request: Server already initialized',\n },\n id: null,\n });\n return;\n }\n\n // Use existing transport\n const transport = this.transports[sessionId];\n\n // Handle the request with existing transport\n await transport.handleRequest(req.raw, res.raw, body);\n\n return;\n }\n\n // Case 3: No session ID and not initialization\n res.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: No valid session ID provided',\n },\n id: null,\n });\n }\n\n /**\n * Handle GET requests for SSE streams\n */\n async handleGetRequest(req: any, res: any): Promise<void> {\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n if (this.isStatelessMode) {\n // Stateless mode doesn't support SSE\n adaptedRes.status(405).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed in stateless mode',\n },\n id: null,\n });\n return;\n }\n\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n if (!sessionId) {\n adaptedRes.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: Mcp-Session-Id header is required',\n },\n id: null,\n });\n return;\n }\n\n if (!this.transports[sessionId]) {\n adaptedRes.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n this.logger.debug(`Establishing SSE stream for session ${sessionId}`);\n const transport = this.transports[sessionId];\n\n // Let transport handle the GET request for SSE\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n }\n\n /**\n * Handle DELETE requests for terminating sessions\n */\n async handleDeleteRequest(req: any, res: any): Promise<void> {\n const adapter = HttpAdapterFactory.getAdapter(req, res);\n const adaptedReq = adapter.adaptRequest(req);\n const adaptedRes = adapter.adaptResponse(res);\n\n if (this.isStatelessMode) {\n adaptedRes.status(405).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Method not allowed in stateless mode',\n },\n id: null,\n });\n return;\n }\n\n const sessionId = adaptedReq.headers['mcp-session-id'] as\n | string\n | undefined;\n\n if (!sessionId) {\n adaptedRes.status(400).json({\n jsonrpc: '2.0',\n error: {\n code: -32000,\n message: 'Bad Request: Mcp-Session-Id header is required',\n },\n id: null,\n });\n return;\n }\n\n if (!this.transports[sessionId]) {\n adaptedRes.status(404).json({\n jsonrpc: '2.0',\n error: {\n code: -32001,\n message: 'Session not found',\n },\n id: null,\n });\n return;\n }\n\n this.logger.debug(`Processing DELETE request for session ${sessionId}`);\n const transport = this.transports[sessionId];\n\n // Let transport handle the DELETE request\n // The onsessionclosed callback will trigger cleanup\n await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);\n }\n\n /**\n * Helper to detect initialization requests\n */\n private isInitializeRequest(body: unknown): boolean {\n if (Array.isArray(body)) {\n return body.some(\n (msg) =>\n typeof msg === 'object' &&\n msg !== null &&\n 'method' in msg &&\n msg.method === 'initialize',\n );\n }\n return (\n typeof body === 'object' &&\n body !== null &&\n 'method' in body &&\n body.method === 'initialize'\n );\n }\n\n /**\n * Clean up session resources\n */\n private async cleanupSession(sessionId: string): Promise<void> {\n if (!sessionId) return;\n\n this.logger.debug(`Cleaning up session: ${sessionId}`);\n\n try {\n // Close transport if still open\n const transport = this.transports[sessionId];\n if (transport) {\n await transport.close();\n }\n delete this.transports[sessionId];\n\n // Close MCP server\n const server = this.mcpServers[sessionId];\n if (server) {\n await server.close();\n }\n delete this.mcpServers[sessionId];\n\n // Clean up executor\n delete this.sessionExecutors[sessionId];\n\n this.logger.log(`Session cleanup complete: ${sessionId}`);\n } catch (error) {\n this.logger.error(`Error cleaning up session ${sessionId}:`, error);\n }\n }\n\n /**\n * Clean up all sessions on module destroy\n */\n async onModuleDestroy(): Promise<void> {\n this.logger.log('Cleaning up all MCP sessions...');\n\n const sessionIds = Object.keys(this.transports);\n await Promise.all(\n sessionIds.map((sessionId) => this.cleanupSession(sessionId)),\n );\n\n this.logger.log('All MCP sessions cleaned up');\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
1
|
+
import { Inject, Injectable, Logger, OnModuleDestroy } from '@nestjs/common';
|
|
2
2
|
import { ContextIdFactory, ModuleRef } from '@nestjs/core';
|
|
3
3
|
import { randomUUID } from 'crypto';
|
|
4
4
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
@@ -14,12 +14,18 @@ import { McpRegistryService } from './mcp-registry.service';
|
|
|
14
14
|
import { buildMcpCapabilities } from '../utils/capabilities-builder';
|
|
15
15
|
|
|
16
16
|
@Injectable()
|
|
17
|
-
export class McpStreamableHttpService {
|
|
17
|
+
export class McpStreamableHttpService implements OnModuleDestroy {
|
|
18
18
|
private readonly logger = new Logger(McpStreamableHttpService.name);
|
|
19
|
+
|
|
20
|
+
// Session management for stateful mode
|
|
19
21
|
private readonly transports: {
|
|
20
22
|
[sessionId: string]: StreamableHTTPServerTransport;
|
|
21
23
|
} = {};
|
|
22
24
|
private readonly mcpServers: { [sessionId: string]: McpServer } = {};
|
|
25
|
+
private readonly sessionExecutors: {
|
|
26
|
+
[sessionId: string]: McpExecutorService;
|
|
27
|
+
} = {};
|
|
28
|
+
|
|
23
29
|
private readonly isStatelessMode: boolean;
|
|
24
30
|
|
|
25
31
|
constructor(
|
|
@@ -28,46 +34,52 @@ export class McpStreamableHttpService {
|
|
|
28
34
|
private readonly moduleRef: ModuleRef,
|
|
29
35
|
private readonly toolRegistry: McpRegistryService,
|
|
30
36
|
) {
|
|
31
|
-
// Determine if we're in stateless mode
|
|
32
37
|
this.isStatelessMode = !!options.streamableHttp?.statelessMode;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
/**
|
|
36
|
-
*
|
|
41
|
+
* Creates a new MCP server instance with the shared registry
|
|
37
42
|
*/
|
|
38
|
-
|
|
39
|
-
server: McpServer;
|
|
40
|
-
transport: StreamableHTTPServerTransport;
|
|
41
|
-
}> {
|
|
42
|
-
// Create a new transport for this request (stateless = no session management)
|
|
43
|
-
const transport = new StreamableHTTPServerTransport({
|
|
44
|
-
sessionIdGenerator: undefined,
|
|
45
|
-
enableJsonResponse:
|
|
46
|
-
this.options.streamableHttp?.enableJsonResponse || false,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Create a new MCP server instance with dynamic capabilities
|
|
43
|
+
private createMcpServer(): McpServer {
|
|
50
44
|
const capabilities = buildMcpCapabilities(
|
|
51
45
|
this.mcpModuleId,
|
|
52
46
|
this.toolRegistry,
|
|
53
47
|
this.options,
|
|
54
48
|
);
|
|
55
|
-
this.logger.debug(
|
|
56
|
-
`[Stateless] Built MCP capabilities: ${JSON.stringify(capabilities)}`,
|
|
57
|
-
);
|
|
58
49
|
|
|
59
|
-
|
|
50
|
+
this.logger.debug('Building MCP server with capabilities:', capabilities);
|
|
51
|
+
|
|
52
|
+
return new McpServer(
|
|
60
53
|
{ name: this.options.name, version: this.options.version },
|
|
61
54
|
{
|
|
62
|
-
capabilities
|
|
55
|
+
capabilities,
|
|
63
56
|
instructions: this.options.instructions || '',
|
|
64
57
|
},
|
|
65
58
|
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Create a new MCP server instance for stateless requests
|
|
63
|
+
*/
|
|
64
|
+
async createStatelessServer(rawReq: any): Promise<{
|
|
65
|
+
server: McpServer;
|
|
66
|
+
transport: StreamableHTTPServerTransport;
|
|
67
|
+
executor: McpExecutorService;
|
|
68
|
+
}> {
|
|
69
|
+
// Create transport without session ID for true stateless mode
|
|
70
|
+
const transport = new StreamableHTTPServerTransport({
|
|
71
|
+
sessionIdGenerator: undefined,
|
|
72
|
+
enableJsonResponse:
|
|
73
|
+
this.options.streamableHttp?.enableJsonResponse || false,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Create new MCP server instance
|
|
77
|
+
const server = this.createMcpServer();
|
|
66
78
|
|
|
67
|
-
// Connect
|
|
79
|
+
// Connect transport to server
|
|
68
80
|
await server.connect(transport);
|
|
69
81
|
|
|
70
|
-
//
|
|
82
|
+
// Resolve request-scoped executor
|
|
71
83
|
const contextId = ContextIdFactory.getByRequest(rawReq);
|
|
72
84
|
const executor = await this.moduleRef.resolve(
|
|
73
85
|
McpExecutorService,
|
|
@@ -75,30 +87,22 @@ export class McpStreamableHttpService {
|
|
|
75
87
|
{ strict: true },
|
|
76
88
|
);
|
|
77
89
|
|
|
78
|
-
// Register
|
|
79
|
-
this.logger.debug(
|
|
80
|
-
'[Stateless] Registering request handlers for stateless MCP server',
|
|
81
|
-
);
|
|
90
|
+
// Register handlers for this request
|
|
82
91
|
executor.registerRequestHandlers(server, rawReq);
|
|
83
92
|
|
|
84
|
-
return { server, transport };
|
|
93
|
+
return { server, transport, executor };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
96
|
/**
|
|
88
97
|
* Handle POST requests
|
|
89
98
|
*/
|
|
90
99
|
async handlePostRequest(req: any, res: any, body: unknown): Promise<void> {
|
|
91
|
-
|
|
100
|
+
this.logger.debug('Received MCP POST request');
|
|
101
|
+
|
|
102
|
+
// Get the appropriate HTTP adapter
|
|
92
103
|
const adapter = HttpAdapterFactory.getAdapter(req, res);
|
|
93
104
|
const adaptedReq = adapter.adaptRequest(req);
|
|
94
105
|
const adaptedRes = adapter.adaptResponse(res);
|
|
95
|
-
const sessionId = adaptedReq.headers['mcp-session-id'] as
|
|
96
|
-
| string
|
|
97
|
-
| undefined;
|
|
98
|
-
|
|
99
|
-
this.logger.debug(
|
|
100
|
-
`[${sessionId || 'No-Session'}] Received MCP request: ${JSON.stringify(body)}`,
|
|
101
|
-
);
|
|
102
106
|
|
|
103
107
|
try {
|
|
104
108
|
if (this.isStatelessMode) {
|
|
@@ -107,9 +111,7 @@ export class McpStreamableHttpService {
|
|
|
107
111
|
return this.handleStatefulRequest(adaptedReq, adaptedRes, body);
|
|
108
112
|
}
|
|
109
113
|
} catch (error) {
|
|
110
|
-
this.logger.error(
|
|
111
|
-
`[${sessionId || 'No-Session'}] Error handling MCP request: ${error}`,
|
|
112
|
-
);
|
|
114
|
+
this.logger.error('Error handling MCP request:', error);
|
|
113
115
|
if (!adaptedRes.headersSent) {
|
|
114
116
|
adaptedRes.status(500).json({
|
|
115
117
|
jsonrpc: '2.0',
|
|
@@ -127,19 +129,17 @@ export class McpStreamableHttpService {
|
|
|
127
129
|
* Handle requests in stateless mode
|
|
128
130
|
*/
|
|
129
131
|
async handleStatelessRequest(
|
|
130
|
-
req:
|
|
132
|
+
req: HttpRequest,
|
|
131
133
|
res: HttpResponse,
|
|
132
134
|
body: unknown,
|
|
133
135
|
): Promise<void> {
|
|
134
|
-
this.logger.debug(
|
|
135
|
-
`[Stateless] Handling stateless MCP request at ${req.url} with body: ${JSON.stringify(body)}`,
|
|
136
|
-
);
|
|
136
|
+
this.logger.debug('Handling stateless MCP request');
|
|
137
137
|
|
|
138
138
|
let server: McpServer | null = null;
|
|
139
139
|
let transport: StreamableHTTPServerTransport | null = null;
|
|
140
140
|
|
|
141
141
|
try {
|
|
142
|
-
// Create
|
|
142
|
+
// Create new server and transport for each request
|
|
143
143
|
const stateless = await this.createStatelessServer(req);
|
|
144
144
|
server = stateless.server;
|
|
145
145
|
transport = stateless.transport;
|
|
@@ -147,23 +147,37 @@ export class McpStreamableHttpService {
|
|
|
147
147
|
// Handle the request
|
|
148
148
|
await transport.handleRequest(req.raw, res.raw, body);
|
|
149
149
|
|
|
150
|
-
// Clean up
|
|
151
|
-
res.on?.('
|
|
152
|
-
this.logger.debug('
|
|
153
|
-
|
|
154
|
-
void server?.close();
|
|
150
|
+
// Clean up after response is sent
|
|
151
|
+
res.on?.('finish', async () => {
|
|
152
|
+
this.logger.debug('Stateless response finished, cleaning up');
|
|
153
|
+
await this.cleanupStatelessResources(server, transport);
|
|
155
154
|
});
|
|
156
155
|
} catch (error) {
|
|
157
|
-
this.logger.error(
|
|
158
|
-
|
|
159
|
-
);
|
|
160
|
-
// Clean up on error
|
|
161
|
-
void transport?.close();
|
|
162
|
-
void server?.close();
|
|
156
|
+
this.logger.error('Error in stateless request handling:', error);
|
|
157
|
+
await this.cleanupStatelessResources(server, transport);
|
|
163
158
|
throw error;
|
|
164
159
|
}
|
|
165
160
|
}
|
|
166
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Clean up stateless resources
|
|
164
|
+
*/
|
|
165
|
+
private async cleanupStatelessResources(
|
|
166
|
+
server: McpServer | null,
|
|
167
|
+
transport: StreamableHTTPServerTransport | null,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
try {
|
|
170
|
+
if (transport) {
|
|
171
|
+
await transport.close();
|
|
172
|
+
}
|
|
173
|
+
if (server) {
|
|
174
|
+
await server.close();
|
|
175
|
+
}
|
|
176
|
+
} catch (error) {
|
|
177
|
+
this.logger.error('Error cleaning up stateless resources:', error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
167
181
|
/**
|
|
168
182
|
* Handle requests in stateful mode
|
|
169
183
|
*/
|
|
@@ -172,142 +186,133 @@ export class McpStreamableHttpService {
|
|
|
172
186
|
res: HttpResponse,
|
|
173
187
|
body: unknown,
|
|
174
188
|
): Promise<void> {
|
|
175
|
-
this.logger.debug(
|
|
176
|
-
`[Stateful] Handling stateful MCP request at ${req.url} with body: ${JSON.stringify(body)}`,
|
|
177
|
-
);
|
|
189
|
+
this.logger.debug('Handling stateful MCP request');
|
|
178
190
|
|
|
179
|
-
// Check for existing session ID
|
|
180
191
|
const sessionId = req.headers['mcp-session-id'] as string | undefined;
|
|
181
|
-
let transport: StreamableHTTPServerTransport;
|
|
182
|
-
|
|
183
|
-
if (sessionId && this.transports[sessionId]) {
|
|
184
|
-
// Reuse existing transport
|
|
185
|
-
transport = this.transports[sessionId];
|
|
186
|
-
} else if (!sessionId && this.isInitializeRequest(body)) {
|
|
187
|
-
// Build capabilities and create the MCP server first so the init callback can capture it
|
|
188
|
-
const capabilities = buildMcpCapabilities(
|
|
189
|
-
this.mcpModuleId,
|
|
190
|
-
this.toolRegistry,
|
|
191
|
-
this.options,
|
|
192
|
-
);
|
|
193
|
-
this.logger.debug(
|
|
194
|
-
`[${sessionId || 'New-Session'}] Built MCP capabilities: ${JSON.stringify(capabilities)}`,
|
|
195
|
-
);
|
|
196
192
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
193
|
+
// Case 1: New initialization request
|
|
194
|
+
if (!sessionId && this.isInitializeRequest(body)) {
|
|
195
|
+
// Validate single initialization request
|
|
196
|
+
if (Array.isArray(body) && body.length > 1) {
|
|
197
|
+
res.status(400).json({
|
|
198
|
+
jsonrpc: '2.0',
|
|
199
|
+
error: {
|
|
200
|
+
code: -32600,
|
|
201
|
+
message:
|
|
202
|
+
'Invalid Request: Only one initialization request is allowed',
|
|
203
|
+
},
|
|
204
|
+
id: null,
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
204
208
|
|
|
205
|
-
// Create
|
|
206
|
-
transport = new StreamableHTTPServerTransport({
|
|
209
|
+
// Create new transport with session management
|
|
210
|
+
const transport = new StreamableHTTPServerTransport({
|
|
207
211
|
sessionIdGenerator:
|
|
208
212
|
this.options.streamableHttp?.sessionIdGenerator ||
|
|
209
213
|
(() => randomUUID()),
|
|
210
214
|
enableJsonResponse:
|
|
211
215
|
this.options.streamableHttp?.enableJsonResponse || false,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
this.transports[
|
|
216
|
-
|
|
216
|
+
onsessioninitialized: (sessionId: string) => {
|
|
217
|
+
this.logger.log(`Session initialized: ${sessionId}`);
|
|
218
|
+
// Store transport when session is initialized
|
|
219
|
+
this.transports[sessionId] = transport;
|
|
220
|
+
},
|
|
221
|
+
onsessionclosed: async (sessionId: string) => {
|
|
222
|
+
// Called by DELETE requests
|
|
223
|
+
this.logger.log(`Session termination requested: ${sessionId}`);
|
|
224
|
+
await this.cleanupSession(sessionId);
|
|
217
225
|
},
|
|
218
226
|
});
|
|
219
227
|
|
|
220
|
-
//
|
|
221
|
-
|
|
222
|
-
if (transport.sessionId) {
|
|
223
|
-
this.cleanupSession(transport.sessionId);
|
|
224
|
-
}
|
|
225
|
-
};
|
|
228
|
+
// Create new MCP server for this session
|
|
229
|
+
const mcpServer = this.createMcpServer();
|
|
226
230
|
|
|
227
|
-
// Connect
|
|
231
|
+
// Connect transport to server BEFORE handling request
|
|
228
232
|
await mcpServer.connect(transport);
|
|
229
233
|
|
|
230
|
-
//
|
|
234
|
+
// Resolve request-scoped executor
|
|
231
235
|
const contextId = ContextIdFactory.getByRequest(req);
|
|
232
236
|
const executor = await this.moduleRef.resolve(
|
|
233
237
|
McpExecutorService,
|
|
234
238
|
contextId,
|
|
235
239
|
{ strict: true },
|
|
236
240
|
);
|
|
241
|
+
|
|
242
|
+
// Register handlers ONCE during initialization
|
|
237
243
|
executor.registerRequestHandlers(mcpServer, req);
|
|
238
244
|
|
|
239
245
|
// Handle the initialization request
|
|
240
246
|
await transport.handleRequest(req.raw, res.raw, body);
|
|
241
247
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
error: {
|
|
249
|
-
code: -32001,
|
|
250
|
-
message: 'Session not found',
|
|
251
|
-
},
|
|
252
|
-
id: null,
|
|
253
|
-
});
|
|
254
|
-
return;
|
|
255
|
-
} else {
|
|
256
|
-
// Invalid request - no session ID or not initialization request
|
|
257
|
-
// Match SDK wording for missing session id after init
|
|
258
|
-
res.status(400).json({
|
|
259
|
-
jsonrpc: '2.0',
|
|
260
|
-
error: {
|
|
261
|
-
code: -32000,
|
|
262
|
-
message: 'Bad Request: Mcp-Session-Id header is required',
|
|
263
|
-
},
|
|
264
|
-
id: null,
|
|
265
|
-
});
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
248
|
+
// Store server and executor after successful initialization
|
|
249
|
+
if (transport.sessionId) {
|
|
250
|
+
this.mcpServers[transport.sessionId] = mcpServer;
|
|
251
|
+
this.sessionExecutors[transport.sessionId] = executor;
|
|
252
|
+
this.logger.log(`Session fully initialized: ${transport.sessionId}`);
|
|
253
|
+
}
|
|
268
254
|
|
|
269
|
-
// ---- Subsequent requests to an existing session ----
|
|
270
|
-
const mcpServer = this.mcpServers[sessionId];
|
|
271
|
-
if (!mcpServer) {
|
|
272
|
-
this.logger.debug(`[${sessionId}] Session not found`);
|
|
273
|
-
res.status(404).json({
|
|
274
|
-
jsonrpc: '2.0',
|
|
275
|
-
error: {
|
|
276
|
-
code: -32001,
|
|
277
|
-
message: 'Session not found',
|
|
278
|
-
},
|
|
279
|
-
id: null,
|
|
280
|
-
});
|
|
281
255
|
return;
|
|
282
256
|
}
|
|
283
257
|
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
258
|
+
// Case 2: Existing session with session ID
|
|
259
|
+
if (sessionId) {
|
|
260
|
+
// Validate session exists
|
|
261
|
+
if (!this.transports[sessionId]) {
|
|
262
|
+
res.status(404).json({
|
|
263
|
+
jsonrpc: '2.0',
|
|
264
|
+
error: {
|
|
265
|
+
code: -32001,
|
|
266
|
+
message: 'Session not found',
|
|
267
|
+
},
|
|
268
|
+
id: null,
|
|
269
|
+
});
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Reject re-initialization attempts
|
|
274
|
+
if (this.isInitializeRequest(body)) {
|
|
275
|
+
res.status(400).json({
|
|
276
|
+
jsonrpc: '2.0',
|
|
277
|
+
error: {
|
|
278
|
+
code: -32600,
|
|
279
|
+
message: 'Invalid Request: Server already initialized',
|
|
280
|
+
},
|
|
281
|
+
id: null,
|
|
282
|
+
});
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Use existing transport
|
|
287
|
+
const transport = this.transports[sessionId];
|
|
291
288
|
|
|
292
|
-
|
|
293
|
-
|
|
289
|
+
// Handle the request with existing transport
|
|
290
|
+
await transport.handleRequest(req.raw, res.raw, body);
|
|
294
291
|
|
|
295
|
-
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
296
294
|
|
|
297
|
-
//
|
|
298
|
-
|
|
295
|
+
// Case 3: No session ID and not initialization
|
|
296
|
+
res.status(400).json({
|
|
297
|
+
jsonrpc: '2.0',
|
|
298
|
+
error: {
|
|
299
|
+
code: -32000,
|
|
300
|
+
message: 'Bad Request: No valid session ID provided',
|
|
301
|
+
},
|
|
302
|
+
id: null,
|
|
303
|
+
});
|
|
299
304
|
}
|
|
300
305
|
|
|
301
306
|
/**
|
|
302
307
|
* Handle GET requests for SSE streams
|
|
303
308
|
*/
|
|
304
309
|
async handleGetRequest(req: any, res: any): Promise<void> {
|
|
305
|
-
// Get the appropriate HTTP adapter for the request/response
|
|
306
310
|
const adapter = HttpAdapterFactory.getAdapter(req, res);
|
|
307
311
|
const adaptedReq = adapter.adaptRequest(req);
|
|
308
312
|
const adaptedRes = adapter.adaptResponse(res);
|
|
309
313
|
|
|
310
314
|
if (this.isStatelessMode) {
|
|
315
|
+
// Stateless mode doesn't support SSE
|
|
311
316
|
adaptedRes.status(405).json({
|
|
312
317
|
jsonrpc: '2.0',
|
|
313
318
|
error: {
|
|
@@ -323,18 +328,34 @@ export class McpStreamableHttpService {
|
|
|
323
328
|
| string
|
|
324
329
|
| undefined;
|
|
325
330
|
|
|
326
|
-
if (!sessionId
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
331
|
+
if (!sessionId) {
|
|
332
|
+
adaptedRes.status(400).json({
|
|
333
|
+
jsonrpc: '2.0',
|
|
334
|
+
error: {
|
|
335
|
+
code: -32000,
|
|
336
|
+
message: 'Bad Request: Mcp-Session-Id header is required',
|
|
337
|
+
},
|
|
338
|
+
id: null,
|
|
339
|
+
});
|
|
333
340
|
return;
|
|
334
341
|
}
|
|
335
342
|
|
|
336
|
-
this.
|
|
343
|
+
if (!this.transports[sessionId]) {
|
|
344
|
+
adaptedRes.status(404).json({
|
|
345
|
+
jsonrpc: '2.0',
|
|
346
|
+
error: {
|
|
347
|
+
code: -32001,
|
|
348
|
+
message: 'Session not found',
|
|
349
|
+
},
|
|
350
|
+
id: null,
|
|
351
|
+
});
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
this.logger.debug(`Establishing SSE stream for session ${sessionId}`);
|
|
337
356
|
const transport = this.transports[sessionId];
|
|
357
|
+
|
|
358
|
+
// Let transport handle the GET request for SSE
|
|
338
359
|
await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
|
|
339
360
|
}
|
|
340
361
|
|
|
@@ -342,7 +363,6 @@ export class McpStreamableHttpService {
|
|
|
342
363
|
* Handle DELETE requests for terminating sessions
|
|
343
364
|
*/
|
|
344
365
|
async handleDeleteRequest(req: any, res: any): Promise<void> {
|
|
345
|
-
// Get the appropriate HTTP adapter for the request/response
|
|
346
366
|
const adapter = HttpAdapterFactory.getAdapter(req, res);
|
|
347
367
|
const adaptedReq = adapter.adaptRequest(req);
|
|
348
368
|
const adaptedRes = adapter.adaptResponse(res);
|
|
@@ -363,24 +383,42 @@ export class McpStreamableHttpService {
|
|
|
363
383
|
| string
|
|
364
384
|
| undefined;
|
|
365
385
|
|
|
366
|
-
if (!sessionId
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
386
|
+
if (!sessionId) {
|
|
387
|
+
adaptedRes.status(400).json({
|
|
388
|
+
jsonrpc: '2.0',
|
|
389
|
+
error: {
|
|
390
|
+
code: -32000,
|
|
391
|
+
message: 'Bad Request: Mcp-Session-Id header is required',
|
|
392
|
+
},
|
|
393
|
+
id: null,
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (!this.transports[sessionId]) {
|
|
399
|
+
adaptedRes.status(404).json({
|
|
400
|
+
jsonrpc: '2.0',
|
|
401
|
+
error: {
|
|
402
|
+
code: -32001,
|
|
403
|
+
message: 'Session not found',
|
|
404
|
+
},
|
|
405
|
+
id: null,
|
|
406
|
+
});
|
|
371
407
|
return;
|
|
372
408
|
}
|
|
373
409
|
|
|
374
|
-
this.logger.debug(`
|
|
410
|
+
this.logger.debug(`Processing DELETE request for session ${sessionId}`);
|
|
375
411
|
const transport = this.transports[sessionId];
|
|
376
412
|
|
|
377
|
-
// Let
|
|
413
|
+
// Let transport handle the DELETE request
|
|
414
|
+
// The onsessionclosed callback will trigger cleanup
|
|
378
415
|
await transport.handleRequest(adaptedReq.raw, adaptedRes.raw);
|
|
379
|
-
// DO NOT call this.cleanupSession(sessionId) here to avoid double cleanup.
|
|
380
416
|
}
|
|
381
417
|
|
|
382
|
-
|
|
383
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Helper to detect initialization requests
|
|
420
|
+
*/
|
|
421
|
+
private isInitializeRequest(body: unknown): boolean {
|
|
384
422
|
if (Array.isArray(body)) {
|
|
385
423
|
return body.some(
|
|
386
424
|
(msg) =>
|
|
@@ -394,16 +432,53 @@ export class McpStreamableHttpService {
|
|
|
394
432
|
typeof body === 'object' &&
|
|
395
433
|
body !== null &&
|
|
396
434
|
'method' in body &&
|
|
397
|
-
|
|
435
|
+
body.method === 'initialize'
|
|
398
436
|
);
|
|
399
437
|
}
|
|
400
438
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
439
|
+
/**
|
|
440
|
+
* Clean up session resources
|
|
441
|
+
*/
|
|
442
|
+
private async cleanupSession(sessionId: string): Promise<void> {
|
|
443
|
+
if (!sessionId) return;
|
|
444
|
+
|
|
445
|
+
this.logger.debug(`Cleaning up session: ${sessionId}`);
|
|
446
|
+
|
|
447
|
+
try {
|
|
448
|
+
// Close transport if still open
|
|
449
|
+
const transport = this.transports[sessionId];
|
|
450
|
+
if (transport) {
|
|
451
|
+
await transport.close();
|
|
452
|
+
}
|
|
405
453
|
delete this.transports[sessionId];
|
|
454
|
+
|
|
455
|
+
// Close MCP server
|
|
456
|
+
const server = this.mcpServers[sessionId];
|
|
457
|
+
if (server) {
|
|
458
|
+
await server.close();
|
|
459
|
+
}
|
|
406
460
|
delete this.mcpServers[sessionId];
|
|
461
|
+
|
|
462
|
+
// Clean up executor
|
|
463
|
+
delete this.sessionExecutors[sessionId];
|
|
464
|
+
|
|
465
|
+
this.logger.log(`Session cleanup complete: ${sessionId}`);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
this.logger.error(`Error cleaning up session ${sessionId}:`, error);
|
|
407
468
|
}
|
|
408
469
|
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Clean up all sessions on module destroy
|
|
473
|
+
*/
|
|
474
|
+
async onModuleDestroy(): Promise<void> {
|
|
475
|
+
this.logger.log('Cleaning up all MCP sessions...');
|
|
476
|
+
|
|
477
|
+
const sessionIds = Object.keys(this.transports);
|
|
478
|
+
await Promise.all(
|
|
479
|
+
sessionIds.map((sessionId) => this.cleanupSession(sessionId)),
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
this.logger.log('All MCP sessions cleaned up');
|
|
483
|
+
}
|
|
409
484
|
}
|