Skip to content

MCP servers disabled with /mcps reconnect after an idle Location eviction #62868

Open

Description

@kfunigeneral

Description

Disabling MCP servers with /mcps is undone after a Location is evicted and rebuilt. On 2.2.6 the log still shows location services evicted, then on the next request location services booted and mcp connected for every server I had disabled, with a new connectionID. A rebuilt Location initializes MCP from config only, so the runtime disconnect is discarded.

This also kills stateful local stdio MCP servers: rea keeps its project in memory, so the next call starts a new process and a 3–4 hour analysis has to be redone.

This is not the 2.2.6 shutdown-event bug from #60668; that stops MCP events during shutdown, but the next boot still reconnects. It is also not #13763 (V1 restart, closed not planned). #57902 is the same mechanism for ACP-added servers, but an ACP fix would re-add those servers, not preserve a /mcps override.

I reproduced it in the v2.2.6 source (tag dc2ffcf) with a scratch test: MCP.disconnect → status "disabled", close the Location scope as eviction does, rebuild → status "connected" and a second mcp connected with a new connectionID.

Plugins

opencode-loop (from ~/.config/opencode/plugins), not MCP-related.

OpenCode version

2.2.6 (tested at tag v2.2.6 / dc2ffcf; also reproduced on 2.2.5)

Steps to reproduce

  1. Configure a stateful local stdio MCP server (in my case rea with npx -y [email protected] mcp) plus other MCP servers.
  2. Open /mcps and disable all of them.
  3. Use a stateful tool in a session (e.g. rea.open_binary + rea.binary_overview).
  4. Leave the location idle for more than 60 minutes (or run /reload).
  5. Send another prompt and check the log.

Screenshot

2026-11-08T23:30:04 location services evicted ...\Downloads\reatests
2026-11-09T09:39:15 location services booted ...\Downloads\reatests
2026-11-09T09:39:28 mcp connected server=rea tools=229 ... connectionID=0a3bd776-d518-4706-9807-460ac6eca116

Operating System

Windows 10

Terminal

Windows Terminal

Activity

  1. opencodejr commented on Nov 9, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report. I could reproduce this on 2.2.6 and on the latest dev (12a661e).

    The script below uses a minimal local stdio MCP server that logs every process start, and sets experimental.location_idle_timeout to 5 seconds so eviction happens quickly. It:

    1. Waits for the server to connect.
    2. Disables it with the same API call /mcps makes. opencode mcp list shows disabled.
    3. Waits for location services evicted in the log.
    4. Sends another request.

    That request logs location services booted and mcp connected server=stateful with a new connectionID. The status is back to connected and a second server process is started, so any in-memory state in the server is lost.

    As a workaround for now, experimental.location_idle_timeout: false should keep the Location loaded so it isn't evicted for idleness. It won't help with /reload.

    Related: #57902 is the same eviction problem for MCP servers added through ACP. The open PR #56452 would save the /mcps toggle to the config file, which would also avoid this for servers defined there.

    Reproduction script (repro.sh)
    #!/usr/bin/env bash
    # Repro for #62868: an MCP server disconnected at runtime (what /mcps does) reconnects
    # after the Location is evicted for idleness. Needs node and bun. Usage: OC="opencode" ./repro.sh  (takes ~2-3 min)
    set -u
    OC=${OC:-opencode}
    ROOT=$(mktemp -d /tmp/opencode/repro-XXXX)
    export HOME=$ROOT/home XDG_CONFIG_HOME=$ROOT/home/.config XDG_DATA_HOME=$ROOT/home/.local/share XDG_STATE_HOME=$ROOT/home/.local/state XDG_CACHE_HOME=$ROOT/home/.cache
    export SPAWN_LOG=$ROOT/spawn.log
    mkdir -p $XDG_CONFIG_HOME/opencode $ROOT/project; : > $SPAWN_LOG
    DIR=$ROOT
    cat > $ROOT/mcp-server.mjs <<'MJS'
    // Minimal stateful stdio MCP server: logs each process start to $SPAWN_LOG
    import { appendFileSync } from "node:fs"
    import { createInterface } from "node:readline"
    appendFileSync(process.env.SPAWN_LOG, `spawn pid=${process.pid} ${new Date().toISOString()}\n`)
    const send = (m) => process.stdout.write(JSON.stringify(m) + "\n")
    createInterface({ input: process.stdin }).on("line", (line) => {
      let msg; try { msg = JSON.parse(line) } catch { return }
      if (msg.id === undefined) return
      if (msg.method === "initialize")
        return send({ jsonrpc: "2.0", id: msg.id, result: { protocolVersion: msg.params?.protocolVersion ?? "2025-06-18", capabilities: { tools: {} }, serverInfo: { name: "stateful", version: "1.0.0" } } })
      if (msg.method === "tools/list")
        return send({ jsonrpc: "2.0", id: msg.id, result: { tools: [{ name: "ping", description: "ping", inputSchema: { type: "object", properties: {} } }] } })
      if (msg.method === "ping") return send({ jsonrpc: "2.0", id: msg.id, result: {} })
      send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: "method not found" } })
    })
    MJS
    cat > $XDG_CONFIG_HOME/opencode/opencode.json <<JSON
    { "experimental": { "location_idle_timeout": "5 seconds" },
      "mcp": { "servers": { "stateful": { "type": "local", "command": ["node", "$DIR/mcp-server.mjs"], "environment": { "SPAWN_LOG": "$SPAWN_LOG" } } } } }
    JSON
    cd $ROOT/project
    $OC service start >/dev/null 2>&1
    for i in $(seq 30); do $OC mcp list 2>&1 | grep -q "stateful  connected" && break; sleep 1; done
    echo "== initial status"; $OC mcp list 2>&1
    echo "== disconnect (same call /mcps makes)"
    $OC api POST "/api/experimental/mcp/stateful/disconnect?location%5Bdirectory%5D=$ROOT/project" 2>&1 | head -5
    $OC mcp list 2>&1
    echo "== waiting for idle eviction (sweep runs every minute)"
    for i in $(seq 40); do sleep 5; grep -rqs "location services evicted" $XDG_DATA_HOME $XDG_STATE_HOME && break; done
    sleep 2
    echo "== status after eviction (this request boots the location again)"
    for i in $(seq 20); do out=$($OC mcp list 2>&1); echo "$out" | grep -q stateful && break; sleep 1; done; echo "$out"
    sleep 2
    echo "== spawn log (each line = a new MCP server process)"; cat $SPAWN_LOG
    echo "== service log"; grep -rhE "location services (evicted|booted)|mcp connected" $XDG_DATA_HOME $XDG_STATE_HOME 2>/dev/null | cut -c1-220 | tail -10
    $OC service stop >/dev/null 2>&1
    echo "EXPECTED: stateful stays disabled and only one spawn line"

    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions