mcp-proxy 6.4.2 → 6.4.4

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.2"
6
+ "version": "6.4.4"
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-proxy",
3
- "version": "6.4.2",
3
+ "version": "6.4.4",
4
4
  "main": "dist/index.mjs",
5
5
  "scripts": {
6
6
  "build": "tsdown",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@eslint/js": "^9.39.1",
42
- "@modelcontextprotocol/sdk": "^1.24.3",
42
+ "@modelcontextprotocol/sdk": "^1.27.1",
43
43
  "@sebbo2002/semantic-release-jsr": "^3.1.0",
44
44
  "@tsconfig/node24": "^24.0.3",
45
45
  "@types/express": "^5.0.6",
@@ -41,7 +41,7 @@ describe("JSONFilterTransform", () => {
41
41
  expect(outputLines).toHaveLength(4);
42
42
  expect(outputLines[0]).toBe('{"type": "request", "id": 1}');
43
43
  expect(outputLines[1]).toBe('{"type": "response", "id": 2}');
44
- expect(outputLines[2]).toBe(' {"type": "notification"} ');
44
+ expect(outputLines[2]).toBe('{"type": "notification"}');
45
45
  expect(outputLines[3]).toBe('{"type": "request", "id": 3}');
46
46
 
47
47
  // Should have warned about non-JSON lines
@@ -50,7 +50,6 @@ describe("JSONFilterTransform", () => {
50
50
  expect.arrayContaining([
51
51
  "This is not JSON",
52
52
  "Another non-JSON line",
53
- "",
54
53
  "Error: something went wrong",
55
54
  ]),
56
55
  );
@@ -58,6 +57,52 @@ describe("JSONFilterTransform", () => {
58
57
  consoleWarnSpy.mockRestore();
59
58
  });
60
59
 
60
+ it("extracts JSON from lines with non-JSON prefixes", async () => {
61
+ const input = [
62
+ 'Loading...{"type": "request", "id": 1}',
63
+ 'WARNING: deprecation{"type": "response", "id": 2}',
64
+ '{"type": "clean", "id": 3}',
65
+ "Pure noise with no JSON",
66
+ ].join("\n");
67
+
68
+ const consoleWarnSpy = vi
69
+ .spyOn(console, "warn")
70
+ .mockImplementation(() => {});
71
+
72
+ const readable = Readable.from([input]);
73
+ const transform = new JSONFilterTransform();
74
+ const chunks: Buffer[] = [];
75
+
76
+ const writable = new Writable({
77
+ write(chunk, _encoding, callback) {
78
+ chunks.push(chunk);
79
+ callback();
80
+ },
81
+ });
82
+
83
+ await pipeline(readable, transform, writable);
84
+
85
+ const output = Buffer.concat(chunks).toString();
86
+ const outputLines = output.trim().split("\n");
87
+
88
+ expect(outputLines).toHaveLength(3);
89
+ expect(outputLines[0]).toBe('{"type": "request", "id": 1}');
90
+ expect(outputLines[1]).toBe('{"type": "response", "id": 2}');
91
+ expect(outputLines[2]).toBe('{"type": "clean", "id": 3}');
92
+
93
+ // Should have warned about stripped prefixes
94
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
95
+ "[mcp-proxy] stripped non-JSON prefix from output:",
96
+ "Loading...",
97
+ );
98
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
99
+ "[mcp-proxy] stripped non-JSON prefix from output:",
100
+ "WARNING: deprecation",
101
+ );
102
+
103
+ consoleWarnSpy.mockRestore();
104
+ });
105
+
61
106
  it("handles incomplete JSON lines across multiple chunks", async () => {
62
107
  // Simulate data arriving in chunks where JSON lines are split
63
108
  const chunks = [
@@ -1,8 +1,12 @@
1
1
  import { Transform } from "node:stream";
2
2
 
3
3
  /**
4
- * Filters out lines that do not start with '{' from the input stream.
5
- * We use this to drop anything that is obviously not a JSON-RPC message.
4
+ * Extracts JSON-RPC messages from a stream that may contain non-JSON output.
5
+ *
6
+ * Lines that start with '{' are passed through as-is. Lines that contain '{'
7
+ * but have a non-JSON prefix (e.g. Python warnings prepended to a JSON message)
8
+ * have the prefix stripped and the JSON portion extracted. Lines with no '{'
9
+ * are dropped entirely.
6
10
  */
7
11
  export class JSONFilterTransform extends Transform {
8
12
  private buffer = "";
@@ -13,8 +17,9 @@ export class JSONFilterTransform extends Transform {
13
17
 
14
18
  _flush(callback: (error: Error | null, chunk: Buffer | null) => void) {
15
19
  // Handle any remaining data in buffer
16
- if (this.buffer.trim().startsWith("{")) {
17
- callback(null, Buffer.from(this.buffer));
20
+ const json = extractJson(this.buffer);
21
+ if (json !== null) {
22
+ callback(null, Buffer.from(json));
18
23
  } else {
19
24
  callback(null, null);
20
25
  }
@@ -31,14 +36,14 @@ export class JSONFilterTransform extends Transform {
31
36
  // Keep the last incomplete line in the buffer
32
37
  this.buffer = lines.pop() || "";
33
38
 
34
- // Filter lines that start with '{'
35
39
  const jsonLines = [];
36
40
  const nonJsonLines = [];
37
41
 
38
42
  for (const line of lines) {
39
- if (line.trim().startsWith("{")) {
40
- jsonLines.push(line);
41
- } else {
43
+ const json = extractJson(line);
44
+ if (json !== null) {
45
+ jsonLines.push(json);
46
+ } else if (line.trim().length > 0) {
42
47
  nonJsonLines.push(line);
43
48
  }
44
49
  }
@@ -57,3 +62,31 @@ export class JSONFilterTransform extends Transform {
57
62
  }
58
63
  }
59
64
  }
65
+
66
+ /**
67
+ * Extracts the JSON portion from a line that may have a non-JSON prefix.
68
+ * Returns null if the line contains no '{'.
69
+ */
70
+ function extractJson(line: string): null | string {
71
+ const trimmed = line.trim();
72
+ if (trimmed.length === 0) {
73
+ return null;
74
+ }
75
+
76
+ const braceIndex = trimmed.indexOf("{");
77
+ if (braceIndex === -1) {
78
+ return null;
79
+ }
80
+
81
+ if (braceIndex === 0) {
82
+ return trimmed;
83
+ }
84
+
85
+ // There's a non-JSON prefix — strip it and return from the first '{'
86
+ const jsonPart = trimmed.slice(braceIndex);
87
+ console.warn(
88
+ "[mcp-proxy] stripped non-JSON prefix from output:",
89
+ trimmed.slice(0, braceIndex),
90
+ );
91
+ return jsonPart;
92
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
5
6
  import { EventSource } from "eventsource";
6
7
  import { createRequire } from "node:module";
7
8
  import { setTimeout } from "node:timers";
@@ -196,9 +197,7 @@ const proxy = async () => {
196
197
  version: string;
197
198
  };
198
199
 
199
- const serverCapabilities = client.getServerCapabilities() as {
200
- capabilities: Record<string, unknown>;
201
- };
200
+ const serverCapabilities = client.getServerCapabilities() as ServerCapabilities;
202
201
 
203
202
  console.info("starting server on port %d", argv.port);
204
203
 
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Simulates a Python-like MCP server that emits non-JSON output to stdout
4
+ * before and during startup. This mimics the behavior of Python servers
5
+ * where warnings, import messages, or unbuffered print() calls can
6
+ * pollute the stdout stream that mcp-proxy reads as JSON-RPC.
7
+ *
8
+ * Related: https://github.com/punkpeye/mcp-proxy/issues/55
9
+ */
10
+
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import {
14
+ CallToolRequestSchema,
15
+ ListToolsRequestSchema,
16
+ } from "@modelcontextprotocol/sdk/types.js";
17
+
18
+ // Simulate non-JSON output on stdout before the server starts,
19
+ // like Python import warnings or startup messages
20
+ const noiseBefore = process.env.NOISE_BEFORE;
21
+ if (noiseBefore) {
22
+ process.stdout.write(noiseBefore);
23
+ }
24
+
25
+ const server = new Server(
26
+ {
27
+ name: "noisy-stdout-server",
28
+ version: "1.0.0",
29
+ },
30
+ {
31
+ capabilities: {
32
+ tools: {},
33
+ },
34
+ },
35
+ );
36
+
37
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
38
+ // Simulate noise interleaved with normal responses
39
+ const noiseDuring = process.env.NOISE_DURING;
40
+ if (noiseDuring) {
41
+ process.stdout.write(noiseDuring);
42
+ }
43
+
44
+ return {
45
+ tools: [
46
+ {
47
+ description: "A test tool",
48
+ inputSchema: {
49
+ properties: {},
50
+ type: "object",
51
+ },
52
+ name: "ping",
53
+ },
54
+ ],
55
+ };
56
+ });
57
+
58
+ server.setRequestHandler(CallToolRequestSchema, async () => {
59
+ return {
60
+ content: [
61
+ {
62
+ text: "pong",
63
+ type: "text" as const,
64
+ },
65
+ ],
66
+ };
67
+ });
68
+
69
+ const transport = new StdioServerTransport();
70
+ await server.connect(transport);
@@ -2,7 +2,7 @@ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
2
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
3
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
- import { McpError } from "@modelcontextprotocol/sdk/types.js";
5
+ import { McpError, ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { EventSource } from "eventsource";
7
7
  import { getRandomPort } from "get-port-please";
8
8
  import { describe, expect, it } from "vitest";
@@ -59,9 +59,7 @@ async function createTestEnvironment(
59
59
  name: string;
60
60
  version: string;
61
61
  };
62
- const serverCapabilities = stdioClient.getServerCapabilities() as {
63
- capabilities: Record<string, unknown>;
64
- };
62
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
65
63
  const port = await getRandomPort();
66
64
 
67
65
  const httpServer = await startHTTPServer({
@@ -3,6 +3,7 @@ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
3
3
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
4
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
5
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
6
+ import { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
6
7
  import { EventSource } from "eventsource";
7
8
  import fs from "fs";
8
9
  import { getRandomPort } from "get-port-please";
@@ -42,9 +43,7 @@ it("proxies messages between HTTP stream and stdio servers", async () => {
42
43
  version: string;
43
44
  };
44
45
 
45
- const serverCapabilities = stdioClient.getServerCapabilities() as {
46
- capabilities: Record<string, unknown>;
47
- };
46
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
48
47
 
49
48
  const port = await getRandomPort();
50
49
 
@@ -155,9 +154,7 @@ it("proxies messages between SSE and stdio servers", async () => {
155
154
  version: string;
156
155
  };
157
156
 
158
- const serverCapabilities = stdioClient.getServerCapabilities() as {
159
- capabilities: Record<string, unknown>;
160
- };
157
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
161
158
 
162
159
  const port = await getRandomPort();
163
160
 
@@ -265,9 +262,7 @@ it("supports stateless HTTP streamable transport", async () => {
265
262
  version: string;
266
263
  };
267
264
 
268
- const serverCapabilities = stdioClient.getServerCapabilities() as {
269
- capabilities: Record<string, unknown>;
270
- };
265
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
271
266
 
272
267
  const port = await getRandomPort();
273
268
 
@@ -354,9 +349,7 @@ it("allows requests when no auth is configured", async () => {
354
349
  version: string;
355
350
  };
356
351
 
357
- const serverCapabilities = stdioClient.getServerCapabilities() as {
358
- capabilities: Record<string, unknown>;
359
- };
352
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
360
353
 
361
354
  const port = await getRandomPort();
362
355
 
@@ -434,9 +427,7 @@ it("rejects requests without API key when auth is enabled", async () => {
434
427
  version: string;
435
428
  };
436
429
 
437
- const serverCapabilities = stdioClient.getServerCapabilities() as {
438
- capabilities: Record<string, unknown>;
439
- };
430
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
440
431
 
441
432
  const port = await getRandomPort();
442
433
 
@@ -503,9 +494,7 @@ it("accepts requests with valid API key", async () => {
503
494
  version: string;
504
495
  };
505
496
 
506
- const serverCapabilities = stdioClient.getServerCapabilities() as {
507
- capabilities: Record<string, unknown>;
508
- };
497
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
509
498
 
510
499
  const port = await getRandomPort();
511
500
  const apiKey = "test-api-key-123";
@@ -591,9 +580,7 @@ it("works with SSE transport and authentication", async () => {
591
580
  version: string;
592
581
  };
593
582
 
594
- const serverCapabilities = stdioClient.getServerCapabilities() as {
595
- capabilities: Record<string, unknown>;
596
- };
583
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
597
584
 
598
585
  const port = await getRandomPort();
599
586
  const apiKey = "test-api-key-456";
@@ -730,9 +717,7 @@ it("accepts requests with valid Bearer token in stateless mode", async () => {
730
717
  version: string;
731
718
  };
732
719
 
733
- const serverCapabilities = stdioClient.getServerCapabilities() as {
734
- capabilities: Record<string, unknown>;
735
- };
720
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
736
721
 
737
722
  const port = await getRandomPort();
738
723
 
@@ -825,9 +810,7 @@ it("returns 401 when authenticate callback returns null in stateless mode", asyn
825
810
  version: string;
826
811
  };
827
812
 
828
- const serverCapabilities = stdioClient.getServerCapabilities() as {
829
- capabilities: Record<string, unknown>;
830
- };
813
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
831
814
 
832
815
  const port = await getRandomPort();
833
816
 
@@ -908,9 +891,7 @@ it("returns 401 when authenticate callback throws error in stateless mode", asyn
908
891
  version: string;
909
892
  };
910
893
 
911
- const serverCapabilities = stdioClient.getServerCapabilities() as {
912
- capabilities: Record<string, unknown>;
913
- };
894
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
914
895
 
915
896
  const port = await getRandomPort();
916
897
 
@@ -993,9 +974,7 @@ it("calls authenticate on every request in stateful mode", async () => {
993
974
  version: string;
994
975
  };
995
976
 
996
- const serverCapabilities = stdioClient.getServerCapabilities() as {
997
- capabilities: Record<string, unknown>;
998
- };
977
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
999
978
 
1000
979
  const port = await getRandomPort();
1001
980
 
@@ -1083,9 +1062,7 @@ it("calls authenticate on every request in stateless mode", async () => {
1083
1062
  version: string;
1084
1063
  };
1085
1064
 
1086
- const serverCapabilities = stdioClient.getServerCapabilities() as {
1087
- capabilities: Record<string, unknown>;
1088
- };
1065
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
1089
1066
 
1090
1067
  const port = await getRandomPort();
1091
1068
 
@@ -1211,9 +1188,7 @@ it("returns 401 when authenticate callback returns { authenticated: false } in s
1211
1188
  version: string;
1212
1189
  };
1213
1190
 
1214
- const serverCapabilities = stdioClient.getServerCapabilities() as {
1215
- capabilities: Record<string, unknown>;
1216
- };
1191
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
1217
1192
 
1218
1193
  const port = await getRandomPort();
1219
1194
 
@@ -1297,9 +1272,7 @@ it("returns 401 with custom error message when { authenticated: false, error: '.
1297
1272
  version: string;
1298
1273
  };
1299
1274
 
1300
- const serverCapabilities = stdioClient.getServerCapabilities() as {
1301
- capabilities: Record<string, unknown>;
1302
- };
1275
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
1303
1276
 
1304
1277
  const port = await getRandomPort();
1305
1278
 
@@ -1766,9 +1739,7 @@ it("succeeds when authenticate returns { authenticated: true } in stateless mode
1766
1739
  version: string;
1767
1740
  };
1768
1741
 
1769
- const serverCapabilities = stdioClient.getServerCapabilities() as {
1770
- capabilities: Record<string, unknown>;
1771
- };
1742
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
1772
1743
 
1773
1744
  const port = await getRandomPort();
1774
1745
 
@@ -2167,9 +2138,7 @@ it("DELETE request terminates session cleanly and calls onClose exactly once", a
2167
2138
  name: string;
2168
2139
  version: string;
2169
2140
  };
2170
- const serverCapabilities = stdioClient.getServerCapabilities() as {
2171
- capabilities: Record<string, unknown>;
2172
- };
2141
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
2173
2142
 
2174
2143
  const port = await getRandomPort();
2175
2144
  const onClose = vi.fn().mockResolvedValue(undefined);
@@ -2240,9 +2209,7 @@ it("DELETE request to non-existent session returns 400", async () => {
2240
2209
  name: string;
2241
2210
  version: string;
2242
2211
  };
2243
- const serverCapabilities = stdioClient.getServerCapabilities() as {
2244
- capabilities: Record<string, unknown>;
2245
- };
2212
+ const serverCapabilities = stdioClient.getServerCapabilities() as ServerCapabilities;
2246
2213
 
2247
2214
  const port = await getRandomPort();
2248
2215
 
@@ -5,6 +5,7 @@ import { StreamableHTTPClientTransportOptions } from "@modelcontextprotocol/sdk/
5
5
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
6
6
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { ServerCapabilities } from "@modelcontextprotocol/sdk/types.js";
8
9
 
9
10
  import { proxyServer } from "./proxyServer.js";
10
11
 
@@ -58,9 +59,7 @@ export const startStdioServer = async ({
58
59
  version: string;
59
60
  };
60
61
 
61
- const serverCapabilities = streamClient.getServerCapabilities() as {
62
- capabilities: Record<string, unknown>;
63
- };
62
+ const serverCapabilities = streamClient.getServerCapabilities() as ServerCapabilities;
64
63
 
65
64
  const stdioServer = initStdioServer
66
65
  ? await initStdioServer()