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/dist/bin/mcp-proxy.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{stdio-CyOk7u4Q.mjs → stdio-CvFTizsx.mjs} +4 -5
- package/dist/{stdio-CyOk7u4Q.mjs.map → stdio-CvFTizsx.mjs.map} +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/startHTTPServer.test.ts +147 -1
- package/src/startHTTPServer.ts +13 -7
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -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);
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
751
|
+
if (!res.headersSent) {
|
|
752
|
+
res.writeHead(500).end("Error handling delete request");
|
|
753
|
+
}
|
|
748
754
|
}
|
|
749
755
|
|
|
750
756
|
return true;
|