mcp-proxy 6.4.0 → 6.4.2

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/jsr.json CHANGED
@@ -3,5 +3,5 @@
3
3
  "include": ["src/index.ts", "src/bin/mcp-proxy.ts"],
4
4
  "license": "MIT",
5
5
  "name": "@punkpeye/mcp-proxy",
6
- "version": "6.4.0"
6
+ "version": "6.4.2"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proxy",
3
- "version": "6.4.0",
3
+ "version": "6.4.2",
4
4
  "main": "dist/index.mjs",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -6,6 +6,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
6
  import { EventSource } from "eventsource";
7
7
  import fs from "fs";
8
8
  import { getRandomPort } from "get-port-please";
9
+ import http from "http";
9
10
  import https from "https";
10
11
  import { setTimeout as delay } from "node:timers/promises";
11
12
  import { expect, it, vi } from "vitest";
@@ -2047,7 +2048,7 @@ it("uses default CORS settings when cors: true", async () => {
2047
2048
  expect(response.status).toBe(204);
2048
2049
  expect(response.headers.get("Access-Control-Allow-Origin")).toBe("*");
2049
2050
  expect(response.headers.get("Access-Control-Allow-Headers")).toBe(
2050
- "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
2051
+ "Content-Type, Authorization, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-Id",
2051
2052
  );
2052
2053
  expect(response.headers.get("Access-Control-Allow-Credentials")).toBe("true");
2053
2054
 
@@ -2148,3 +2149,148 @@ it("supports creating an SSL server", async () => {
2148
2149
 
2149
2150
  await httpServer.close();
2150
2151
  });
2152
+
2153
+ it("DELETE request terminates session cleanly and calls onClose exactly once", async () => {
2154
+ const stdioTransport = new StdioClientTransport({
2155
+ args: ["src/fixtures/simple-stdio-server.ts"],
2156
+ command: "tsx",
2157
+ });
2158
+
2159
+ const stdioClient = new Client(
2160
+ { name: "mcp-proxy", version: "1.0.0" },
2161
+ { capabilities: {} },
2162
+ );
2163
+
2164
+ await stdioClient.connect(stdioTransport);
2165
+
2166
+ const serverVersion = stdioClient.getServerVersion() as {
2167
+ name: string;
2168
+ version: string;
2169
+ };
2170
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
2171
+ capabilities: Record<string, unknown>;
2172
+ };
2173
+
2174
+ const port = await getRandomPort();
2175
+ const onClose = vi.fn().mockResolvedValue(undefined);
2176
+ const onConnect = vi.fn().mockResolvedValue(undefined);
2177
+
2178
+ const httpServer = await startHTTPServer({
2179
+ createServer: async () => {
2180
+ const mcpServer = new Server(serverVersion, {
2181
+ capabilities: serverCapabilities,
2182
+ });
2183
+ await proxyServer({
2184
+ client: stdioClient,
2185
+ server: mcpServer,
2186
+ serverCapabilities,
2187
+ });
2188
+ return mcpServer;
2189
+ },
2190
+ onClose,
2191
+ onConnect,
2192
+ port,
2193
+ });
2194
+
2195
+ const streamClient = new Client(
2196
+ { name: "stream-client", version: "1.0.0" },
2197
+ { capabilities: {} },
2198
+ );
2199
+
2200
+ const transport = new StreamableHTTPClientTransport(
2201
+ new URL(`http://localhost:${port}/mcp`),
2202
+ );
2203
+
2204
+ await streamClient.connect(transport);
2205
+
2206
+ // Verify the session works
2207
+ const result = await streamClient.listResources();
2208
+ expect(result.resources).toHaveLength(1);
2209
+
2210
+ expect(onConnect).toHaveBeenCalled();
2211
+ expect(onClose).not.toHaveBeenCalled();
2212
+
2213
+ // Send DELETE to terminate the session — this should not cause ECONNRESET
2214
+ await transport.terminateSession();
2215
+ await streamClient.close();
2216
+
2217
+ await delay(500);
2218
+
2219
+ // onClose should be called exactly once, not twice
2220
+ expect(onClose).toHaveBeenCalledTimes(1);
2221
+
2222
+ await httpServer.close();
2223
+ await stdioClient.close();
2224
+ }, 15000);
2225
+
2226
+ it("DELETE request to non-existent session returns 400", async () => {
2227
+ const stdioTransport = new StdioClientTransport({
2228
+ args: ["src/fixtures/simple-stdio-server.ts"],
2229
+ command: "tsx",
2230
+ });
2231
+
2232
+ const stdioClient = new Client(
2233
+ { name: "mcp-proxy", version: "1.0.0" },
2234
+ { capabilities: {} },
2235
+ );
2236
+
2237
+ await stdioClient.connect(stdioTransport);
2238
+
2239
+ const serverVersion = stdioClient.getServerVersion() as {
2240
+ name: string;
2241
+ version: string;
2242
+ };
2243
+ const serverCapabilities = stdioClient.getServerCapabilities() as {
2244
+ capabilities: Record<string, unknown>;
2245
+ };
2246
+
2247
+ const port = await getRandomPort();
2248
+
2249
+ const httpServer = await startHTTPServer({
2250
+ createServer: async () => {
2251
+ const mcpServer = new Server(serverVersion, {
2252
+ capabilities: serverCapabilities,
2253
+ });
2254
+ await proxyServer({
2255
+ client: stdioClient,
2256
+ server: mcpServer,
2257
+ serverCapabilities,
2258
+ });
2259
+ return mcpServer;
2260
+ },
2261
+ port,
2262
+ });
2263
+
2264
+ // Send DELETE with a fake session ID
2265
+ const response = await new Promise<{ statusCode: number; text: string }>(
2266
+ (resolve, reject) => {
2267
+ const req = http.request(
2268
+ {
2269
+ headers: {
2270
+ "mcp-session-id": "non-existent-session-id",
2271
+ },
2272
+ hostname: "localhost",
2273
+ method: "DELETE",
2274
+ path: "/mcp",
2275
+ port,
2276
+ },
2277
+ (res) => {
2278
+ let text = "";
2279
+ res.on("data", (chunk: Buffer) => {
2280
+ text += chunk.toString();
2281
+ });
2282
+ res.on("end", () => {
2283
+ resolve({ statusCode: res.statusCode!, text });
2284
+ });
2285
+ },
2286
+ );
2287
+ req.on("error", reject);
2288
+ req.end();
2289
+ },
2290
+ );
2291
+
2292
+ expect(response.statusCode).toBe(400);
2293
+
2294
+ await httpServer.close();
2295
+ await stdioClient.close();
2296
+ }, 15000);
@@ -219,7 +219,7 @@ const applyCorsHeaders = (
219
219
  // Default CORS configuration for backward compatibility
220
220
  const defaultCorsOptions: CorsOptions = {
221
221
  allowedHeaders:
222
- "Content-Type, Authorization, Accept, Mcp-Session-Id, Last-Event-Id",
222
+ "Content-Type, Authorization, Accept, Mcp-Session-Id, Mcp-Protocol-Version, Last-Event-Id",
223
223
  credentials: true,
224
224
  exposedHeaders: ["Mcp-Session-Id"],
225
225
  methods: ["GET", "POST", "OPTIONS"],
@@ -348,9 +348,12 @@ const handleStreamRequest = async <T extends ServerLike>({
348
348
  ) {
349
349
  let body: unknown;
350
350
  try {
351
- const sessionId = Array.isArray(req.headers["mcp-session-id"])
352
- ? req.headers["mcp-session-id"][0]
353
- : req.headers["mcp-session-id"];
351
+ // In stateless mode, ignore session ID header entirely (like Python MCP SDK)
352
+ const sessionId = stateless
353
+ ? undefined
354
+ : (Array.isArray(req.headers["mcp-session-id"])
355
+ ? req.headers["mcp-session-id"][0]
356
+ : req.headers["mcp-session-id"]);
354
357
 
355
358
  let transport: StreamableHTTPServerTransport;
356
359
 
@@ -738,13 +741,16 @@ const handleStreamRequest = async <T extends ServerLike>({
738
741
  }
739
742
 
740
743
  try {
744
+ // handleRequest for DELETE calls transport.close() internally,
745
+ // which triggers the transport.onclose callback that already
746
+ // handles server cleanup. No need to call cleanupServer again.
741
747
  await activeTransport.transport.handleRequest(req, res);
742
-
743
- await cleanupServer(activeTransport.server, onClose);
744
748
  } catch (error) {
745
749
  console.error("[mcp-proxy] error handling delete request", error);
746
750
 
747
- res.writeHead(500).end("Error handling delete request");
751
+ if (!res.headersSent) {
752
+ res.writeHead(500).end("Error handling delete request");
753
+ }
748
754
  }
749
755
 
750
756
  return true;