From 871b7c5fc4293e252ec52c645a74d629ab40e4d3 Mon Sep 17 00:00:00 2001 From: oyaruchyk Date: Thu, 26 Mar 2026 17:10:52 +0100 Subject: [PATCH 1/3] fix: simplify MCP STDIO reconnection by closing client before reconnect Instead of catching 'StdioClientTransport already started' errors and retrying, proactively close the client before establishing a new connection. This eliminates the race condition where the transport state could be inconsistent during reconnection. --- core/context/mcp/MCPConnection.ts | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index d25e9f6a0e7..10b2cc6e63f 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -213,29 +213,14 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co }); }), (async () => { + await this.client.close(); if ("command" in this.options) { // STDIO: no need to check type, just if command is present const transport = await this.constructStdioTransport( this.options, ); - try { - await this.client.connect(transport, {}); - this.transport = transport; - } catch (error) { - // Allow the case where for whatever reason is already connected - if ( - error instanceof Error && - error.message.startsWith( - "StdioClientTransport already started", - ) - ) { - await this.client.close(); - await this.client.connect(transport); - this.transport = transport; - } else { - throw error; - } - } + await this.client.connect(transport, {}); + this.transport = transport; } else { // SSE/HTTP: if type isn't explicit: try http and fall back to sse if (this.options.type === "sse") { From cf55775d5aa4f37c7b3399220699b38853020866 Mon Sep 17 00:00:00 2001 From: oyaruchyk Date: Thu, 26 Mar 2026 17:26:56 +0100 Subject: [PATCH 2/3] fix: resolve MCP reconnection 'Already connected to a transport' error Create a fresh Client instance on each reconnection instead of reusing the same one. The MCP SDK's Protocol class guards connect() with an internal _transport check that may not be cleared in time after close(), especially with SSE transports where the onclose callback can fire asynchronously after close() resolves. This also fixes the auto-detect fallback path (HTTP -> SSE) which had the same issue: if the HTTP connect fails after setting _transport, the SSE fallback would hit the same guard on the same Client instance. Fixes continuedev/continue#11886 --- core/context/mcp/MCPConnection.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index 10b2cc6e63f..2818cf9726e 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -85,7 +85,13 @@ class MCPConnection { // Don't construct transport in constructor to avoid blocking this.transport = {} as Transport; // Will be set in connectClient - this.client = new Client( + this.client = this.createClient(); + + this.abortController = new AbortController(); + } + + private createClient(): Client { + return new Client( { name: "continue-client", version: "1.0.0", @@ -94,8 +100,6 @@ class MCPConnection { capabilities: {}, }, ); - - this.abortController = new AbortController(); } async disconnect(disable = false) { @@ -213,7 +217,13 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co }); }), (async () => { - await this.client.close(); + try { + await this.client.close(); + } catch { + // Best-effort cleanup; may fail if never connected + } + this.client = this.createClient(); + if ("command" in this.options) { // STDIO: no need to check type, just if command is present const transport = await this.constructStdioTransport( @@ -250,6 +260,7 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co await this.client.connect(transport, {}); this.transport = transport; } catch (e) { + this.client = this.createClient(); try { const transport = this.constructSseTransport({ ...this.options, From db3d590702ec5ade4593c5bfa349e0b8150d652a Mon Sep 17 00:00:00 2001 From: oyaruchyk Date: Thu, 26 Mar 2026 20:45:40 +0100 Subject: [PATCH 3/3] fix: preserve Client instance on reconnect, fallback to fresh Client only on race condition Reuse the existing Client (close + connect) to preserve server session and context. Only create a fresh Client as a fallback when the SDK throws 'Already connected to a transport' due to the async _onclose race condition. Extract connectTransport() helper to apply this resilience to all transport types (SSE, HTTP, WebSocket, STDIO), not just STDIO. --- core/context/mcp/MCPConnection.ts | 44 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/core/context/mcp/MCPConnection.ts b/core/context/mcp/MCPConnection.ts index 2818cf9726e..e96c6197fa5 100644 --- a/core/context/mcp/MCPConnection.ts +++ b/core/context/mcp/MCPConnection.ts @@ -102,6 +102,24 @@ class MCPConnection { ); } + private async connectTransport(transport: Transport): Promise { + try { + await this.client.close(); + await this.client.connect(transport, {}); + } catch (error) { + if ( + error instanceof Error && + error.message.startsWith("Already connected") + ) { + this.client = this.createClient(); + await this.client.connect(transport, {}); + } else { + throw error; + } + } + this.transport = transport; + } + async disconnect(disable = false) { this.abortController.abort(); await this.client.close(); @@ -217,36 +235,25 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co }); }), (async () => { - try { - await this.client.close(); - } catch { - // Best-effort cleanup; may fail if never connected - } - this.client = this.createClient(); - if ("command" in this.options) { // STDIO: no need to check type, just if command is present const transport = await this.constructStdioTransport( this.options, ); - await this.client.connect(transport, {}); - this.transport = transport; + await this.connectTransport(transport); } else { // SSE/HTTP: if type isn't explicit: try http and fall back to sse if (this.options.type === "sse") { const transport = this.constructSseTransport(this.options); - await this.client.connect(transport, {}); - this.transport = transport; + await this.connectTransport(transport); } else if (this.options.type === "streamable-http") { const transport = this.constructHttpTransport(this.options); - await this.client.connect(transport, {}); - this.transport = transport; + await this.connectTransport(transport); } else if (this.options.type === "websocket") { const transport = this.constructWebsocketTransport( this.options, ); - await this.client.connect(transport, {}); - this.transport = transport; + await this.connectTransport(transport); } else if (this.options.type) { throw new Error( `Unsupported transport type: ${this.options.type}`, @@ -257,17 +264,14 @@ Org-level secrets can only be used for MCP by Background Agents (https://docs.co ...this.options, type: "streamable-http", }); - await this.client.connect(transport, {}); - this.transport = transport; + await this.connectTransport(transport); } catch (e) { - this.client = this.createClient(); try { const transport = this.constructSseTransport({ ...this.options, type: "sse", }); - await this.client.connect(transport, {}); - this.transport = transport; + await this.connectTransport(transport); } catch (e) { throw new Error( `MCP config with URL and no type specified failed both SSE and HTTP connection: ${e instanceof Error ? e.message : String(e)}`,