mcp-proxy 6.5.1 → 6.5.3
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/LICENSE +21 -20
- package/README.md +2 -0
- package/dist/bin/mcp-proxy.mjs +7 -1
- package/dist/bin/mcp-proxy.mjs.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +1 -1
- package/dist/{stdio-DN-u_q7a.mjs → stdio-DNR9B0BZ.mjs} +12 -3
- package/dist/stdio-DNR9B0BZ.mjs.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/bin/mcp-proxy.ts +7 -0
- package/src/startHTTPServer.test.ts +97 -0
- package/src/startHTTPServer.ts +21 -1
- package/dist/stdio-DN-u_q7a.mjs.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/bin/mcp-proxy.ts
CHANGED
|
@@ -80,6 +80,12 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
80
80
|
describe: "The host to listen on",
|
|
81
81
|
type: "string",
|
|
82
82
|
},
|
|
83
|
+
keepAliveTimeout: {
|
|
84
|
+
default: 300000,
|
|
85
|
+
describe:
|
|
86
|
+
"The HTTP keep-alive timeout in milliseconds for stateful stream sessions (default: 5 minutes)",
|
|
87
|
+
type: "number",
|
|
88
|
+
},
|
|
83
89
|
port: {
|
|
84
90
|
default: 8080,
|
|
85
91
|
describe: "The port to listen on",
|
|
@@ -244,6 +250,7 @@ const proxy = async () => {
|
|
|
244
250
|
createServer,
|
|
245
251
|
eventStore: new InMemoryEventStore(),
|
|
246
252
|
host: argv.host,
|
|
253
|
+
keepAliveTimeout: argv.keepAliveTimeout,
|
|
247
254
|
port: argv.port,
|
|
248
255
|
sseEndpoint:
|
|
249
256
|
argv.server && argv.server !== "sse"
|
|
@@ -131,6 +131,73 @@ it("proxies messages between HTTP stream and stdio servers", async () => {
|
|
|
131
131
|
expect(onClose).toHaveBeenCalled();
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
it(
|
|
135
|
+
"keeps stateful HTTP stream sessions alive after idle keep-alive timeout window",
|
|
136
|
+
async () => {
|
|
137
|
+
const port = await getRandomPort();
|
|
138
|
+
const onClose = vi.fn().mockResolvedValue(undefined);
|
|
139
|
+
|
|
140
|
+
const httpServer = await startHTTPServer({
|
|
141
|
+
createServer: async () => {
|
|
142
|
+
return new Server(
|
|
143
|
+
{ name: "test", version: "1.0.0" },
|
|
144
|
+
{ capabilities: {} },
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
onClose,
|
|
148
|
+
port,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const initializeResponse = await fetch(`http://localhost:${port}/mcp`, {
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
id: 1,
|
|
154
|
+
jsonrpc: "2.0",
|
|
155
|
+
method: "initialize",
|
|
156
|
+
params: {
|
|
157
|
+
capabilities: {},
|
|
158
|
+
clientInfo: { name: "test", version: "1.0.0" },
|
|
159
|
+
protocolVersion: "2025-03-26",
|
|
160
|
+
},
|
|
161
|
+
}),
|
|
162
|
+
headers: {
|
|
163
|
+
Accept: "application/json, text/event-stream",
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
},
|
|
166
|
+
method: "POST",
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
expect(initializeResponse.status).toBe(200);
|
|
170
|
+
const sessionId = initializeResponse.headers.get("mcp-session-id");
|
|
171
|
+
expect(sessionId).toBeTruthy();
|
|
172
|
+
await initializeResponse.text();
|
|
173
|
+
|
|
174
|
+
await delay(6_000);
|
|
175
|
+
|
|
176
|
+
const listToolsResponse = await fetch(`http://localhost:${port}/mcp`, {
|
|
177
|
+
body: JSON.stringify({
|
|
178
|
+
id: 2,
|
|
179
|
+
jsonrpc: "2.0",
|
|
180
|
+
method: "tools/list",
|
|
181
|
+
params: {},
|
|
182
|
+
}),
|
|
183
|
+
headers: {
|
|
184
|
+
Accept: "application/json, text/event-stream",
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
"mcp-session-id": sessionId!,
|
|
187
|
+
},
|
|
188
|
+
method: "POST",
|
|
189
|
+
});
|
|
190
|
+
const listToolsBody = await listToolsResponse.text();
|
|
191
|
+
|
|
192
|
+
expect(listToolsResponse.status).not.toBe(404);
|
|
193
|
+
expect(listToolsBody).not.toContain("Session not found");
|
|
194
|
+
expect(onClose).not.toHaveBeenCalled();
|
|
195
|
+
|
|
196
|
+
await httpServer.close();
|
|
197
|
+
},
|
|
198
|
+
15_000,
|
|
199
|
+
);
|
|
200
|
+
|
|
134
201
|
it("proxies messages between SSE and stdio servers", async () => {
|
|
135
202
|
const stdioTransport = new StdioClientTransport({
|
|
136
203
|
args: ["src/fixtures/simple-stdio-server.ts"],
|
|
@@ -667,6 +734,36 @@ it("does not require auth for /ping endpoint", async () => {
|
|
|
667
734
|
await httpServer.close();
|
|
668
735
|
});
|
|
669
736
|
|
|
737
|
+
it("responds with 400 to a malformed request target instead of crashing", async () => {
|
|
738
|
+
const port = await getRandomPort();
|
|
739
|
+
|
|
740
|
+
const httpServer = await startHTTPServer({
|
|
741
|
+
createServer: async () => {
|
|
742
|
+
return new Server({ name: "test", version: "1.0.0" }, { capabilities: {} });
|
|
743
|
+
},
|
|
744
|
+
port,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
// `//` is not a valid URL target; sent via http.request so it isn't
|
|
748
|
+
// normalized away. Before the fix this threw inside the request listener
|
|
749
|
+
// and crashed the process.
|
|
750
|
+
const statusCode = await new Promise<number>((resolve, reject) => {
|
|
751
|
+
const request = http.request(
|
|
752
|
+
{ host: "localhost", path: "//", port },
|
|
753
|
+
(res) => {
|
|
754
|
+
res.resume();
|
|
755
|
+
resolve(res.statusCode ?? 0);
|
|
756
|
+
},
|
|
757
|
+
);
|
|
758
|
+
request.on("error", reject);
|
|
759
|
+
request.end();
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
expect(statusCode).toBe(400);
|
|
763
|
+
|
|
764
|
+
await httpServer.close();
|
|
765
|
+
});
|
|
766
|
+
|
|
670
767
|
it("does not require auth for OPTIONS requests", async () => {
|
|
671
768
|
const port = await getRandomPort();
|
|
672
769
|
const apiKey = "test-api-key-999";
|
package/src/startHTTPServer.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { randomUUID } from "node:crypto";
|
|
|
13
13
|
import { AuthConfig, AuthenticationMiddleware } from "./authentication.js";
|
|
14
14
|
import { InMemoryEventStore } from "./InMemoryEventStore.js";
|
|
15
15
|
|
|
16
|
+
const DEFAULT_KEEP_ALIVE_TIMEOUT = 300_000;
|
|
17
|
+
|
|
16
18
|
export interface CorsOptions {
|
|
17
19
|
allowedHeaders?: string | string[]; // Allow string[] or '*' for wildcard
|
|
18
20
|
credentials?: boolean;
|
|
@@ -881,6 +883,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
881
883
|
enableJsonResponse,
|
|
882
884
|
eventStore,
|
|
883
885
|
host = "::",
|
|
886
|
+
keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT,
|
|
884
887
|
oauth,
|
|
885
888
|
onClose,
|
|
886
889
|
onConnect,
|
|
@@ -900,6 +903,7 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
900
903
|
enableJsonResponse?: boolean;
|
|
901
904
|
eventStore?: EventStore;
|
|
902
905
|
host?: string;
|
|
906
|
+
keepAliveTimeout?: number;
|
|
903
907
|
oauth?: AuthConfig["oauth"];
|
|
904
908
|
onClose?: (server: T) => Promise<void>;
|
|
905
909
|
onConnect?: (server: T) => Promise<void>;
|
|
@@ -951,7 +955,15 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
951
955
|
// and would otherwise short-circuit the MCP protocol handlers.
|
|
952
956
|
// Use a fixed base because `host` may be "::" (IPv6 any), which is not a
|
|
953
957
|
// valid URL authority. We only need pathname here.
|
|
954
|
-
|
|
958
|
+
// A malformed request target (e.g. "//") makes `new URL` throw, which
|
|
959
|
+
// would crash the process from this listener, so reject it with 400.
|
|
960
|
+
let requestUrl: URL;
|
|
961
|
+
try {
|
|
962
|
+
requestUrl = new URL(req.url || "", "http://localhost");
|
|
963
|
+
} catch {
|
|
964
|
+
res.writeHead(400).end("Bad Request");
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
955
967
|
const isMcpEndpoint =
|
|
956
968
|
(sseEndpoint && requestUrl.pathname === sseEndpoint) ||
|
|
957
969
|
(streamEndpoint && requestUrl.pathname === streamEndpoint);
|
|
@@ -1047,6 +1059,14 @@ export const startHTTPServer = async <T extends ServerLike>({
|
|
|
1047
1059
|
httpServer = http.createServer(requestListener);
|
|
1048
1060
|
}
|
|
1049
1061
|
|
|
1062
|
+
// Keep stateful stream sessions from being torn down when Node closes
|
|
1063
|
+
// otherwise-idle HTTP keep-alive sockets after its 5 second default.
|
|
1064
|
+
httpServer.keepAliveTimeout = keepAliveTimeout;
|
|
1065
|
+
httpServer.headersTimeout = Math.max(
|
|
1066
|
+
httpServer.headersTimeout,
|
|
1067
|
+
keepAliveTimeout + 1000,
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1050
1070
|
await new Promise((resolve) => {
|
|
1051
1071
|
httpServer.listen(port, host, () => {
|
|
1052
1072
|
resolve(undefined);
|