Skip to content

fix(core): V1 to V2 MCP migration drops timeout.startup, so scalar timeouts never reach the connect budget #54184

Closed

Description

@Bearmancer

Description

The V1 to V2 MCP config migration drops a per-server scalar timeout on the startup budget, which is the only budget that governs the MCP connection handshake.

A local MCP server still bearing its V1 shape has timeout as a plain integer. In V1 that integer applied to the handshake as well as tool calls. After migration it only reaches catalog and execution, so connection keeps using the built-in default no matter what the user set.

Reproduction

Normalize a config that still carries a legacy per-server integer timeout:

ConfigNormalize.normalize({ mcp: { example: { type: "local", command: ["tool"], timeout: 5000 } } })

Observed:

"timeout": { "catalog": 5000, "execution": 5000 }

Expected:

"timeout": { "startup": 5000, "catalog": 5000, "execution": 5000 }

Because the built-in startup default is also 30000, the regression is invisible for the common timeout: 30000 case. It becomes visible for any differing value: a 5000 scalar gives a 5s tool-call budget while connection still gets the 30s default.

Environment

  • opencode v2.0.26, and the same mapping is present at origin/v2 HEAD
  • Windows 10 Pro 10.0.19045 x64
  • bun 1.4.2

Impact

Anyone who migrated a V1 mcp.<server>.timeout scalar to V2 lost control of the MCP connection budget. Raising it no longer grants a slow server more time to connect; lowering it no longer fails fast at connection time. This restores the V1 behaviour for a field people were already setting.

Related

Activity

  1. opencode-agent commented on Oct 9, 2026

    @opencode-agent
    Contributor

    Thanks for the detailed report. I reproduced this on 2.0.26 with a local stdio MCP server that waits 12s before answering initialize:

    • With the V1-style "timeout": 5000, opencode debug config shows the normalized value as {"catalog":5000,"execution":5000}, with no startup. opencode mcp list then reports the server as connected after about 12s, so the 30s default startup budget applied instead of 5s.
    • With the V2 form "timeout": { "startup": 5000 } under mcp.servers, the same server fails with Request timed out after about 5s, as expected.

    On the latest v2 (229b422), ConfigNormalize.normalize still produces { catalog, execution } without startup for the same input. The mapping comes from migrateMcp in packages/core/src/v1/config/migrate.ts. V1 did use the per-server timeout as the connect timeout, so this is a regression from the migration. The script below reproduces it end to end.

    Until it's fixed, you can work around it by writing the V2 form explicitly, mcp.servers.<name>.timeout: { startup, catalog, execution }.

    Related:

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #54184: a V1-style scalar MCP `timeout` is not migrated to `timeout.startup`.
    # Requires node and `opencode` (v2) on PATH. Override with OPENCODE="bun ... index.ts" to run from source.
    set -u
    OPENCODE=${OPENCODE:-opencode}
    DIR=$(mktemp -d); export HOME=$DIR/home; mkdir -p "$HOME" "$DIR/proj"
    cat > "$DIR/slow-mcp.mjs" <<'JS'
    // stdio MCP server whose `initialize` reply takes 12s
    import readline from "node:readline"
    const send = (m) => process.stdout.write(JSON.stringify(m) + "\n")
    readline.createInterface({ input: process.stdin }).on("line", (line) => {
      let msg; try { msg = JSON.parse(line) } catch { return }
      if (msg.method === "initialize")
        setTimeout(() => send({ jsonrpc: "2.0", id: msg.id, result: { protocolVersion: msg.params?.protocolVersion ?? "2025-06-18",
          capabilities: { tools: {} }, serverInfo: { name: "slow", version: "1.0.0" } } }), 12000)
      else if (msg.method === "tools/list") send({ jsonrpc: "2.0", id: msg.id, result: { tools: [] } })
      else if (msg.id !== undefined && msg.method) send({ jsonrpc: "2.0", id: msg.id, result: {} })
    })
    JS
    run() { # $1 = label, $2 = opencode.json content
      cd "$DIR/proj"
      echo "$2" > opencode.json
      echo "=== $1: $2"
      $OPENCODE service stop >/dev/null 2>&1
      echo "--- normalized timeout (opencode debug config):"
      $OPENCODE debug config 2>&1 | tr -d ' \n' | grep -o '"timeout":{[^}]*}'
      for i in $(seq 1 10); do sleep 2; echo "t~$((i*2))s: $($OPENCODE mcp list 2>&1 | tail -1)"; done
    }
    SERVER="\"type\":\"local\",\"command\":[\"node\",\"$DIR/slow-mcp.mjs\"]"
    run "V1 scalar (expected: fails after ~5s)" "{\"mcp\":{\"example\":{$SERVER,\"timeout\":5000}}}"
    run "V2 control (fails after ~5s)" "{\"mcp\":{\"servers\":{\"example\":{$SERVER,\"timeout\":{\"startup\":5000}}}}}"
    $OPENCODE service stop >/dev/null 2>&1

    Run it: bash repro.sh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions