@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 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 to have the same experience throughout Deno, Bun or Node you can also use `srvx`
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(async (req) => {
276
- const response = await transport.respond(req);
277
- if (response === null) {
278
- return new Response('Not Found', { status: 404 });
279
- }
280
- return response;
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",
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.16.0",
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
- for (let [session_id, controller] of this.#session.entries()) {
87
- if (sessions === undefined || sessions.includes(session_id)) {
88
- controller.enqueue(
89
- this.#text_encoder.encode(
90
- 'data: ' + JSON.stringify(request) + '\n\n',
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
- const controller = this.#session.get(session_id);
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.#session;
123
- const streams = this.#streams;
124
- // If session already exists, return existing stream
125
- const existing_stream = this.#streams.get(session_id);
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.set(session_id, controller);
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
- return this.#handle_delete(session_id);
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
- return this.#handle_get(session_id);
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
- return this.#handle_post(session_id, request, auth_info);
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
- return this.#handle_default(method);
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
  }
@@ -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 {};
@@ -10,6 +10,6 @@
10
10
  "sourcesContent": [
11
11
  null
12
12
  ],
13
- "mappings": ";;;cAcaA,aAAaA",
13
+ "mappings": ";;;;cA6BaA,aAAaA",
14
14
  "ignoreList": []
15
15
  }