mcp-proxy 6.5.2 → 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/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.5.2"
6
+ "version": "6.5.3"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proxy",
3
- "version": "6.5.2",
3
+ "version": "6.5.3",
4
4
  "main": "dist/index.mjs",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -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"],
@@ -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>;
@@ -1055,6 +1059,14 @@ export const startHTTPServer = async <T extends ServerLike>({
1055
1059
  httpServer = http.createServer(requestListener);
1056
1060
  }
1057
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
+
1058
1070
  await new Promise((resolve) => {
1059
1071
  httpServer.listen(port, host, () => {
1060
1072
  resolve(undefined);