mcp-proxy 5.6.1 → 5.8.0
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/README.md +1 -0
- package/dist/bin/mcp-proxy.js +7 -1
- package/dist/bin/mcp-proxy.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -1
- package/dist/{stdio-D0Lv8ytu.js → stdio-so1-I7Pn.js} +43 -16
- package/dist/stdio-so1-I7Pn.js.map +1 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/bin/mcp-proxy.ts +6 -0
- package/src/fixtures/slow-stdio-server.ts +91 -0
- package/src/proxyServer.test.ts +148 -0
- package/src/proxyServer.ts +43 -10
- package/src/startHTTPServer.test.ts +480 -0
- package/src/startHTTPServer.ts +42 -2
- package/dist/stdio-D0Lv8ytu.js.map +0 -1
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/bin/mcp-proxy.ts
CHANGED
|
@@ -66,6 +66,11 @@ const argv = await yargs(hideBin(process.argv))
|
|
|
66
66
|
describe: "The port to listen on",
|
|
67
67
|
type: "number",
|
|
68
68
|
},
|
|
69
|
+
requestTimeout: {
|
|
70
|
+
default: 300000,
|
|
71
|
+
describe: "The timeout (in milliseconds) for requests to the MCP server (default: 5 minutes)",
|
|
72
|
+
type: "number",
|
|
73
|
+
},
|
|
69
74
|
server: {
|
|
70
75
|
choices: ["sse", "stream"],
|
|
71
76
|
describe:
|
|
@@ -156,6 +161,7 @@ const proxy = async () => {
|
|
|
156
161
|
|
|
157
162
|
proxyServer({
|
|
158
163
|
client,
|
|
164
|
+
requestTimeout: argv.requestTimeout,
|
|
159
165
|
server,
|
|
160
166
|
serverCapabilities,
|
|
161
167
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* A test fixture that simulates a slow MCP server for testing timeout functionality.
|
|
4
|
+
* This server intentionally delays responses to test timeout behavior.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
8
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
9
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
10
|
+
|
|
11
|
+
const server = new Server(
|
|
12
|
+
{
|
|
13
|
+
name: "slow-test-server",
|
|
14
|
+
version: "1.0.0",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
capabilities: {
|
|
18
|
+
resources: {},
|
|
19
|
+
tools: {},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
// Configure delay via environment variable or default to 2 seconds
|
|
25
|
+
const RESPONSE_DELAY = parseInt(process.env.RESPONSE_DELAY || "2000", 10);
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
CallToolRequestSchema,
|
|
29
|
+
ListResourcesRequestSchema,
|
|
30
|
+
ListToolsRequestSchema,
|
|
31
|
+
ReadResourceRequestSchema,
|
|
32
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
33
|
+
|
|
34
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
35
|
+
await delay(RESPONSE_DELAY);
|
|
36
|
+
return {
|
|
37
|
+
resources: [
|
|
38
|
+
{
|
|
39
|
+
name: "Slow Resource",
|
|
40
|
+
uri: "file:///slow.txt",
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
server.setRequestHandler(ReadResourceRequestSchema, async ({ params }) => {
|
|
47
|
+
await delay(RESPONSE_DELAY);
|
|
48
|
+
return {
|
|
49
|
+
contents: [
|
|
50
|
+
{
|
|
51
|
+
text: `Content from slow server after ${RESPONSE_DELAY}ms delay`,
|
|
52
|
+
uri: params.uri,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
59
|
+
await delay(RESPONSE_DELAY);
|
|
60
|
+
return {
|
|
61
|
+
tools: [
|
|
62
|
+
{
|
|
63
|
+
description: "A slow test tool",
|
|
64
|
+
inputSchema: {
|
|
65
|
+
properties: {
|
|
66
|
+
input: {
|
|
67
|
+
type: "string",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
type: "object",
|
|
71
|
+
},
|
|
72
|
+
name: "slowTool",
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
server.setRequestHandler(CallToolRequestSchema, async ({ params }) => {
|
|
79
|
+
await delay(RESPONSE_DELAY);
|
|
80
|
+
return {
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
text: `Tool response after ${RESPONSE_DELAY}ms delay: ${params.arguments?.input}`,
|
|
84
|
+
type: "text" as const,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const transport = new StdioServerTransport();
|
|
91
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
3
|
+
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
4
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
5
|
+
import { McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
import { EventSource } from "eventsource";
|
|
7
|
+
import { getRandomPort } from "get-port-please";
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
|
|
10
|
+
import { proxyServer } from "./proxyServer.js";
|
|
11
|
+
import { startHTTPServer } from "./startHTTPServer.js";
|
|
12
|
+
|
|
13
|
+
if (!("EventSource" in global)) {
|
|
14
|
+
// @ts-expect-error - figure out how to use --experimental-eventsource with vitest
|
|
15
|
+
global.EventSource = EventSource;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface TestConfig {
|
|
19
|
+
requestTimeout?: number;
|
|
20
|
+
serverDelay?: string;
|
|
21
|
+
serverFixture?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface TestEnvironment {
|
|
25
|
+
cleanup: () => Promise<void>;
|
|
26
|
+
httpServer: { close: () => Promise<void> };
|
|
27
|
+
stdioClient: Client;
|
|
28
|
+
streamClient: Client;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function createTestEnvironment(config: TestConfig = {}): Promise<TestEnvironment> {
|
|
32
|
+
const {
|
|
33
|
+
requestTimeout,
|
|
34
|
+
serverDelay,
|
|
35
|
+
serverFixture = "simple-stdio-server.ts"
|
|
36
|
+
} = config;
|
|
37
|
+
|
|
38
|
+
const stdioTransport = new StdioClientTransport({
|
|
39
|
+
args: [`src/fixtures/${serverFixture}`],
|
|
40
|
+
command: "tsx",
|
|
41
|
+
env: serverDelay ? { ...process.env, RESPONSE_DELAY: serverDelay } as Record<string, string> : process.env as Record<string, string>,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const stdioClient = new Client(
|
|
45
|
+
{ name: "mcp-proxy-test", version: "1.0.0" },
|
|
46
|
+
{ capabilities: {} }
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
await stdioClient.connect(stdioTransport);
|
|
50
|
+
|
|
51
|
+
const serverVersion = stdioClient.getServerVersion() as { name: string; version: string };
|
|
52
|
+
const serverCapabilities = stdioClient.getServerCapabilities() as { capabilities: Record<string, unknown> };
|
|
53
|
+
const port = await getRandomPort();
|
|
54
|
+
|
|
55
|
+
const httpServer = await startHTTPServer({
|
|
56
|
+
createServer: async () => {
|
|
57
|
+
const mcpServer = new Server(serverVersion, { capabilities: serverCapabilities });
|
|
58
|
+
await proxyServer({
|
|
59
|
+
client: stdioClient,
|
|
60
|
+
requestTimeout,
|
|
61
|
+
server: mcpServer,
|
|
62
|
+
serverCapabilities,
|
|
63
|
+
});
|
|
64
|
+
return mcpServer;
|
|
65
|
+
},
|
|
66
|
+
port,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const streamClient = new Client(
|
|
70
|
+
{ name: "stream-client", version: "1.0.0" },
|
|
71
|
+
{ capabilities: {} }
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const transport = new StreamableHTTPClientTransport(new URL(`http://localhost:${port}/mcp`));
|
|
75
|
+
await streamClient.connect(transport);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
cleanup: async () => {
|
|
79
|
+
await streamClient.close();
|
|
80
|
+
await stdioClient.close();
|
|
81
|
+
},
|
|
82
|
+
httpServer,
|
|
83
|
+
stdioClient,
|
|
84
|
+
streamClient
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe("proxyServer timeout functionality", () => {
|
|
89
|
+
it("should respect custom timeout settings", async () => {
|
|
90
|
+
const { cleanup, streamClient } = await createTestEnvironment({
|
|
91
|
+
requestTimeout: 1000,
|
|
92
|
+
serverDelay: "500",
|
|
93
|
+
serverFixture: "slow-stdio-server.ts"
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// This should succeed as timeout (1s) > delay (500ms)
|
|
97
|
+
const result = await streamClient.listResources();
|
|
98
|
+
expect(result.resources).toHaveLength(1);
|
|
99
|
+
expect(result.resources[0].name).toBe("Slow Resource");
|
|
100
|
+
|
|
101
|
+
await cleanup();
|
|
102
|
+
}, 10000);
|
|
103
|
+
|
|
104
|
+
it("should timeout when request takes longer than configured timeout", async () => {
|
|
105
|
+
const { cleanup, streamClient } = await createTestEnvironment({
|
|
106
|
+
requestTimeout: 500,
|
|
107
|
+
serverDelay: "1000",
|
|
108
|
+
serverFixture: "slow-stdio-server.ts"
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// This should throw a timeout error as delay (1s) > timeout (500ms)
|
|
112
|
+
await expect(streamClient.listResources()).rejects.toThrow(McpError);
|
|
113
|
+
|
|
114
|
+
await cleanup();
|
|
115
|
+
}, 10000);
|
|
116
|
+
|
|
117
|
+
it("should use default SDK timeout when no custom timeout is provided", async () => {
|
|
118
|
+
const { cleanup, streamClient } = await createTestEnvironment();
|
|
119
|
+
|
|
120
|
+
// This should succeed with default timeout
|
|
121
|
+
const result = await streamClient.listResources();
|
|
122
|
+
expect(result.resources).toBeDefined();
|
|
123
|
+
|
|
124
|
+
await cleanup();
|
|
125
|
+
}, 10000);
|
|
126
|
+
|
|
127
|
+
it("should handle resource reads with custom timeout", async () => {
|
|
128
|
+
const { cleanup, streamClient } = await createTestEnvironment({
|
|
129
|
+
requestTimeout: 600,
|
|
130
|
+
serverDelay: "300",
|
|
131
|
+
serverFixture: "slow-stdio-server.ts"
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// First get the resources
|
|
135
|
+
const resources = await streamClient.listResources();
|
|
136
|
+
expect(resources.resources).toHaveLength(1);
|
|
137
|
+
|
|
138
|
+
// Resource read should succeed as timeout (600ms) > delay (300ms)
|
|
139
|
+
const resourceContent = await streamClient.readResource({
|
|
140
|
+
uri: resources.resources[0].uri,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(resourceContent.contents).toBeDefined();
|
|
144
|
+
expect(resourceContent.contents[0].text).toContain("300ms delay");
|
|
145
|
+
|
|
146
|
+
await cleanup();
|
|
147
|
+
}, 10000);
|
|
148
|
+
});
|
package/src/proxyServer.ts
CHANGED
|
@@ -18,10 +18,12 @@ import {
|
|
|
18
18
|
|
|
19
19
|
export const proxyServer = async ({
|
|
20
20
|
client,
|
|
21
|
+
requestTimeout,
|
|
21
22
|
server,
|
|
22
23
|
serverCapabilities,
|
|
23
24
|
}: {
|
|
24
25
|
client: Client;
|
|
26
|
+
requestTimeout?: number;
|
|
25
27
|
server: Server;
|
|
26
28
|
serverCapabilities: ServerCapabilities;
|
|
27
29
|
}): Promise<void> => {
|
|
@@ -42,28 +44,43 @@ export const proxyServer = async ({
|
|
|
42
44
|
|
|
43
45
|
if (serverCapabilities?.prompts) {
|
|
44
46
|
server.setRequestHandler(GetPromptRequestSchema, async (args) => {
|
|
45
|
-
return client.getPrompt(
|
|
47
|
+
return client.getPrompt(
|
|
48
|
+
args.params,
|
|
49
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
50
|
+
);
|
|
46
51
|
});
|
|
47
52
|
|
|
48
53
|
server.setRequestHandler(ListPromptsRequestSchema, async (args) => {
|
|
49
|
-
return client.listPrompts(
|
|
54
|
+
return client.listPrompts(
|
|
55
|
+
args.params,
|
|
56
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
57
|
+
);
|
|
50
58
|
});
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
if (serverCapabilities?.resources) {
|
|
54
62
|
server.setRequestHandler(ListResourcesRequestSchema, async (args) => {
|
|
55
|
-
return client.listResources(
|
|
63
|
+
return client.listResources(
|
|
64
|
+
args.params,
|
|
65
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
66
|
+
);
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
server.setRequestHandler(
|
|
59
70
|
ListResourceTemplatesRequestSchema,
|
|
60
71
|
async (args) => {
|
|
61
|
-
return client.listResourceTemplates(
|
|
72
|
+
return client.listResourceTemplates(
|
|
73
|
+
args.params,
|
|
74
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
75
|
+
);
|
|
62
76
|
},
|
|
63
77
|
);
|
|
64
78
|
|
|
65
79
|
server.setRequestHandler(ReadResourceRequestSchema, async (args) => {
|
|
66
|
-
return client.readResource(
|
|
80
|
+
return client.readResource(
|
|
81
|
+
args.params,
|
|
82
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
83
|
+
);
|
|
67
84
|
});
|
|
68
85
|
|
|
69
86
|
if (serverCapabilities?.resources.subscribe) {
|
|
@@ -75,26 +92,42 @@ export const proxyServer = async ({
|
|
|
75
92
|
);
|
|
76
93
|
|
|
77
94
|
server.setRequestHandler(SubscribeRequestSchema, async (args) => {
|
|
78
|
-
return client.subscribeResource(
|
|
95
|
+
return client.subscribeResource(
|
|
96
|
+
args.params,
|
|
97
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
98
|
+
);
|
|
79
99
|
});
|
|
80
100
|
|
|
81
101
|
server.setRequestHandler(UnsubscribeRequestSchema, async (args) => {
|
|
82
|
-
return client.unsubscribeResource(
|
|
102
|
+
return client.unsubscribeResource(
|
|
103
|
+
args.params,
|
|
104
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
105
|
+
);
|
|
83
106
|
});
|
|
84
107
|
}
|
|
85
108
|
}
|
|
86
109
|
|
|
87
110
|
if (serverCapabilities?.tools) {
|
|
88
111
|
server.setRequestHandler(CallToolRequestSchema, async (args) => {
|
|
89
|
-
return client.callTool(
|
|
112
|
+
return client.callTool(
|
|
113
|
+
args.params,
|
|
114
|
+
undefined,
|
|
115
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
116
|
+
);
|
|
90
117
|
});
|
|
91
118
|
|
|
92
119
|
server.setRequestHandler(ListToolsRequestSchema, async (args) => {
|
|
93
|
-
return client.listTools(
|
|
120
|
+
return client.listTools(
|
|
121
|
+
args.params,
|
|
122
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
123
|
+
);
|
|
94
124
|
});
|
|
95
125
|
}
|
|
96
126
|
|
|
97
127
|
server.setRequestHandler(CompleteRequestSchema, async (args) => {
|
|
98
|
-
return client.complete(
|
|
128
|
+
return client.complete(
|
|
129
|
+
args.params,
|
|
130
|
+
requestTimeout ? { timeout: requestTimeout } : undefined,
|
|
131
|
+
);
|
|
99
132
|
});
|
|
100
133
|
};
|