From f7bb65239ed3cbe9db87309c39c4121ef3f41394 Mon Sep 17 00:00:00 2001 From: anandka Date: Wed, 14 Jan 2026 14:09:40 +0530 Subject: [PATCH] fix(mcp): abort hanging connections to non-standard servers Add AbortController support to remote MCP transports (StreamableHTTP and SSE) to prevent indefinite hangs when servers send malformed responses or stall during handshake. - Pass AbortSignal to transport requestInit for native fetch abort - Set timeout to abort connections after configured timeout - Ensure cleanup on all error paths (timeout, error, success) Fixes #8406 --- packages/opencode/src/mcp/index.ts | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 4e0968391f3..64c8e7088ff 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -318,38 +318,59 @@ export namespace MCP { ) } - const transports: Array<{ name: string; transport: TransportWithAuth }> = [ + const streamableAbort = new AbortController() + const sseAbort = new AbortController() + + const transports: Array<{ name: string; transport: TransportWithAuth; abort: AbortController }> = [ { name: "StreamableHTTP", + abort: streamableAbort, transport: new StreamableHTTPClientTransport(new URL(mcp.url), { authProvider, - requestInit: mcp.headers ? { headers: mcp.headers } : undefined, + requestInit: mcp.headers + ? { headers: mcp.headers, signal: streamableAbort.signal } + : { signal: streamableAbort.signal }, }), }, { name: "SSE", + abort: sseAbort, transport: new SSEClientTransport(new URL(mcp.url), { authProvider, - requestInit: mcp.headers ? { headers: mcp.headers } : undefined, + requestInit: mcp.headers + ? { headers: mcp.headers, signal: sseAbort.signal } + : { signal: sseAbort.signal }, }), }, ] let lastError: Error | undefined const connectTimeout = mcp.timeout ?? DEFAULT_TIMEOUT - for (const { name, transport } of transports) { + for (const { name, transport, abort } of transports) { try { const client = new Client({ name: "opencode", version: Installation.VERSION, }) + const timer = setTimeout(() => { + log.debug("connection timeout, aborting transport", { key, transport: name }) + abort.abort() + }, connectTimeout) + await withTimeout(client.connect(transport), connectTimeout) + .then(() => clearTimeout(timer)) + .catch((error) => { + clearTimeout(timer) + abort.abort() + throw error + }) registerNotificationHandlers(client, key) mcpClient = client log.info("connected", { key, transport: name }) status = { status: "connected" } break } catch (error) { + abort.abort() lastError = error instanceof Error ? error : new Error(String(error)) // Handle OAuth-specific errors