@tmcp/transport-http 0.1.1 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # @tmcp/transport-http
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Patch Changes
6
+
7
+ - f5d8390: fix: optional `getSessionId`
8
+
9
+ ## 0.2.0
10
+
11
+ ### Minor Changes
12
+
13
+ - d12964e: breaking: correctly handle http requests
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [d921480]
18
+ - tmcp@1.7.0
19
+
3
20
  ## 0.1.1
4
21
 
5
22
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Paolo Ricciuti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -50,7 +50,7 @@ server.tool(
50
50
  },
51
51
  );
52
52
 
53
- // Create the HTTP transport
53
+ // Create the HTTP transport (defaults to '/mcp' path)
54
54
  const transport = new HttpTransport(server);
55
55
 
56
56
  // Use with your preferred HTTP server
@@ -58,25 +58,32 @@ const transport = new HttpTransport(server);
58
58
  import { createServer } from 'http';
59
59
 
60
60
  const httpServer = createServer(async (req, res) => {
61
- if (req.method === 'POST' && req.url === '/mcp') {
62
- try {
63
- let body = '';
64
- req.on('data', (chunk) => (body += chunk));
65
- req.on('end', async () => {
66
- const request = new Request('http://localhost/mcp', {
67
- method: 'POST',
68
- headers: req.headers,
69
- body: body,
70
- });
71
-
72
- const response = await transport.respond(request);
73
-
74
- // Copy response to Node.js response
75
- response.headers.forEach((value, key) => {
76
- res.setHeader(key, value);
77
- });
78
- res.statusCode = response.status;
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;
79
85
 
86
+ if (response.body) {
80
87
  const reader = response.body.getReader();
81
88
  const pump = async () => {
82
89
  const { done, value } = await reader.read();
@@ -88,14 +95,13 @@ const httpServer = createServer(async (req, res) => {
88
95
  pump();
89
96
  };
90
97
  pump();
91
- });
92
- } catch (error) {
93
- res.statusCode = 500;
94
- res.end('Internal Server Error');
95
- }
96
- } else {
97
- res.statusCode = 404;
98
- res.end('Not Found');
98
+ } else {
99
+ res.end();
100
+ }
101
+ });
102
+ } catch (error) {
103
+ res.statusCode = 500;
104
+ res.end('Internal Server Error');
99
105
  }
100
106
  });
101
107
 
@@ -104,12 +110,14 @@ httpServer.listen(3000, () => {
104
110
  });
105
111
  ```
106
112
 
107
- ### With Custom Session Management
113
+ ### With Custom Configuration
108
114
 
109
115
  ```javascript
110
116
  const transport = new HttpTransport(server, {
117
+ // Custom MCP endpoint path (default: '/mcp')
118
+ path: '/api/mcp',
119
+ // Custom session ID generation
111
120
  getSessionId: () => {
112
- // Custom session ID generation
113
121
  return `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
114
122
  },
115
123
  });
@@ -120,9 +128,11 @@ const transport = new HttpTransport(server, {
120
128
  - **🌐 HTTP/SSE Communication**: Uses Server-Sent Events for real-time bidirectional communication
121
129
  - **🔄 Session Management**: Maintains client sessions with automatic session ID generation
122
130
  - **📡 Streaming Responses**: Supports streaming responses through SSE
131
+ - **🛤️ Configurable Path**: Customizable MCP endpoint path with automatic filtering
123
132
  - **🔧 Framework Agnostic**: Works with any HTTP server framework (Fastify, Bun, Deno, etc.)
124
133
  - **⚡ Real-time Updates**: Server can push notifications and updates to connected clients
125
134
  - **🛡️ Error Handling**: Graceful error handling for malformed requests
135
+ - **🔀 Multiple HTTP Methods**: Supports GET (notifications), POST (messages), and DELETE (disconnect)
126
136
 
127
137
  ## API
128
138
 
@@ -146,14 +156,15 @@ Creates a new HTTP transport instance.
146
156
  ```typescript
147
157
  interface HttpTransportOptions {
148
158
  getSessionId: () => string; // Custom session ID generator
159
+ path?: string; // MCP endpoint path (default: '/mcp')
149
160
  }
150
161
  ```
151
162
 
152
163
  #### Methods
153
164
 
154
- ##### `respond(request: Request): Promise<Response>`
165
+ ##### `respond(request: Request): Promise<Response | null>`
155
166
 
156
- Processes an HTTP request and returns a Response with Server-Sent Events.
167
+ Processes an HTTP request and returns a Response with Server-Sent Events, or null if the request path doesn't match the configured MCP path.
157
168
 
158
169
  **Parameters:**
159
170
 
@@ -161,12 +172,21 @@ Processes an HTTP request and returns a Response with Server-Sent Events.
161
172
 
162
173
  **Returns:**
163
174
 
164
- - A Response object with SSE stream for ongoing communication
175
+ - A Response object with SSE stream for ongoing communication, or null if the request path doesn't match the MCP endpoint
176
+
177
+ **HTTP Methods:**
178
+
179
+ - **POST**: Processes MCP messages and returns short-lived event stream responses
180
+ - **GET**: Establishes long-lived connections for server notifications
181
+ - **DELETE**: Disconnects sessions and cleans up resources
165
182
 
166
183
  ## Protocol Details
167
184
 
168
- ### Request Format
185
+ ### HTTP Methods
169
186
 
187
+ The transport supports three HTTP methods:
188
+
189
+ #### POST - Message Processing
170
190
  Clients send JSON-RPC messages via HTTP POST requests:
171
191
 
172
192
  ```http
@@ -182,9 +202,27 @@ mcp-session-id: optional-session-id
182
202
  }
183
203
  ```
184
204
 
185
- ### Response Format
205
+ Response: Short-lived event stream that closes after sending the response:
206
+
207
+ ```http
208
+ HTTP/1.1 200 OK
209
+ Content-Type: text/event-stream
210
+ Cache-Control: no-cache
211
+ mcp-session-id: generated-or-provided-session-id
212
+
213
+ data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
214
+
215
+ ```
216
+
217
+ #### GET - Notification Stream
218
+ Establishes long-lived connections for server notifications:
219
+
220
+ ```http
221
+ GET /mcp HTTP/1.1
222
+ mcp-session-id: optional-session-id
223
+ ```
186
224
 
187
- The server responds with Server-Sent Events:
225
+ Response: Long-lived event stream for server notifications:
188
226
 
189
227
  ```http
190
228
  HTTP/1.1 200 OK
@@ -193,8 +231,23 @@ Cache-Control: no-cache
193
231
  Connection: keep-alive
194
232
  mcp-session-id: generated-or-provided-session-id
195
233
 
196
- data: {"jsonrpc":"2.0","id":1,"result":{"tools":[...]}}
234
+ data: {"jsonrpc":"2.0","method":"notifications/initialized","params":{}}
235
+
236
+ ```
237
+
238
+ #### DELETE - Session Disconnect
239
+ Disconnects a session and cleans up resources:
197
240
 
241
+ ```http
242
+ DELETE /mcp HTTP/1.1
243
+ mcp-session-id: session-to-disconnect
244
+ ```
245
+
246
+ Response:
247
+
248
+ ```http
249
+ HTTP/1.1 204 No Content
250
+ mcp-session-id: session-to-disconnect
198
251
  ```
199
252
 
200
253
  ### Session Management
@@ -218,10 +271,11 @@ const transport = new HttpTransport(server);
218
271
  Bun.serve({
219
272
  port: 3000,
220
273
  async fetch(req) {
221
- if (req.method === 'POST' && new URL(req.url).pathname === '/mcp') {
222
- return await transport.respond(req);
274
+ const response = await transport.respond(req);
275
+ if (response === null) {
276
+ return new Response('Not Found', { status: 404 });
223
277
  }
224
- return new Response('Not Found', { status: 404 });
278
+ return response;
225
279
  },
226
280
  });
227
281
  ```
@@ -236,10 +290,11 @@ const server = new McpServer(/* ... */);
236
290
  const transport = new HttpTransport(server);
237
291
 
238
292
  Deno.serve({ port: 3000 }, async (req) => {
239
- if (req.method === 'POST' && new URL(req.url).pathname === '/mcp') {
240
- return await transport.respond(req);
293
+ const response = await transport.respond(req);
294
+ if (response === null) {
295
+ return new Response('Not Found', { status: 404 });
241
296
  }
242
- return new Response('Not Found', { status: 404 });
297
+ return response;
243
298
  });
244
299
  ```
245
300
 
@@ -278,6 +333,10 @@ pnpm lint
278
333
  - [`@tmcp/adapter-zod`](../adapter-zod) - Zod schema adapter
279
334
  - [`@tmcp/adapter-valibot`](../adapter-valibot) - Valibot schema adapter
280
335
 
336
+ ## Acknowledgments
337
+
338
+ Huge thanks to Sean O'Bannon that provided us with the `@tmcp` scope on npm.
339
+
281
340
  ## License
282
341
 
283
342
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmcp/transport-http",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "Transport for TMCP using HTTP",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -13,7 +13,7 @@
13
13
  "./package.json": "./package.json"
14
14
  },
15
15
  "peerDependencies": {
16
- "tmcp": "^1.5.0"
16
+ "tmcp": "^1.7.0"
17
17
  },
18
18
  "keywords": [
19
19
  "tmcp",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "publint": "^0.3.12",
30
- "tmcp": "^1.5.0"
30
+ "tmcp": "^1.7.0"
31
31
  },
32
32
  "publishConfig": {
33
33
  "access": "public"
package/src/index.d.ts CHANGED
@@ -3,7 +3,8 @@
3
3
  */
4
4
  /**
5
5
  * @typedef {{
6
- * getSessionId: () => string
6
+ * getSessionId?: () => string
7
+ * path?: string
7
8
  * }} HttpTransportOptions
8
9
  */
9
10
  export class HttpTransport {
@@ -16,12 +17,13 @@ export class HttpTransport {
16
17
  /**
17
18
  *
18
19
  * @param {Request} request
19
- * @returns
20
+ * @returns {Promise<Response | null>}
20
21
  */
21
- respond(request: Request): Promise<Response>;
22
+ respond(request: Request): Promise<Response | null>;
22
23
  #private;
23
24
  }
24
25
  export type HttpTransportOptions = {
25
- getSessionId: () => string;
26
+ getSessionId?: () => string;
27
+ path?: string;
26
28
  };
27
29
  import type { McpServer } from "tmcp";
package/src/index.js CHANGED
@@ -4,7 +4,8 @@
4
4
 
5
5
  /**
6
6
  * @typedef {{
7
- * getSessionId: () => string
7
+ * getSessionId?: () => string
8
+ * path?: string
8
9
  * }} HttpTransportOptions
9
10
  */
10
11
 
@@ -15,10 +16,15 @@ export class HttpTransport {
15
16
  #server;
16
17
 
17
18
  /**
18
- * @type {HttpTransportOptions}
19
+ * @type {Required<HttpTransportOptions>}
19
20
  */
20
21
  #options;
21
22
 
23
+ /**
24
+ * @type {string}
25
+ */
26
+ #path;
27
+
22
28
  /**
23
29
  * @type {Map<string, ReadableStreamDefaultController>}
24
30
  */
@@ -35,9 +41,12 @@ export class HttpTransport {
35
41
  */
36
42
  constructor(server, options) {
37
43
  this.#server = server;
38
- this.#options = options || {
39
- getSessionId: () => crypto.randomUUID(),
40
- };
44
+ const { getSessionId = () => crypto.randomUUID(), path = '/mcp' } =
45
+ options ?? {
46
+ getSessionId: () => crypto.randomUUID(),
47
+ };
48
+ this.#options = { getSessionId, path };
49
+ this.#path = path;
41
50
  this.#server.on('send', ({ request, context: { sessions } }) => {
42
51
  for (let [session_id, controller] of this.#session.entries()) {
43
52
  if (sessions === undefined || sessions.includes(session_id)) {
@@ -49,42 +58,54 @@ export class HttpTransport {
49
58
  });
50
59
  }
51
60
 
61
+ /**
62
+ * @param {string} session_id
63
+ */
64
+ #handle_delete(session_id) {
65
+ const controller = this.#session.get(session_id);
66
+ if (controller) {
67
+ controller.close();
68
+ this.#session.delete(session_id);
69
+ this.#streams.delete(session_id);
70
+ }
71
+ return new Response(null, {
72
+ status: 204,
73
+ headers: {
74
+ 'mcp-session-id': session_id,
75
+ },
76
+ });
77
+ }
78
+
52
79
  /**
53
80
  *
54
- * @param {Request} request
81
+ * @param {string} session_id
55
82
  * @returns
56
83
  */
57
- async respond(request) {
84
+ #handle_get(session_id) {
58
85
  const sessions = this.#session;
59
86
  const streams = this.#streams;
60
-
61
- const body = await request.json();
62
- const session_id =
63
- request.headers.get('mcp-session-id') ||
64
- this.#options.getSessionId();
65
- const controller = sessions.get(session_id);
66
- const response = await this.#server.receive(body, session_id);
67
-
68
- if (controller) {
69
- controller.enqueue('data: ' + JSON.stringify(response) + '\n\n');
70
- return new Response(null, {
71
- status: 204,
87
+ // If session already exists, return existing stream
88
+ const existing_stream = this.#streams.get(session_id);
89
+ if (existing_stream) {
90
+ return new Response(existing_stream, {
72
91
  headers: {
73
92
  'Content-Type': 'text/event-stream',
93
+ 'Cache-Control': 'no-cache',
94
+ Connection: 'keep-alive',
74
95
  'mcp-session-id': session_id,
75
96
  },
97
+ status: 200,
76
98
  });
77
99
  }
78
100
 
101
+ // Create new long-lived stream for notifications
79
102
  const stream = new ReadableStream({
80
103
  start(controller) {
81
104
  sessions.set(session_id, controller);
82
- controller.enqueue(
83
- 'data: ' + JSON.stringify(response) + '\n\n',
84
- );
85
105
  },
86
106
  cancel() {
87
107
  sessions.delete(session_id);
108
+ streams.delete(session_id);
88
109
  },
89
110
  });
90
111
 
@@ -99,4 +120,141 @@ export class HttpTransport {
99
120
  status: 200,
100
121
  });
101
122
  }
123
+
124
+ /**
125
+ *
126
+ * @param {string} session_id
127
+ * @param {Request} request
128
+ */
129
+ async #handle_post(session_id, request) {
130
+ // Check Content-Type header
131
+ const content_type = request.headers.get('content-type');
132
+ if (!content_type || !content_type.includes('application/json')) {
133
+ return new Response(
134
+ JSON.stringify({
135
+ jsonrpc: '2.0',
136
+ error: {
137
+ code: -32600,
138
+ message: 'Invalid Request',
139
+ data: 'Content-Type must be application/json',
140
+ },
141
+ }),
142
+ {
143
+ status: 415,
144
+ headers: {
145
+ 'Content-Type': 'application/json',
146
+ 'mcp-session-id': session_id,
147
+ },
148
+ },
149
+ );
150
+ }
151
+
152
+ try {
153
+ const body = await request.clone().json();
154
+ const response = await this.#server.receive(body, session_id);
155
+
156
+ // Determine status code based on response type
157
+ // 202 Accepted for notifications/responses, 200 OK for standard requests
158
+ const status = response == null || response.id == null ? 202 : 200;
159
+
160
+ // Create a short-lived stream that closes after sending the response
161
+ const stream = new ReadableStream({
162
+ start(controller) {
163
+ controller.enqueue(
164
+ 'data: ' + JSON.stringify(response) + '\n\n',
165
+ );
166
+ controller.close();
167
+ },
168
+ });
169
+
170
+ return new Response(stream, {
171
+ headers: {
172
+ 'Content-Type': 'text/event-stream',
173
+ 'Cache-Control': 'no-cache',
174
+ 'mcp-session-id': session_id,
175
+ },
176
+ status,
177
+ });
178
+ } catch (error) {
179
+ // Handle JSON parsing errors
180
+ return new Response(
181
+ JSON.stringify({
182
+ jsonrpc: '2.0',
183
+ error: {
184
+ code: -32700,
185
+ message: 'Parse error',
186
+ data: /** @type {Error} */ (error).message,
187
+ },
188
+ }),
189
+ {
190
+ status: 400,
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ 'mcp-session-id': session_id,
194
+ },
195
+ },
196
+ );
197
+ }
198
+ }
199
+
200
+ /**
201
+ *
202
+ * @param {string} method
203
+ * @returns
204
+ */
205
+ #handle_default(method) {
206
+ return new Response(
207
+ JSON.stringify({
208
+ jsonrpc: '2.0',
209
+ error: {
210
+ code: -32601,
211
+ message: 'Method not found',
212
+ data: `HTTP method ${method} not supported`,
213
+ },
214
+ }),
215
+ {
216
+ status: 405,
217
+ headers: {
218
+ 'Content-Type': 'application/json',
219
+ Allow: 'GET, POST, DELETE',
220
+ },
221
+ },
222
+ );
223
+ }
224
+
225
+ /**
226
+ *
227
+ * @param {Request} request
228
+ * @returns {Promise<Response | null>}
229
+ */
230
+ async respond(request) {
231
+ const url = new URL(request.url);
232
+
233
+ // Check if the request path matches the configured MCP path
234
+ if (url.pathname !== this.#path) {
235
+ return null;
236
+ }
237
+ const method = request.method;
238
+ const session_id =
239
+ request.headers.get('mcp-session-id') ||
240
+ this.#options.getSessionId();
241
+
242
+ // Handle DELETE request - disconnect session
243
+ if (method === 'DELETE') {
244
+ return this.#handle_delete(session_id);
245
+ }
246
+
247
+ // Handle GET request - establish long-lived connection for notifications
248
+ if (method === 'GET') {
249
+ return this.#handle_get(session_id);
250
+ }
251
+
252
+ // Handle POST request - process message and respond through event stream
253
+ if (method === 'POST') {
254
+ return this.#handle_post(session_id, request);
255
+ }
256
+
257
+ // Method not supported
258
+ return this.#handle_default(method);
259
+ }
102
260
  }