@tmcp/transport-http 0.7.1 → 0.8.1

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
@@ -135,10 +135,16 @@ The HTTP transport supports custom session managers for different deployment sce
135
135
  #### In-Memory Sessions (Default)
136
136
 
137
137
  ```javascript
138
- import { InMemorySessionManager } from '@tmcp/session-manager';
138
+ import {
139
+ InMemoryStreamSessionManager,
140
+ InMemoryInfoSessionManager,
141
+ } from '@tmcp/session-manager';
139
142
 
140
143
  const transport = new HttpTransport(server, {
141
- sessionManager: new InMemorySessionManager(), // Default behavior
144
+ sessionManager: {
145
+ streams: new InMemoryStreamSessionManager(),
146
+ info: new InMemoryInfoSessionManager(),
147
+ },
142
148
  });
143
149
  ```
144
150
 
@@ -147,10 +153,16 @@ const transport = new HttpTransport(server, {
147
153
  For deployments across multiple servers or serverless environments where sessions need to be shared:
148
154
 
149
155
  ```javascript
150
- import { RedisSessionManager } from '@tmcp/session-manager-redis';
156
+ import {
157
+ RedisStreamSessionManager,
158
+ RedisInfoSessionManager,
159
+ } from '@tmcp/session-manager-redis';
151
160
 
152
161
  const transport = new HttpTransport(server, {
153
- sessionManager: new RedisSessionManager('redis://localhost:6379'),
162
+ sessionManager: {
163
+ streams: new RedisStreamSessionManager('redis://localhost:6379'),
164
+ info: new RedisInfoSessionManager('redis://localhost:6379'),
165
+ },
154
166
  });
155
167
  ```
156
168
 
@@ -170,6 +182,7 @@ const transport = new HttpTransport(server, {
170
182
  - **⚡ Real-time Updates**: Server can push notifications and updates to connected clients
171
183
  - **🛡️ Error Handling**: Graceful error handling for malformed requests
172
184
  - **🔀 Multiple HTTP Methods**: Supports GET (notifications), POST (messages), and DELETE (disconnect)
185
+ - **🧠 Session Metadata**: Automatically persists client capabilities, info, and log levels and exposes them via `server.ctx.sessionInfo`
173
186
 
174
187
  ## API
175
188
 
@@ -195,10 +208,15 @@ interface HttpTransportOptions {
195
208
  getSessionId: () => string; // Custom session ID generator
196
209
  path?: string | null; // MCP endpoint path (default: '/mcp', null responds on every path)
197
210
  oauth?: OAuth; // an oauth provider generated from @tmcp/auth
198
- sessionManager?: SessionManager; // Custom session manager (default: InMemorySessionManager)
211
+ sessionManager?: {
212
+ streams?: StreamSessionManager;
213
+ info?: InfoSessionManager;
214
+ }; // Provide custom managers; defaults to in-memory implementations
199
215
  }
200
216
  ```
201
217
 
218
+ If you omit `sessionManager` the transport creates `InMemoryStreamSessionManager` and `InMemoryInfoSessionManager` instances for you. You can override either field independently (for example, Redis streams with in-memory metadata during development).
219
+
202
220
  #### Methods
203
221
 
204
222
  ##### `respond(request: Request, customContext?: T): Promise<Response | null>`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmcp/transport-http",
3
- "version": "0.7.1",
3
+ "version": "0.8.1",
4
4
  "description": "Transport for TMCP using HTTP",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -16,7 +16,13 @@
16
16
  "src/**/*"
17
17
  ],
18
18
  "peerDependencies": {
19
- "tmcp": "^1.15.3"
19
+ "tmcp": "^1.16.3",
20
+ "@tmcp/auth": "^0.3.3"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "@tmcp/auth": {
24
+ "optional": true
25
+ }
20
26
  },
21
27
  "keywords": [
22
28
  "tmcp",
@@ -30,11 +36,11 @@
30
36
  },
31
37
  "dependencies": {
32
38
  "esm-env": "^1.2.2",
33
- "@tmcp/session-manager": "^0.1.2"
39
+ "@tmcp/session-manager": "^0.2.1"
34
40
  },
35
41
  "devDependencies": {
36
42
  "@modelcontextprotocol/sdk": "^1.17.1",
37
- "@remix-run/node-fetch-server": "^0.11.0",
43
+ "@remix-run/node-fetch-server": "^0.12.0",
38
44
  "@types/node": "^24.0.15",
39
45
  "dts-buddy": "^0.6.2",
40
46
  "publint": "^0.3.12",
@@ -42,7 +48,7 @@
42
48
  "vitest": "^4.0.0",
43
49
  "@tmcp/adapter-valibot": "^0.1.4",
44
50
  "@tmcp/auth": "^0.3.3",
45
- "tmcp": "^1.15.3"
51
+ "tmcp": "^1.16.3"
46
52
  },
47
53
  "publishConfig": {
48
54
  "access": "public"
package/src/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * @import { AuthInfo, McpServer } from "tmcp";
3
3
  * @import { OAuth } from "@tmcp/auth";
4
- * @import { SessionManager } from "@tmcp/session-manager";
4
+ * @import { StreamSessionManager, InfoSessionManager } from "@tmcp/session-manager";
5
+ * @import { OptionalizeSessionManager } from "./type-utils.js"
5
6
  */
6
7
 
7
8
  /**
@@ -21,25 +22,32 @@
21
22
  * path?: string | null
22
23
  * oauth?: OAuth<"built">
23
24
  * cors?: CorsConfig | boolean,
24
- * sessionManager?: SessionManager
25
+ * sessionManager?: { streams?: StreamSessionManager, info?: OptionalizeSessionManager<InfoSessionManager> }
25
26
  * }} HttpTransportOptions
26
27
  */
27
28
 
28
29
  import { AsyncLocalStorage } from 'node:async_hooks';
29
- import { InMemorySessionManager } from '@tmcp/session-manager';
30
+ import {
31
+ InMemoryStreamSessionManager,
32
+ InMemoryInfoSessionManager,
33
+ } from '@tmcp/session-manager';
30
34
  import { DEV } from 'esm-env';
31
35
 
32
36
  /**
33
37
  * @template {Record<string, unknown> | undefined} [TCustom=undefined]
34
38
  */
35
39
  export class HttpTransport {
40
+ /**
41
+ * @typedef {NonNullable<Required<Pick<HttpTransportOptions, "sessionManager">["sessionManager"]>>} SessionManager
42
+ */
43
+
36
44
  /**
37
45
  * @type {McpServer<any, TCustom>}
38
46
  */
39
47
  #server;
40
48
 
41
49
  /**
42
- * @type {Required<Omit<HttpTransportOptions, 'oauth' | 'cors'>> & { cors?: CorsConfig | boolean }}
50
+ * @type {Required<Omit<HttpTransportOptions, 'oauth' | 'cors' | 'sessionManager'>> & { cors?: CorsConfig | boolean, sessionManager: SessionManager }}
43
51
  */
44
52
  #options;
45
53
 
@@ -53,6 +61,11 @@ export class HttpTransport {
53
61
  */
54
62
  #controller_storage = new AsyncLocalStorage();
55
63
 
64
+ /**
65
+ * @type {AsyncLocalStorage<string>}
66
+ */
67
+ #session_id_storage = new AsyncLocalStorage();
68
+
56
69
  /**
57
70
  * @type {OAuth<"built"> | undefined}
58
71
  */
@@ -72,11 +85,23 @@ export class HttpTransport {
72
85
  path = '/mcp',
73
86
  oauth,
74
87
  cors,
75
- sessionManager = new InMemorySessionManager(),
88
+ sessionManager: _sessionManager = {
89
+ streams: new InMemoryStreamSessionManager(),
90
+ info: new InMemoryInfoSessionManager(),
91
+ },
76
92
  } = options ?? {
77
93
  getSessionId: () => crypto.randomUUID(),
78
94
  };
79
95
 
96
+ /**
97
+ * @type {SessionManager}
98
+ */
99
+ const sessionManager = {
100
+ streams:
101
+ _sessionManager.streams ?? new InMemoryStreamSessionManager(),
102
+ info: _sessionManager.info ?? new InMemoryInfoSessionManager(),
103
+ };
104
+
80
105
  if (options?.path === undefined && DEV) {
81
106
  // TODO: remove on 1.0.0 release
82
107
  console.warn(
@@ -90,24 +115,67 @@ export class HttpTransport {
90
115
 
91
116
  this.#options = { getSessionId, path, cors, sessionManager };
92
117
  this.#path = path;
93
- this.#server.on('send', async ({ request, context: { sessions } }) => {
94
- // use the current controller if the request has an id (it means it's a request and not a notification)
95
- if (request.id != null) {
96
- const controller = this.#controller_storage.getStore();
97
- if (!controller) return;
98
118
 
99
- controller.enqueue(
100
- this.#text_encoder.encode(
101
- 'data: ' + JSON.stringify(request) + '\n\n',
102
- ),
119
+ this.#server.on('initialize', ({ capabilities, clientInfo }) => {
120
+ const sessionId = this.#session_id_storage.getStore();
121
+ if (!sessionId) return;
122
+ this.#options.sessionManager.info.setClientCapabilities(
123
+ sessionId,
124
+ capabilities,
125
+ );
126
+ this.#options.sessionManager.info.setClientInfo(
127
+ sessionId,
128
+ clientInfo,
129
+ );
130
+ });
131
+
132
+ this.#server.on('subscription', async ({ uri, action }) => {
133
+ const sessionId = this.#session_id_storage.getStore();
134
+ if (!sessionId) return;
135
+ if (action === 'remove') {
136
+ this.#options.sessionManager.info.removeSubscription?.(
137
+ sessionId,
138
+ uri,
103
139
  );
104
- return;
140
+ } else {
141
+ this.#options.sessionManager.info.addSubscription(
142
+ sessionId,
143
+ uri,
144
+ );
145
+ }
146
+ });
147
+
148
+ this.#server.on('loglevelchange', ({ level }) => {
149
+ const sessionId = this.#session_id_storage.getStore();
150
+ if (!sessionId) return;
151
+ this.#options.sessionManager.info.setLogLevel(sessionId, level);
152
+ });
153
+
154
+ this.#server.on('broadcast', async ({ request }) => {
155
+ let sessions = undefined;
156
+ if (request.method === 'notifications/resources/updated') {
157
+ sessions =
158
+ await this.#options.sessionManager.info.getSubscriptions(
159
+ request.params.uri,
160
+ );
105
161
  }
106
- await this.#options.sessionManager.send(
162
+ await this.#options.sessionManager.streams.send(
107
163
  sessions,
108
164
  'data: ' + JSON.stringify(request) + '\n\n',
109
165
  );
110
166
  });
167
+
168
+ this.#server.on('send', async ({ request }) => {
169
+ // use the current controller if the request has an id (it means it's a request and not a notification)
170
+ const controller = this.#controller_storage.getStore();
171
+ if (!controller) return;
172
+
173
+ controller.enqueue(
174
+ this.#text_encoder.encode(
175
+ 'data: ' + JSON.stringify(request) + '\n\n',
176
+ ),
177
+ );
178
+ });
111
179
  }
112
180
 
113
181
  /**
@@ -197,7 +265,8 @@ export class HttpTransport {
197
265
  * @param {string} session_id
198
266
  */
199
267
  async #handle_delete(session_id) {
200
- await this.#options.sessionManager.delete(session_id);
268
+ await this.#options.sessionManager.streams.delete(session_id);
269
+ await this.#options.sessionManager.info.delete(session_id);
201
270
  return new Response(null, {
202
271
  status: 200,
203
272
  headers: {
@@ -215,7 +284,7 @@ export class HttpTransport {
215
284
  const sessions = this.#options.sessionManager;
216
285
  const text_encoder = this.#text_encoder;
217
286
  // If session already exists, return error
218
- const existing_session = await sessions.has(session_id);
287
+ const existing_session = await sessions.streams.has(session_id);
219
288
  if (existing_session) {
220
289
  return new Response(
221
290
  JSON.stringify({
@@ -240,12 +309,12 @@ export class HttpTransport {
240
309
  // Create new long-lived stream for notifications
241
310
  const stream = new ReadableStream({
242
311
  async start(controller) {
243
- await sessions.create(session_id, controller);
312
+ await sessions.streams.create(session_id, controller);
244
313
  // send a comment to flush the headers immediately
245
314
  controller.enqueue(text_encoder.encode(': connected\n\n'));
246
315
  },
247
316
  async cancel() {
248
- await sessions.delete(session_id);
317
+ await sessions.streams.delete(session_id);
249
318
  },
250
319
  });
251
320
 
@@ -305,15 +374,35 @@ export class HttpTransport {
305
374
  },
306
375
  });
307
376
 
377
+ const session_id_storage = this.#session_id_storage;
378
+
308
379
  const handle = async () => {
380
+ const client_capabilities =
381
+ await this.#options.sessionManager.info
382
+ .getClientCapabilities(session_id)
383
+ .catch(() => undefined);
384
+ const client_info = await this.#options.sessionManager.info
385
+ .getClientInfo(session_id)
386
+ .catch(() => undefined);
387
+ const log_level = await this.#options.sessionManager.info
388
+ .getLogLevel(session_id)
389
+ .catch(() => undefined);
390
+
309
391
  const response = await this.#controller_storage.run(
310
392
  controller,
311
393
  () =>
312
- this.#server.receive(body, {
313
- sessionId: session_id,
314
- auth: auth_info ?? undefined,
315
- custom: ctx,
316
- }),
394
+ session_id_storage.run(session_id, () =>
395
+ this.#server.receive(body, {
396
+ sessionId: session_id,
397
+ auth: auth_info ?? undefined,
398
+ sessionInfo: {
399
+ clientCapabilities: client_capabilities,
400
+ clientInfo: client_info,
401
+ logLevel: log_level,
402
+ },
403
+ custom: ctx,
404
+ }),
405
+ ),
317
406
  );
318
407
 
319
408
  controller?.enqueue(
@@ -0,0 +1,6 @@
1
+
2
+ import { InfoSessionManager } from "@tmcp/session-manager";
3
+
4
+ type ToOmit = 'removeSubscription';
5
+
6
+ export type OptionalizeSessionManager<TInfoSessionManager extends InfoSessionManager> = Omit<TInfoSessionManager, ToOmit> & Partial<Pick<TInfoSessionManager, ToOmit>>;
@@ -1,7 +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
+ import type { StreamSessionManager, InfoSessionManager } from '@tmcp/session-manager';
5
5
  export class HttpTransport<TCustom extends Record<string, unknown> | undefined = undefined> {
6
6
 
7
7
  constructor(server: McpServer<any, TCustom>, options?: HttpTransportOptions);
@@ -22,8 +22,14 @@ declare module '@tmcp/transport-http' {
22
22
  path?: string | null;
23
23
  oauth?: OAuth<"built">;
24
24
  cors?: CorsConfig | boolean;
25
- sessionManager?: SessionManager;
25
+ sessionManager?: {
26
+ streams?: StreamSessionManager;
27
+ info?: OptionalizeSessionManager<InfoSessionManager>;
28
+ };
26
29
  };
30
+ type ToOmit = 'removeSubscription';
31
+
32
+ type OptionalizeSessionManager<TInfoSessionManager extends InfoSessionManager> = Omit<TInfoSessionManager, ToOmit> & Partial<Pick<TInfoSessionManager, ToOmit>>;
27
33
 
28
34
  export {};
29
35
  }
@@ -2,14 +2,18 @@
2
2
  "version": 3,
3
3
  "file": "index.d.ts",
4
4
  "names": [
5
- "HttpTransport"
5
+ "HttpTransport",
6
+ "ToOmit",
7
+ "OptionalizeSessionManager"
6
8
  ],
7
9
  "sources": [
8
- "../index.js"
10
+ "../index.js",
11
+ "../type-utils.d.ts"
9
12
  ],
10
13
  "sourcesContent": [
14
+ null,
11
15
  null
12
16
  ],
13
- "mappings": ";;;;cAkCaA,aAAaA",
17
+ "mappings": ";;;;cAsCaA,aAAaA;;;;;;;;;;;;;;;;;;;;;;;;;MCnCrBC,MAAMA;;MAECC,yBAAyBA",
14
18
  "ignoreList": []
15
19
  }