@tmcp/transport-http 0.5.2 → 0.5.4
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 +36 -53
- package/package.json +7 -6
- package/src/index.js +129 -13
- package/src/types/index.d.ts +9 -0
- package/src/types/index.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -5,11 +5,7 @@ An HTTP transport implementation for TMCP (TypeScript Model Context Protocol) se
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npm install @tmcp/transport-http tmcp
|
|
9
|
-
# or
|
|
10
8
|
pnpm add @tmcp/transport-http tmcp
|
|
11
|
-
# or
|
|
12
|
-
yarn add @tmcp/transport-http tmcp
|
|
13
9
|
```
|
|
14
10
|
|
|
15
11
|
## Usage
|
|
@@ -54,56 +50,18 @@ server.tool(
|
|
|
54
50
|
const transport = new HttpTransport(server);
|
|
55
51
|
|
|
56
52
|
// Use with your preferred HTTP server
|
|
57
|
-
// Example with Node.js built-in server
|
|
58
|
-
import
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const request = new Request(`http://localhost${req.url}`, {
|
|
66
|
-
method: req.method,
|
|
67
|
-
headers: req.headers,
|
|
68
|
-
body: req.method === 'POST' ? body : undefined,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const response = await transport.respond(request);
|
|
72
|
-
|
|
73
|
-
// If response is null, the request wasn't for MCP
|
|
74
|
-
if (response === null) {
|
|
75
|
-
res.statusCode = 404;
|
|
76
|
-
res.end('Not Found');
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Copy response to Node.js response
|
|
81
|
-
response.headers.forEach((value, key) => {
|
|
82
|
-
res.setHeader(key, value);
|
|
83
|
-
});
|
|
84
|
-
res.statusCode = response.status;
|
|
85
|
-
|
|
86
|
-
if (response.body) {
|
|
87
|
-
const reader = response.body.getReader();
|
|
88
|
-
const pump = async () => {
|
|
89
|
-
const { done, value } = await reader.read();
|
|
90
|
-
if (done) {
|
|
91
|
-
res.end();
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
res.write(value);
|
|
95
|
-
pump();
|
|
96
|
-
};
|
|
97
|
-
pump();
|
|
98
|
-
} else {
|
|
99
|
-
res.end();
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
} catch (error) {
|
|
103
|
-
res.statusCode = 500;
|
|
104
|
-
res.end('Internal Server Error');
|
|
53
|
+
// Example with Node.js built-in server + @remix-run/
|
|
54
|
+
import * as http from 'node:http';
|
|
55
|
+
import { createRequestListener } from '@remix-run/node-fetch-server';
|
|
56
|
+
|
|
57
|
+
let httpServer = http.createServer(createRequestListener((request)=>{
|
|
58
|
+
const response = await transport.respond(request);
|
|
59
|
+
if(response){
|
|
60
|
+
return response;
|
|
105
61
|
}
|
|
106
|
-
});
|
|
62
|
+
return new Response(null, { status: 404 });
|
|
63
|
+
}));
|
|
64
|
+
|
|
107
65
|
|
|
108
66
|
httpServer.listen(3000, () => {
|
|
109
67
|
console.log('MCP HTTP server listening on port 3000');
|
|
@@ -157,6 +115,7 @@ Creates a new HTTP transport instance.
|
|
|
157
115
|
interface HttpTransportOptions {
|
|
158
116
|
getSessionId: () => string; // Custom session ID generator
|
|
159
117
|
path?: string; // MCP endpoint path (default: '/mcp')
|
|
118
|
+
oauth?: OAuth; // an oauth provider generated from @tmcp/auth
|
|
160
119
|
}
|
|
161
120
|
```
|
|
162
121
|
|
|
@@ -187,6 +146,7 @@ Processes an HTTP request and returns a Response with Server-Sent Events, or nul
|
|
|
187
146
|
The transport supports three HTTP methods:
|
|
188
147
|
|
|
189
148
|
#### POST - Message Processing
|
|
149
|
+
|
|
190
150
|
Clients send JSON-RPC messages via HTTP POST requests:
|
|
191
151
|
|
|
192
152
|
```http
|
|
@@ -215,6 +175,7 @@ data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
|
|
|
215
175
|
```
|
|
216
176
|
|
|
217
177
|
#### GET - Notification Stream
|
|
178
|
+
|
|
218
179
|
Establishes long-lived connections for server notifications:
|
|
219
180
|
|
|
220
181
|
```http
|
|
@@ -236,6 +197,7 @@ data: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
|
|
|
236
197
|
```
|
|
237
198
|
|
|
238
199
|
#### DELETE - Session Disconnect
|
|
200
|
+
|
|
239
201
|
Disconnects a session and cleans up resources:
|
|
240
202
|
|
|
241
203
|
```http
|
|
@@ -298,6 +260,27 @@ Deno.serve({ port: 3000 }, async (req) => {
|
|
|
298
260
|
});
|
|
299
261
|
```
|
|
300
262
|
|
|
263
|
+
### `srvx`
|
|
264
|
+
|
|
265
|
+
If you want to have the same experience throughout Deno, Bun or Node you can also use `srvx`
|
|
266
|
+
|
|
267
|
+
```js
|
|
268
|
+
import { McpServer } from 'tmcp';
|
|
269
|
+
import { HttpTransport } from '@tmcp/transport-http';
|
|
270
|
+
import { serve } from 'srvx';
|
|
271
|
+
|
|
272
|
+
const server = new McpServer(/* ... */);
|
|
273
|
+
const transport = new HttpTransport(server);
|
|
274
|
+
|
|
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;
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
301
284
|
## Error Handling
|
|
302
285
|
|
|
303
286
|
The transport includes comprehensive error handling:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmcp/transport-http",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
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.
|
|
19
|
+
"tmcp": "^1.10.2"
|
|
20
20
|
},
|
|
21
21
|
"keywords": [
|
|
22
22
|
"tmcp",
|
|
@@ -29,15 +29,16 @@
|
|
|
29
29
|
"directory": "packages/transport-http"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.17.1",
|
|
33
|
+
"@remix-run/node-fetch-server": "^0.8.0",
|
|
33
34
|
"@types/node": "^24.0.15",
|
|
34
35
|
"dts-buddy": "^0.6.2",
|
|
35
36
|
"publint": "^0.3.12",
|
|
36
37
|
"valibot": "^1.1.0",
|
|
37
38
|
"vitest": "^3.2.4",
|
|
38
|
-
"@tmcp/adapter-valibot": "^0.1.
|
|
39
|
-
"
|
|
40
|
-
"tmcp": "^
|
|
39
|
+
"@tmcp/adapter-valibot": "^0.1.4",
|
|
40
|
+
"tmcp": "^1.10.2",
|
|
41
|
+
"@tmcp/auth": "^0.3.3"
|
|
41
42
|
},
|
|
42
43
|
"publishConfig": {
|
|
43
44
|
"access": "public"
|
package/src/index.js
CHANGED
|
@@ -3,11 +3,23 @@
|
|
|
3
3
|
* @import { OAuth } from "@tmcp/auth";
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {{
|
|
8
|
+
* origin?: string | string[] | boolean
|
|
9
|
+
* methods?: string[]
|
|
10
|
+
* allowedHeaders?: string[]
|
|
11
|
+
* exposedHeaders?: string[]
|
|
12
|
+
* credentials?: boolean
|
|
13
|
+
* maxAge?: number
|
|
14
|
+
* }} CorsConfig
|
|
15
|
+
*/
|
|
16
|
+
|
|
6
17
|
/**
|
|
7
18
|
* @typedef {{
|
|
8
19
|
* getSessionId?: () => string
|
|
9
20
|
* path?: string
|
|
10
21
|
* oauth?: OAuth<"built">
|
|
22
|
+
* cors?: CorsConfig | boolean
|
|
11
23
|
* }} HttpTransportOptions
|
|
12
24
|
*/
|
|
13
25
|
|
|
@@ -19,7 +31,7 @@ export class HttpTransport {
|
|
|
19
31
|
#server;
|
|
20
32
|
|
|
21
33
|
/**
|
|
22
|
-
* @type {Required<Omit<HttpTransportOptions, 'oauth'>>}
|
|
34
|
+
* @type {Required<Omit<HttpTransportOptions, 'oauth' | 'cors'>> & { cors?: CorsConfig | boolean }}
|
|
23
35
|
*/
|
|
24
36
|
#options;
|
|
25
37
|
|
|
@@ -60,6 +72,7 @@ export class HttpTransport {
|
|
|
60
72
|
getSessionId = () => crypto.randomUUID(),
|
|
61
73
|
path = '/mcp',
|
|
62
74
|
oauth,
|
|
75
|
+
cors,
|
|
63
76
|
} = options ?? {
|
|
64
77
|
getSessionId: () => crypto.randomUUID(),
|
|
65
78
|
};
|
|
@@ -68,7 +81,7 @@ export class HttpTransport {
|
|
|
68
81
|
this.#oauth = oauth;
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
this.#options = { getSessionId, path };
|
|
84
|
+
this.#options = { getSessionId, path, cors };
|
|
72
85
|
this.#path = path;
|
|
73
86
|
this.#server.on('send', ({ request, context: { sessions } }) => {
|
|
74
87
|
// use the current controller if the request has an id (it means it's a request and not a notification)
|
|
@@ -95,6 +108,89 @@ export class HttpTransport {
|
|
|
95
108
|
});
|
|
96
109
|
}
|
|
97
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Applies CORS headers to a response based on the configuration
|
|
113
|
+
* @param {Response} response - The response to modify
|
|
114
|
+
* @param {Request} request - The original request
|
|
115
|
+
*/
|
|
116
|
+
#apply_cors_headers(response, request) {
|
|
117
|
+
const cors_config = this.#options.cors;
|
|
118
|
+
if (!cors_config) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Handle boolean true (allow all origins)
|
|
123
|
+
if (cors_config === true) {
|
|
124
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
125
|
+
response.headers.set(
|
|
126
|
+
'Access-Control-Allow-Methods',
|
|
127
|
+
'GET, POST, DELETE, OPTIONS',
|
|
128
|
+
);
|
|
129
|
+
response.headers.set('Access-Control-Allow-Headers', '*');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Handle detailed configuration
|
|
134
|
+
const config = /** @type {CorsConfig} */ (cors_config);
|
|
135
|
+
const origin = request.headers.get('origin');
|
|
136
|
+
|
|
137
|
+
// Handle origin
|
|
138
|
+
if (config.origin !== undefined) {
|
|
139
|
+
if (config.origin === true || config.origin === '*') {
|
|
140
|
+
response.headers.set('Access-Control-Allow-Origin', '*');
|
|
141
|
+
} else if (typeof config.origin === 'string') {
|
|
142
|
+
if (origin === config.origin) {
|
|
143
|
+
response.headers.set(
|
|
144
|
+
'Access-Control-Allow-Origin',
|
|
145
|
+
config.origin,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
} else if (Array.isArray(config.origin)) {
|
|
149
|
+
if (origin && config.origin.includes(origin)) {
|
|
150
|
+
response.headers.set('Access-Control-Allow-Origin', origin);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Handle other CORS headers with defaults
|
|
156
|
+
const methods = config.methods ?? ['GET', 'POST', 'DELETE', 'OPTIONS'];
|
|
157
|
+
response.headers.set(
|
|
158
|
+
'Access-Control-Allow-Methods',
|
|
159
|
+
methods.join(', '),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const allowed_headers = config.allowedHeaders ?? '*';
|
|
163
|
+
if (Array.isArray(allowed_headers)) {
|
|
164
|
+
response.headers.set(
|
|
165
|
+
'Access-Control-Allow-Headers',
|
|
166
|
+
allowed_headers.join(', '),
|
|
167
|
+
);
|
|
168
|
+
} else {
|
|
169
|
+
response.headers.set(
|
|
170
|
+
'Access-Control-Allow-Headers',
|
|
171
|
+
allowed_headers,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (config.exposedHeaders) {
|
|
176
|
+
response.headers.set(
|
|
177
|
+
'Access-Control-Expose-Headers',
|
|
178
|
+
config.exposedHeaders.join(', '),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (config.credentials) {
|
|
183
|
+
response.headers.set('Access-Control-Allow-Credentials', 'true');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (config.maxAge !== undefined) {
|
|
187
|
+
response.headers.set(
|
|
188
|
+
'Access-Control-Max-Age',
|
|
189
|
+
config.maxAge.toString(),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
98
194
|
/**
|
|
99
195
|
* @param {string} session_id
|
|
100
196
|
*/
|
|
@@ -288,7 +384,7 @@ export class HttpTransport {
|
|
|
288
384
|
status: 405,
|
|
289
385
|
headers: {
|
|
290
386
|
'Content-Type': 'application/json',
|
|
291
|
-
Allow: 'GET, POST, DELETE',
|
|
387
|
+
Allow: 'GET, POST, DELETE, OPTIONS',
|
|
292
388
|
},
|
|
293
389
|
},
|
|
294
390
|
);
|
|
@@ -339,22 +435,42 @@ export class HttpTransport {
|
|
|
339
435
|
request.headers.get('mcp-session-id') ||
|
|
340
436
|
this.#options.getSessionId();
|
|
341
437
|
|
|
438
|
+
/**
|
|
439
|
+
* @type {Response | null}
|
|
440
|
+
*/
|
|
441
|
+
let response = null;
|
|
442
|
+
|
|
443
|
+
// Handle OPTIONS request - preflight CORS
|
|
444
|
+
if (method === 'OPTIONS') {
|
|
445
|
+
response = new Response(null, {
|
|
446
|
+
status: 204,
|
|
447
|
+
headers: {
|
|
448
|
+
'Content-Type': 'application/json',
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
342
452
|
// Handle DELETE request - disconnect session
|
|
343
|
-
if (method === 'DELETE') {
|
|
344
|
-
|
|
453
|
+
else if (method === 'DELETE') {
|
|
454
|
+
response = this.#handle_delete(session_id);
|
|
345
455
|
}
|
|
346
|
-
|
|
347
456
|
// Handle GET request - establish long-lived connection for notifications
|
|
348
|
-
if (method === 'GET') {
|
|
349
|
-
|
|
457
|
+
else if (method === 'GET') {
|
|
458
|
+
response = this.#handle_get(session_id);
|
|
350
459
|
}
|
|
351
|
-
|
|
352
460
|
// Handle POST request - process message and respond through event stream
|
|
353
|
-
if (method === 'POST') {
|
|
354
|
-
|
|
461
|
+
else if (method === 'POST') {
|
|
462
|
+
response = await this.#handle_post(session_id, request, auth_info);
|
|
355
463
|
}
|
|
356
|
-
|
|
357
464
|
// Method not supported
|
|
358
|
-
|
|
465
|
+
else {
|
|
466
|
+
response = this.#handle_default(method);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Apply CORS headers if we have a response
|
|
470
|
+
if (response) {
|
|
471
|
+
this.#apply_cors_headers(response, request);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return response;
|
|
359
475
|
}
|
|
360
476
|
}
|
package/src/types/index.d.ts
CHANGED
|
@@ -8,10 +8,19 @@ declare module '@tmcp/transport-http' {
|
|
|
8
8
|
respond(request: Request): Promise<Response | null>;
|
|
9
9
|
#private;
|
|
10
10
|
}
|
|
11
|
+
export type CorsConfig = {
|
|
12
|
+
origin?: string | string[] | boolean;
|
|
13
|
+
methods?: string[];
|
|
14
|
+
allowedHeaders?: string[];
|
|
15
|
+
exposedHeaders?: string[];
|
|
16
|
+
credentials?: boolean;
|
|
17
|
+
maxAge?: number;
|
|
18
|
+
};
|
|
11
19
|
export type HttpTransportOptions = {
|
|
12
20
|
getSessionId?: () => string;
|
|
13
21
|
path?: string;
|
|
14
22
|
oauth?: OAuth<"built">;
|
|
23
|
+
cors?: CorsConfig | boolean;
|
|
15
24
|
};
|
|
16
25
|
|
|
17
26
|
export {};
|