@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 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 { createServer } from 'http';
59
-
60
- const httpServer = createServer(async (req, res) => {
61
- try {
62
- let body = '';
63
- req.on('data', (chunk) => (body += chunk));
64
- req.on('end', async () => {
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.2",
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.9.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.16.0",
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.3",
39
- "@tmcp/auth": "^0.3.2",
40
- "tmcp": "^1.9.1"
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
- return this.#handle_delete(session_id);
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
- return this.#handle_get(session_id);
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
- return this.#handle_post(session_id, request, auth_info);
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
- return this.#handle_default(method);
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
  }
@@ -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 {};
@@ -10,6 +10,6 @@
10
10
  "sourcesContent": [
11
11
  null
12
12
  ],
13
- "mappings": ";;;cAcaA,aAAaA",
13
+ "mappings": ";;;cA0BaA,aAAaA",
14
14
  "ignoreList": []
15
15
  }