Repository navigation
Description
Problem
When an MCP tool handler takes a long time to resolve (e.g., an interactive prompt waiting for user input with a 1200s or infinite timeout), Bun's hardcoded ~5 minute fetch timeout fires before the tool completes. This causes:
- The
fetch()call backing theStreamableHTTPClientTransportPOST to abort with aTimeoutError - OpenCode's MCP client retries the tool call on a new HTTP connection
- The MCP server receives a new request for the same tool, but the original SSE stream's
cancel()callback has already removed the request ID from the SDK's internal_streamMapping send()throws"No connection established for request ID"→ the agent sees-32000 Connection closed- The tool result is permanently lost
Even with enableJsonResponse: true on the server (which eliminates SSE streams), the client-side fetch timeout still fires and causes unnecessary retries, producing duplicate tool call attempts in the agent's chat log.
Root cause
Bun hardcodes a ~5 minute fetch timeout that ignores AbortSignal.timeout() values longer than the hardcoded limit. This is a known Bun bug:
- Respect timeout passed to fetch oven-sh/bun#16682 — "Respect timeout passed to fetch" (open, labeled
bug,needs investigate,web:fetch) AbortSignal.timeoutdoes nothing infetchwhen the host drops all packets oven-sh/bun#18536 — "AbortSignal.timeoutdoes nothing infetchwhen the host drops all packets" (closed, but related)
The MCP SDK's StreamableHTTPClientTransport passes fetch calls through to the runtime. When running on Bun, the runtime's internal timeout fires before the SDK's own AbortSignal timeout, causing an abort that the SDK interprets as a connection failure.
Fix
Bun supports a non-standard timeout option on fetch(). Passing timeout: false disables the hardcoded limit, allowing the SDK's own per-request AbortSignal to be the controlling timeout.
Suggested patch for packages/opencode/src/mcp/index.ts
Create a bunSafeFetch wrapper and pass it to all transport constructors:
// Bun hardcodes a ~5 min fetch timeout that ignores longer AbortSignal values.
// Passing `timeout: false` disables that limit so the SDK's own per-request
// timeout (from the user's `timeout` config) is the controlling timeout.
// See: https://github.com/oven-sh/bun/issues/16682
const bunSafeFetch = ((input: RequestInfo | URL, init?: RequestInit) =>
// @ts-ignore Bun-specific option, not in standard RequestInit
fetch(input, { ...init, timeout: false })) as typeof globalThis.fetchThen pass fetch: bunSafeFetch to all three transport constructor sites:
transport: new StreamableHTTPClientTransport(new URL(mcp.url), {
authProvider,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
+ fetch: bunSafeFetch,
}), transport: new SSEClientTransport(new URL(mcp.url), {
authProvider,
requestInit: mcp.headers ? { headers: mcp.headers } : undefined,
+ fetch: bunSafeFetch,
}),-const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), { authProvider })
+const transport = new StreamableHTTPClientTransport(new URL(mcpConfig.url), {
+ authProvider,
+ fetch: bunSafeFetch,
+})Affected MCP servers
Any MCP server with tool handlers that take longer than ~5 minutes to resolve. This is common for:
- Interactive prompt tools where a human user is expected to type a response
- Long-running CI/CD or build tools
- Tools that wait for external approval workflows
Environment
- OpenCode version: latest main
- Bun version: 1.2.x
- OS: macOS (arm64)
- MCP SDK:
@modelcontextprotocol/sdk(StreamableHTTPClientTransport)
Activity
rekram1-node commentedon Jun 22, 2026
this should be fixed due to the progress stuff