fix: MCP proxy resilience — timeouts, parallel discovery, error propagation
All checks were successful
CI/CD / typecheck (pull_request) Successful in 49s
CI/CD / lint (pull_request) Successful in 1m49s
CI/CD / test (pull_request) Successful in 1m4s
CI/CD / build (pull_request) Successful in 1m49s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
CI/CD / smoke (pull_request) Successful in 10m3s
All checks were successful
CI/CD / typecheck (pull_request) Successful in 49s
CI/CD / lint (pull_request) Successful in 1m49s
CI/CD / test (pull_request) Successful in 1m4s
CI/CD / build (pull_request) Successful in 1m49s
CI/CD / publish-rpm (pull_request) Has been skipped
CI/CD / publish-deb (pull_request) Has been skipped
CI/CD / smoke (pull_request) Successful in 10m3s
- McpdClient: add 30s AbortSignal timeout to all fetch calls (was infinite) - CLI bridge: return JSON-RPC error on stdout when HTTP fails (was silent) - Router: parallel tool/resource discovery via Promise.allSettled (was sequential — one slow server blocked all) - vllm-managed: 60s error cooldown prevents retry-on-every-call when vLLM is broken - Tests: McpdClient timeout suite (9), parallel discovery, vllm cooldown, bridge error response Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,15 @@ export async function runMcpBridge(opts: McpBridgeOptions): Promise<void> {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
// Parse request ID for error responses
|
||||
let requestId: unknown = null;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
|
||||
requestId = parsed.id ?? null;
|
||||
} catch {
|
||||
// Non-JSON or notification — no id to respond to
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await postJsonRpc(endpointUrl, trimmed, sessionId, token);
|
||||
|
||||
@@ -156,7 +165,18 @@ export async function runMcpBridge(opts: McpBridgeOptions): Promise<void> {
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
stderr.write(`MCP bridge error: ${err instanceof Error ? err.message : String(err)}\n`);
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
stderr.write(`MCP bridge error: ${errMsg}\n`);
|
||||
|
||||
// Send JSON-RPC error response so the client doesn't hang
|
||||
if (requestId !== null) {
|
||||
const errorResponse = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
id: requestId,
|
||||
error: { code: -32603, message: `Bridge error: ${errMsg}` },
|
||||
});
|
||||
stdout.write(errorResponse + '\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -347,7 +347,7 @@ describe('MCP STDIO Bridge', () => {
|
||||
expect(recorded.filter((r) => r.method === 'DELETE')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('writes errors to stderr, not stdout', async () => {
|
||||
it('writes errors to stderr and sends JSON-RPC error to stdout', async () => {
|
||||
recorded.length = 0;
|
||||
const stdin = new Readable({ read() {} });
|
||||
const { stdout, stdoutChunks, stderr, stderrChunks } = createMockStreams();
|
||||
@@ -364,8 +364,12 @@ describe('MCP STDIO Bridge', () => {
|
||||
|
||||
// Error should be on stderr
|
||||
expect(stderrChunks.join('')).toContain('MCP bridge error');
|
||||
// stdout should be empty (no corrupted output)
|
||||
expect(stdoutChunks.join('')).toBe('');
|
||||
// stdout should contain a JSON-RPC error response so the client doesn't hang
|
||||
const out = stdoutChunks.join('');
|
||||
const parsed = JSON.parse(out.trim()) as { id: number; error: { code: number; message: string } };
|
||||
expect(parsed.id).toBe(1);
|
||||
expect(parsed.error.code).toBe(-32603);
|
||||
expect(parsed.error.message).toContain('Bridge error');
|
||||
});
|
||||
|
||||
it('skips blank lines in stdin', async () => {
|
||||
|
||||
Reference in New Issue
Block a user