@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 +23 -5
- package/package.json +11 -5
- package/src/index.js +114 -25
- package/src/type-utils.d.ts +6 -0
- package/src/types/index.d.ts +8 -2
- package/src/types/index.d.ts.map +7 -3
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 {
|
|
138
|
+
import {
|
|
139
|
+
InMemoryStreamSessionManager,
|
|
140
|
+
InMemoryInfoSessionManager,
|
|
141
|
+
} from '@tmcp/session-manager';
|
|
139
142
|
|
|
140
143
|
const transport = new HttpTransport(server, {
|
|
141
|
-
sessionManager:
|
|
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 {
|
|
156
|
+
import {
|
|
157
|
+
RedisStreamSessionManager,
|
|
158
|
+
RedisInfoSessionManager,
|
|
159
|
+
} from '@tmcp/session-manager-redis';
|
|
151
160
|
|
|
152
161
|
const transport = new HttpTransport(server, {
|
|
153
|
-
sessionManager:
|
|
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?:
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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 {
|
|
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?:
|
|
25
|
+
* sessionManager?: { streams?: StreamSessionManager, info?: OptionalizeSessionManager<InfoSessionManager> }
|
|
25
26
|
* }} HttpTransportOptions
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
29
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
29
|
-
import {
|
|
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 =
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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>>;
|
package/src/types/index.d.ts
CHANGED
|
@@ -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 {
|
|
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?:
|
|
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
|
}
|
package/src/types/index.d.ts.map
CHANGED
|
@@ -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": ";;;;
|
|
17
|
+
"mappings": ";;;;cAsCaA,aAAaA;;;;;;;;;;;;;;;;;;;;;;;;;MCnCrBC,MAAMA;;MAECC,yBAAyBA",
|
|
14
18
|
"ignoreList": []
|
|
15
19
|
}
|