-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Expand file tree
/
Copy pathconfig-args.test.ts
More file actions
137 lines (114 loc) · 4.38 KB
/
config-args.test.ts
File metadata and controls
137 lines (114 loc) · 4.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/**
* Integration tests for the /config endpoint's args handling.
*
* These tests spawn the real proxy server process and make actual HTTP requests
* to verify that args containing spaces survive the serialisation round-trip
* introduced by the start.js → server path.
*/
import { spawn, type ChildProcess } from "child_process";
import { resolve } from "path";
const SERVER_SRC = resolve(__dirname, "../src/index.ts");
// Fixed credentials — the server is bound to localhost only and killed after each test.
const AUTH_TOKEN = "test-token";
const CLIENT_PORT = "17274";
const ORIGIN = `http://localhost:${CLIENT_PORT}`;
// Use a different port per test to avoid EADDRINUSE across sequential runs.
let portSeed = 17280;
interface ServerHandle {
port: number;
process: ChildProcess;
}
async function startServer(extraArgs: string[] = []): Promise<ServerHandle> {
const port = portSeed++;
const proc = spawn("tsx", [SERVER_SRC, ...extraArgs], {
env: {
...process.env,
SERVER_PORT: String(port),
CLIENT_PORT,
MCP_PROXY_AUTH_TOKEN: AUTH_TOKEN,
ALLOWED_ORIGINS: ORIGIN,
},
stdio: "pipe",
});
await waitForHealth(port);
return { port, process: proc };
}
async function waitForHealth(port: number, timeoutMs = 5000): Promise<void> {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const res = await fetch(`http://localhost:${port}/health`);
if (res.ok) return;
} catch {
// server not up yet
}
await new Promise((r) => setTimeout(r, 100));
}
throw new Error(
`Proxy server on port ${port} did not become healthy in ${timeoutMs}ms`,
);
}
async function fetchConfig(handle: ServerHandle): Promise<{
defaultArgs: string;
defaultCommand: string;
}> {
const res = await fetch(`http://localhost:${handle.port}/config`, {
headers: {
Origin: ORIGIN,
"x-mcp-proxy-auth": `Bearer ${AUTH_TOKEN}`,
},
});
if (!res.ok) throw new Error(`/config returned ${res.status}`);
return res.json() as Promise<{ defaultArgs: string; defaultCommand: string }>;
}
let currentHandle: ServerHandle | null = null;
afterEach(() => {
currentHandle?.process.kill();
currentHandle = null;
});
describe("proxy server /config: args passed from start.js", () => {
it("converts JSON-array args (new start.js format) to a shell-quoted string", async () => {
// start.js now does: `--args=${JSON.stringify(mcpServerArgs)}`
const args = ["--description", "get todays date", "--command", "date"];
currentHandle = await startServer([`--args=${JSON.stringify(args)}`]);
const config = await fetchConfig(currentHandle);
// The /config endpoint must shell-quote the array so the client UI can
// display it and shellParseArgs can round-trip it correctly.
expect(config.defaultArgs).toBe(
"--description 'get todays date' --command date",
);
});
it("passes a legacy plain shell string through unchanged (backward compat)", async () => {
// Direct invocations of the server binary that pass --args as a plain
// shell string must continue to work.
currentHandle = await startServer([
"--args=--description 'get todays date' --command date",
]);
const config = await fetchConfig(currentHandle);
expect(config.defaultArgs).toBe(
"--description 'get todays date' --command date",
);
});
it("returns an empty string when no --args flag is given", async () => {
currentHandle = await startServer([]);
const config = await fetchConfig(currentHandle);
expect(config.defaultArgs).toBe("");
});
it("handles args with backslashes", async () => {
const args = ["--path", "C:\\Users\\foo"];
currentHandle = await startServer([`--args=${JSON.stringify(args)}`]);
const config = await fetchConfig(currentHandle);
// Verify the round-trip: parse the shell-quoted string back into an array
const { parse } = await import("shell-quote");
const parsed = parse(config.defaultArgs) as string[];
expect(parsed).toEqual(args);
});
it("handles args that look like JSON themselves", async () => {
const args = ["--config", '{"key":"val"}'];
currentHandle = await startServer([`--args=${JSON.stringify(args)}`]);
const config = await fetchConfig(currentHandle);
const { parse } = await import("shell-quote");
const parsed = parse(config.defaultArgs) as string[];
expect(parsed).toEqual(args);
});
});