@tmcp/transport-http 0.5.3 → 0.5.5
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/README.md +42 -7
- package/package.json +7 -3
- package/src/index.js +142 -44
- package/src/types/index.d.ts +11 -0
- package/src/types/index.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -81,6 +81,38 @@ const transport = new HttpTransport(server, {
|
|
|
81
81
|
});
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
+
### Session Management
|
|
85
|
+
|
|
86
|
+
The HTTP transport supports custom session managers for different deployment scenarios:
|
|
87
|
+
|
|
88
|
+
#### In-Memory Sessions (Default)
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
import { InMemorySessionManager } from '@tmcp/session-manager';
|
|
92
|
+
|
|
93
|
+
const transport = new HttpTransport(server, {
|
|
94
|
+
sessionManager: new InMemorySessionManager(), // Default behavior
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Redis Sessions (Multi-Server/Serverless)
|
|
99
|
+
|
|
100
|
+
For deployments across multiple servers or serverless environments where sessions need to be shared:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
import { RedisSessionManager } from '@tmcp/session-manager-redis';
|
|
104
|
+
|
|
105
|
+
const transport = new HttpTransport(server, {
|
|
106
|
+
sessionManager: new RedisSessionManager('redis://localhost:6379'),
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**When to use Redis sessions:**
|
|
111
|
+
|
|
112
|
+
- **Multi-server deployments**: When your application runs on multiple servers and clients might connect to different instances
|
|
113
|
+
- **Serverless deployments**: When your transport is deployed on serverless platforms where instances are ephemeral (attention, serverless environment generally kills SSE request after a not-so-long amount of time, this means notification from the server will not reach the client after the shutdown)
|
|
114
|
+
- **Load balancing**: When using load balancers that might route requests to different server instances
|
|
115
|
+
|
|
84
116
|
## Features
|
|
85
117
|
|
|
86
118
|
- **🌐 HTTP/SSE Communication**: Uses Server-Sent Events for real-time bidirectional communication
|
|
@@ -116,6 +148,7 @@ interface HttpTransportOptions {
|
|
|
116
148
|
getSessionId: () => string; // Custom session ID generator
|
|
117
149
|
path?: string; // MCP endpoint path (default: '/mcp')
|
|
118
150
|
oauth?: OAuth; // an oauth provider generated from @tmcp/auth
|
|
151
|
+
sessionManager?: SessionManager; // Custom session manager (default: InMemorySessionManager)
|
|
119
152
|
}
|
|
120
153
|
```
|
|
121
154
|
|
|
@@ -262,7 +295,7 @@ Deno.serve({ port: 3000 }, async (req) => {
|
|
|
262
295
|
|
|
263
296
|
### `srvx`
|
|
264
297
|
|
|
265
|
-
If you want
|
|
298
|
+
If you want the same experience across Deno, Bun, and Node.js, you can use [srvx](https://srvx.h3.dev/).
|
|
266
299
|
|
|
267
300
|
```js
|
|
268
301
|
import { McpServer } from 'tmcp';
|
|
@@ -272,12 +305,14 @@ import { serve } from 'srvx';
|
|
|
272
305
|
const server = new McpServer(/* ... */);
|
|
273
306
|
const transport = new HttpTransport(server);
|
|
274
307
|
|
|
275
|
-
serve(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
308
|
+
serve({
|
|
309
|
+
async fetch(req) {
|
|
310
|
+
const response = await transport.respond(req);
|
|
311
|
+
if (response === null) {
|
|
312
|
+
return new Response('Not Found', { status: 404 });
|
|
313
|
+
}
|
|
314
|
+
return response;
|
|
315
|
+
},
|
|
281
316
|
});
|
|
282
317
|
```
|
|
283
318
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmcp/transport-http",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Transport for TMCP using HTTP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -28,15 +28,19 @@
|
|
|
28
28
|
"url": "git+https://github.com/paoloricciuti/tmcp.git",
|
|
29
29
|
"directory": "packages/transport-http"
|
|
30
30
|
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@tmcp/session-manager": "^0.1.0"
|
|
33
|
+
},
|
|
31
34
|
"devDependencies": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
35
|
+
"@modelcontextprotocol/sdk": "^1.17.1",
|
|
36
|
+
"@remix-run/node-fetch-server": "^0.8.0",
|
|
33
37
|
"@types/node": "^24.0.15",
|
|
34
38
|
"dts-buddy": "^0.6.2",
|
|
35
39
|
"publint": "^0.3.12",
|
|
36
40
|
"valibot": "^1.1.0",
|
|
37
41
|
"vitest": "^3.2.4",
|
|
38
|
-
"tmcp": "^1.10.2",
|
|
39
42
|
"@tmcp/adapter-valibot": "^0.1.4",
|
|
43
|
+
"tmcp": "^1.10.2",
|
|
40
44
|
"@tmcp/auth": "^0.3.3"
|
|
41
45
|
},
|
|
42
46
|
"publishConfig": {
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @import { AuthInfo, McpServer } from "tmcp";
|
|
3
3
|
* @import { OAuth } from "@tmcp/auth";
|
|
4
|
+
* @import { SessionManager } from "@tmcp/session-manager";
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {{
|
|
9
|
+
* origin?: string | string[] | boolean
|
|
10
|
+
* methods?: string[]
|
|
11
|
+
* allowedHeaders?: string[]
|
|
12
|
+
* exposedHeaders?: string[]
|
|
13
|
+
* credentials?: boolean
|
|
14
|
+
* maxAge?: number
|
|
15
|
+
* }} CorsConfig
|
|
4
16
|
*/
|
|
5
17
|
|
|
6
18
|
/**
|
|
@@ -8,10 +20,13 @@
|
|
|
8
20
|
* getSessionId?: () => string
|
|
9
21
|
* path?: string
|
|
10
22
|
* oauth?: OAuth<"built">
|
|
23
|
+
* cors?: CorsConfig | boolean,
|
|
24
|
+
* sessionManager?: SessionManager
|
|
11
25
|
* }} HttpTransportOptions
|
|
12
26
|
*/
|
|
13
27
|
|
|
14
28
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
29
|
+
import { InMemorySessionManager } from '@tmcp/session-manager';
|
|
15
30
|
export class HttpTransport {
|
|
16
31
|
/**
|
|
17
32
|
* @type {McpServer<any>}
|
|
@@ -19,7 +34,7 @@ export class HttpTransport {
|
|
|
19
34
|
#server;
|
|
20
35
|
|
|
21
36
|
/**
|
|
22
|
-
* @type {Required<Omit<HttpTransportOptions, 'oauth'>>}
|
|
37
|
+
* @type {Required<Omit<HttpTransportOptions, 'oauth' | 'cors'>> & { cors?: CorsConfig | boolean }}
|
|
23
38
|
*/
|
|
24
39
|
#options;
|
|
25
40
|
|
|
@@ -28,15 +43,6 @@ export class HttpTransport {
|
|
|
28
43
|
*/
|
|
29
44
|
#path;
|
|
30
45
|
|
|
31
|
-
/**
|
|
32
|
-
* @type {Map<string, ReadableStreamDefaultController>}
|
|
33
|
-
*/
|
|
34
|
-
#session = new Map();
|
|
35
|
-
/**
|
|
36
|
-
* @type {Map<string, ReadableStream>}
|
|
37
|
-
*/
|
|
38
|
-
#streams = new Map();
|
|
39
|
-
|
|
40
46
|
/**
|
|
41
47
|
* @type {AsyncLocalStorage<ReadableStreamDefaultController | undefined>}
|
|
42
48
|
*/
|
|
@@ -60,6 +66,8 @@ export class HttpTransport {
|
|
|
60
66
|
getSessionId = () => crypto.randomUUID(),
|
|
61
67
|
path = '/mcp',
|
|
62
68
|
oauth,
|
|
69
|
+
cors,
|
|
70
|
+
sessionManager = new InMemorySessionManager(),
|
|
63
71
|
} = options ?? {
|
|
64
72
|
getSessionId: () => crypto.randomUUID(),
|
|
65
73
|
};
|
|
@@ -68,7 +76,7 @@ export class HttpTransport {
|
|
|
68
76
|
this.#oauth = oauth;
|
|
69
77
|
}
|
|
70
78
|
|
|
71
|
-
this.#options = { getSessionId, path };
|
|
79
|
+
this.#options = { getSessionId, path, cors, sessionManager };
|
|
72
80
|
this.#path = path;
|
|
73
81
|
this.#server.on('send', ({ request, context: { sessions } }) => {
|
|
74
82
|
// use the current controller if the request has an id (it means it's a request and not a notification)
|
|
@@ -83,28 +91,101 @@ export class HttpTransport {
|
|
|
83
91
|
);
|
|
84
92
|
return;
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
this.#options.sessionManager.send(
|
|
95
|
+
sessions,
|
|
96
|
+
'data: ' + JSON.stringify(request) + '\n\n',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Applies CORS headers to a response based on the configuration
|
|
103
|
+
* @param {Response} response - The response to modify
|
|
104
|
+
* @param {Request} request - The original request
|
|
105
|
+
*/
|
|
106
|
+
#apply_cors_headers(response, request) {
|
|
107
|
+
const cors_config = this.#options.cors;
|
|
108
|
+
if (!cors_config) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Handle boolean true (allow all origins)
|
|
113
|
+
if (cors_config === true) {
|
|
114
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
115
|
+
response.headers.set(
|
|
116
|
+
'Access-Control-Allow-Methods',
|
|
117
|
+
'GET, POST, DELETE, OPTIONS',
|
|
118
|
+
);
|
|
119
|
+
response.headers.set('Access-Control-Allow-Headers', '*');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle detailed configuration
|
|
124
|
+
const config = /** @type {CorsConfig} */ (cors_config);
|
|
125
|
+
const origin = request.headers.get('origin');
|
|
126
|
+
|
|
127
|
+
// Handle origin
|
|
128
|
+
if (config.origin !== undefined) {
|
|
129
|
+
if (config.origin === true || config.origin === '*') {
|
|
130
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
131
|
+
} else if (typeof config.origin === 'string') {
|
|
132
|
+
if (origin === config.origin) {
|
|
133
|
+
response.headers.set(
|
|
134
|
+
'Access-Control-Allow-Origin',
|
|
135
|
+
config.origin,
|
|
92
136
|
);
|
|
93
137
|
}
|
|
138
|
+
} else if (Array.isArray(config.origin)) {
|
|
139
|
+
if (origin && config.origin.includes(origin)) {
|
|
140
|
+
response.headers.set('Access-Control-Allow-Origin', origin);
|
|
141
|
+
}
|
|
94
142
|
}
|
|
95
|
-
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Handle other CORS headers with defaults
|
|
146
|
+
const methods = config.methods ?? ['GET', 'POST', 'DELETE', 'OPTIONS'];
|
|
147
|
+
response.headers.set(
|
|
148
|
+
'Access-Control-Allow-Methods',
|
|
149
|
+
methods.join(', '),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const allowed_headers = config.allowedHeaders ?? '*';
|
|
153
|
+
if (Array.isArray(allowed_headers)) {
|
|
154
|
+
response.headers.set(
|
|
155
|
+
'Access-Control-Allow-Headers',
|
|
156
|
+
allowed_headers.join(', '),
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
response.headers.set(
|
|
160
|
+
'Access-Control-Allow-Headers',
|
|
161
|
+
allowed_headers,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (config.exposedHeaders) {
|
|
166
|
+
response.headers.set(
|
|
167
|
+
'Access-Control-Expose-Headers',
|
|
168
|
+
config.exposedHeaders.join(', '),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (config.credentials) {
|
|
173
|
+
response.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (config.maxAge !== undefined) {
|
|
177
|
+
response.headers.set(
|
|
178
|
+
'Access-Control-Max-Age',
|
|
179
|
+
config.maxAge.toString(),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
96
182
|
}
|
|
97
183
|
|
|
98
184
|
/**
|
|
99
185
|
* @param {string} session_id
|
|
100
186
|
*/
|
|
101
187
|
#handle_delete(session_id) {
|
|
102
|
-
|
|
103
|
-
if (controller) {
|
|
104
|
-
controller.close();
|
|
105
|
-
this.#session.delete(session_id);
|
|
106
|
-
this.#streams.delete(session_id);
|
|
107
|
-
}
|
|
188
|
+
this.#options.sessionManager.delete(session_id);
|
|
108
189
|
return new Response(null, {
|
|
109
190
|
status: 204,
|
|
110
191
|
headers: {
|
|
@@ -118,12 +199,11 @@ export class HttpTransport {
|
|
|
118
199
|
* @param {string} session_id
|
|
119
200
|
* @returns
|
|
120
201
|
*/
|
|
121
|
-
#handle_get(session_id) {
|
|
122
|
-
const sessions = this.#
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (existing_stream) {
|
|
202
|
+
async #handle_get(session_id) {
|
|
203
|
+
const sessions = this.#options.sessionManager;
|
|
204
|
+
// If session already exists, return error
|
|
205
|
+
const existing_session = await sessions.has(session_id);
|
|
206
|
+
if (existing_session) {
|
|
127
207
|
return new Response(
|
|
128
208
|
JSON.stringify({
|
|
129
209
|
jsonrpc: '2.0',
|
|
@@ -147,15 +227,13 @@ export class HttpTransport {
|
|
|
147
227
|
// Create new long-lived stream for notifications
|
|
148
228
|
const stream = new ReadableStream({
|
|
149
229
|
start(controller) {
|
|
150
|
-
sessions.
|
|
230
|
+
sessions.create(session_id, controller);
|
|
151
231
|
},
|
|
152
232
|
cancel() {
|
|
153
233
|
sessions.delete(session_id);
|
|
154
|
-
streams.delete(session_id);
|
|
155
234
|
},
|
|
156
235
|
});
|
|
157
236
|
|
|
158
|
-
streams.set(session_id, stream);
|
|
159
237
|
return new Response(stream, {
|
|
160
238
|
headers: {
|
|
161
239
|
'Content-Type': 'text/event-stream',
|
|
@@ -288,7 +366,7 @@ export class HttpTransport {
|
|
|
288
366
|
status: 405,
|
|
289
367
|
headers: {
|
|
290
368
|
'Content-Type': 'application/json',
|
|
291
|
-
Allow: 'GET, POST, DELETE',
|
|
369
|
+
Allow: 'GET, POST, DELETE, OPTIONS',
|
|
292
370
|
},
|
|
293
371
|
},
|
|
294
372
|
);
|
|
@@ -339,22 +417,42 @@ export class HttpTransport {
|
|
|
339
417
|
request.headers.get('mcp-session-id') ||
|
|
340
418
|
this.#options.getSessionId();
|
|
341
419
|
|
|
420
|
+
/**
|
|
421
|
+
* @type {Response | null}
|
|
422
|
+
*/
|
|
423
|
+
let response = null;
|
|
424
|
+
|
|
425
|
+
// Handle OPTIONS request - preflight CORS
|
|
426
|
+
if (method === 'OPTIONS') {
|
|
427
|
+
response = new Response(null, {
|
|
428
|
+
status: 204,
|
|
429
|
+
headers: {
|
|
430
|
+
'Content-Type': 'application/json',
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
}
|
|
342
434
|
// Handle DELETE request - disconnect session
|
|
343
|
-
if (method === 'DELETE') {
|
|
344
|
-
|
|
435
|
+
else if (method === 'DELETE') {
|
|
436
|
+
response = this.#handle_delete(session_id);
|
|
345
437
|
}
|
|
346
|
-
|
|
347
438
|
// Handle GET request - establish long-lived connection for notifications
|
|
348
|
-
if (method === 'GET') {
|
|
349
|
-
|
|
439
|
+
else if (method === 'GET') {
|
|
440
|
+
response = await this.#handle_get(session_id);
|
|
350
441
|
}
|
|
351
|
-
|
|
352
442
|
// Handle POST request - process message and respond through event stream
|
|
353
|
-
if (method === 'POST') {
|
|
354
|
-
|
|
443
|
+
else if (method === 'POST') {
|
|
444
|
+
response = await this.#handle_post(session_id, request, auth_info);
|
|
355
445
|
}
|
|
356
|
-
|
|
357
446
|
// Method not supported
|
|
358
|
-
|
|
447
|
+
else {
|
|
448
|
+
response = this.#handle_default(method);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Apply CORS headers if we have a response
|
|
452
|
+
if (response) {
|
|
453
|
+
this.#apply_cors_headers(response, request);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return response;
|
|
359
457
|
}
|
|
360
458
|
}
|
package/src/types/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
declare module '@tmcp/transport-http' {
|
|
2
2
|
import type { McpServer } from 'tmcp';
|
|
3
3
|
import type { OAuth } from '@tmcp/auth';
|
|
4
|
+
import type { SessionManager } from '@tmcp/session-manager';
|
|
4
5
|
export class HttpTransport {
|
|
5
6
|
|
|
6
7
|
constructor(server: McpServer<any>, options?: HttpTransportOptions);
|
|
@@ -8,10 +9,20 @@ declare module '@tmcp/transport-http' {
|
|
|
8
9
|
respond(request: Request): Promise<Response | null>;
|
|
9
10
|
#private;
|
|
10
11
|
}
|
|
12
|
+
export type CorsConfig = {
|
|
13
|
+
origin?: string | string[] | boolean;
|
|
14
|
+
methods?: string[];
|
|
15
|
+
allowedHeaders?: string[];
|
|
16
|
+
exposedHeaders?: string[];
|
|
17
|
+
credentials?: boolean;
|
|
18
|
+
maxAge?: number;
|
|
19
|
+
};
|
|
11
20
|
export type HttpTransportOptions = {
|
|
12
21
|
getSessionId?: () => string;
|
|
13
22
|
path?: string;
|
|
14
23
|
oauth?: OAuth<"built">;
|
|
24
|
+
cors?: CorsConfig | boolean;
|
|
25
|
+
sessionManager?: SessionManager;
|
|
15
26
|
};
|
|
16
27
|
|
|
17
28
|
export {};
|