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

- 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:
Michal
2026-04-10 18:28:03 +01:00
parent 383be66286
commit 857f8c72ae
8 changed files with 349 additions and 45 deletions

View File

@@ -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');
}
}
}

View File

@@ -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 () => {