@tmcp/transport-http 0.5.4 → 0.5.6

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.4",
3
+ "version": "0.5.6",
4
4
  "description": "Transport for TMCP using HTTP",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -16,7 +16,7 @@
16
16
  "src/**/*"
17
17
  ],
18
18
  "peerDependencies": {
19
- "tmcp": "^1.10.2"
19
+ "tmcp": "^1.10.3"
20
20
  },
21
21
  "keywords": [
22
22
  "tmcp",
@@ -28,6 +28,9 @@
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.1"
33
+ },
31
34
  "devDependencies": {
32
35
  "@modelcontextprotocol/sdk": "^1.17.1",
33
36
  "@remix-run/node-fetch-server": "^0.8.0",
@@ -37,8 +40,8 @@
37
40
  "valibot": "^1.1.0",
38
41
  "vitest": "^3.2.4",
39
42
  "@tmcp/adapter-valibot": "^0.1.4",
40
- "tmcp": "^1.10.2",
41
- "@tmcp/auth": "^0.3.3"
43
+ "@tmcp/auth": "^0.3.3",
44
+ "tmcp": "^1.10.3"
42
45
  },
43
46
  "publishConfig": {
44
47
  "access": "public"
package/src/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * @import { AuthInfo, McpServer } from "tmcp";
3
3
  * @import { OAuth } from "@tmcp/auth";
4
+ * @import { SessionManager } from "@tmcp/session-manager";
4
5
  */
5
6
 
6
7
  /**
@@ -19,11 +20,13 @@
19
20
  * getSessionId?: () => string
20
21
  * path?: string
21
22
  * oauth?: OAuth<"built">
22
- * cors?: CorsConfig | boolean
23
+ * cors?: CorsConfig | boolean,
24
+ * sessionManager?: SessionManager
23
25
  * }} HttpTransportOptions
24
26
  */
25
27
 
26
28
  import { AsyncLocalStorage } from 'node:async_hooks';
29
+ import { InMemorySessionManager } from '@tmcp/session-manager';
27
30
  export class HttpTransport {
28
31
  /**
29
32
  * @type {McpServer<any>}
@@ -40,15 +43,6 @@ export class HttpTransport {
40
43
  */
41
44
  #path;
42
45
 
43
- /**
44
- * @type {Map<string, ReadableStreamDefaultController>}
45
- */
46
- #session = new Map();
47
- /**
48
- * @type {Map<string, ReadableStream>}
49
- */
50
- #streams = new Map();
51
-
52
46
  /**
53
47
  * @type {AsyncLocalStorage<ReadableStreamDefaultController | undefined>}
54
48
  */
@@ -73,6 +67,7 @@ export class HttpTransport {
73
67
  path = '/mcp',
74
68
  oauth,
75
69
  cors,
70
+ sessionManager = new InMemorySessionManager(),
76
71
  } = options ?? {
77
72
  getSessionId: () => crypto.randomUUID(),
78
73
  };
@@ -81,9 +76,9 @@ export class HttpTransport {
81
76
  this.#oauth = oauth;
82
77
  }
83
78
 
84
- this.#options = { getSessionId, path, cors };
79
+ this.#options = { getSessionId, path, cors, sessionManager };
85
80
  this.#path = path;
86
- this.#server.on('send', ({ request, context: { sessions } }) => {
81
+ this.#server.on('send', async ({ request, context: { sessions } }) => {
87
82
  // use the current controller if the request has an id (it means it's a request and not a notification)
88
83
  if (request.id != null) {
89
84
  const controller = this.#controller_storage.getStore();
@@ -96,15 +91,10 @@ export class HttpTransport {
96
91
  );
97
92
  return;
98
93
  }
99
- for (let [session_id, controller] of this.#session.entries()) {
100
- if (sessions === undefined || sessions.includes(session_id)) {
101
- controller.enqueue(
102
- this.#text_encoder.encode(
103
- 'data: ' + JSON.stringify(request) + '\n\n',
104
- ),
105
- );
106
- }
107
- }
94
+ await this.#options.sessionManager.send(
95
+ sessions,
96
+ 'data: ' + JSON.stringify(request) + '\n\n',
97
+ );
108
98
  });
109
99
  }
110
100
 
@@ -194,13 +184,8 @@ export class HttpTransport {
194
184
  /**
195
185
  * @param {string} session_id
196
186
  */
197
- #handle_delete(session_id) {
198
- const controller = this.#session.get(session_id);
199
- if (controller) {
200
- controller.close();
201
- this.#session.delete(session_id);
202
- this.#streams.delete(session_id);
203
- }
187
+ async #handle_delete(session_id) {
188
+ await this.#options.sessionManager.delete(session_id);
204
189
  return new Response(null, {
205
190
  status: 204,
206
191
  headers: {
@@ -214,12 +199,11 @@ export class HttpTransport {
214
199
  * @param {string} session_id
215
200
  * @returns
216
201
  */
217
- #handle_get(session_id) {
218
- const sessions = this.#session;
219
- const streams = this.#streams;
220
- // If session already exists, return existing stream
221
- const existing_stream = this.#streams.get(session_id);
222
- 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) {
223
207
  return new Response(
224
208
  JSON.stringify({
225
209
  jsonrpc: '2.0',
@@ -242,16 +226,14 @@ export class HttpTransport {
242
226
 
243
227
  // Create new long-lived stream for notifications
244
228
  const stream = new ReadableStream({
245
- start(controller) {
246
- sessions.set(session_id, controller);
229
+ async start(controller) {
230
+ await sessions.create(session_id, controller);
247
231
  },
248
- cancel() {
249
- sessions.delete(session_id);
250
- streams.delete(session_id);
232
+ async cancel() {
233
+ await sessions.delete(session_id);
251
234
  },
252
235
  });
253
236
 
254
- streams.set(session_id, stream);
255
237
  return new Response(stream, {
256
238
  headers: {
257
239
  'Content-Type': 'text/event-stream',
@@ -451,11 +433,11 @@ export class HttpTransport {
451
433
  }
452
434
  // Handle DELETE request - disconnect session
453
435
  else if (method === 'DELETE') {
454
- response = this.#handle_delete(session_id);
436
+ response = await this.#handle_delete(session_id);
455
437
  }
456
438
  // Handle GET request - establish long-lived connection for notifications
457
439
  else if (method === 'GET') {
458
- response = this.#handle_get(session_id);
440
+ response = await this.#handle_get(session_id);
459
441
  }
460
442
  // Handle POST request - process message and respond through event stream
461
443
  else if (method === 'POST') {
@@ -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);
@@ -21,6 +22,7 @@ declare module '@tmcp/transport-http' {
21
22
  path?: string;
22
23
  oauth?: OAuth<"built">;
23
24
  cors?: CorsConfig | boolean;
25
+ sessionManager?: SessionManager;
24
26
  };
25
27
 
26
28
  export {};
@@ -10,6 +10,6 @@
10
10
  "sourcesContent": [
11
11
  null
12
12
  ],
13
- "mappings": ";;;cA0BaA,aAAaA",
13
+ "mappings": ";;;;cA6BaA,aAAaA",
14
14
  "ignoreList": []
15
15
  }