Problem
When a remote MCP server (type: "remote" with StreamableHTTPClientTransport) becomes temporarily unreachable — e.g. the server process restarts, the laptop suspends/resumes, or a TCP keep-alive goes stale — the MCP client has no recovery mechanism.
What happens today
- Server restarts → all in-memory sessions lost
- Client sends a
tools/call → @modelcontextprotocol/sdk's internal reqwest pool has a dead keep-alive connection
- Socket-level error occurs (not even an HTTP 404) — the request never reaches the server
client.callTool() throws an error
- In
packages/opencode/src/mcp/index.ts, the convertMcpTool execute function has no catch/retry logic — the error propagates to Effect.catch which logs it and returns undefined
- The MCP server is marked as
"failed" and never reconnects
Why server-side middleware can't fix this
- Socket errors never reach the server. The client's HTTP library fails before sending a request.
- Even if the request does reach the server (e.g. HTTP 404 for stale session), the server can't tell the client's internal SDK state about a new session ID — that state lives inside
@modelcontextprotocol/sdk's transport layer.
Expected behavior
When a tool call fails due to a transport error, the MCP client should:
- Detect that the connection is dead (socket error, ECONNRESET, etc.)
- Close the old transport/client
- Create a new transport and reconnect (re-initialize)
- Retry the original tool call with the new session
Suggested fix
In convertMcpTool (packages/opencode/src/mcp/index.ts), wrap the execute function with transport-level retry:
execute: async (args: unknown) => {
try {
return await client.callTool(
{ name: mcpTool.name, arguments: (args || {}) as Record<string, unknown> },
CallToolResultSchema,
{ resetTimeoutOnProgress: true, timeout },
)
} catch (e) {
// If this is a transport-level error, try reconnecting once
if (isTransportError(e) && clientKey && mcpConfig) {
log.warn("MCP transport error, attempting reconnect", { clientKey, error: e.message })
try {
await client.close()
const result = await createAndStore(clientKey, { ...mcpConfig, enabled: true })
if (result.status === "connected" && state.clients[clientKey]) {
return await state.clients[clientKey].callTool(
{ name: mcpTool.name, arguments: (args || {}) as Record<string, unknown> },
CallToolResultSchema,
{ resetTimeoutOnProgress: true, timeout },
)
}
} catch (retryError) {
log.error("MCP reconnect failed", { clientKey, error: retryError })
}
}
throw e
}
}
A simpler alternative: use the existing connect function to re-establish the connection, leveraging the transport fallback chain (StreamableHTTP → SSE).
Environment
- OpenCode: latest (v1.x)
- MCP SDK:
@modelcontextprotocol/sdk
- MCP Server: codesearch serve (streamable HTTP)
- OS: Windows (but issue applies to all platforms — any server restart triggers it)
Problem
When a remote MCP server (type: "remote" with StreamableHTTPClientTransport) becomes temporarily unreachable — e.g. the server process restarts, the laptop suspends/resumes, or a TCP keep-alive goes stale — the MCP client has no recovery mechanism.
What happens today
tools/call→@modelcontextprotocol/sdk's internal reqwest pool has a dead keep-alive connectionclient.callTool()throws an errorpackages/opencode/src/mcp/index.ts, theconvertMcpToolexecutefunction has no catch/retry logic — the error propagates toEffect.catchwhich logs it and returns undefined"failed"and never reconnectsWhy server-side middleware can't fix this
@modelcontextprotocol/sdk's transport layer.Expected behavior
When a tool call fails due to a transport error, the MCP client should:
Suggested fix
In
convertMcpTool(packages/opencode/src/mcp/index.ts), wrap theexecutefunction with transport-level retry:A simpler alternative: use the existing
connectfunction to re-establish the connection, leveraging the transport fallback chain (StreamableHTTP → SSE).Environment
@modelcontextprotocol/sdk