Skip to content

mcp: local server not restarted after transport error #53226

Open

Description

@TitanLion72

Summary

When a local (stdio) MCP server process exits unexpectedly while a session is already connected, OpenCode logs a single mcp transport error warning and never restarts it. The server's tools silently disappear from the agent's tool list for the remainder of the session. Only an unrelated event — a write to the OpenCode config file, or starting a new session — causes the server to be spawned again.

Environment

  • opencode version: v2.0.22 (channel latest)
  • OS: Windows_NT 10.0.26200.9550 (win32 x64)
  • Terminal: Windows Terminal (WT_SESSION set), TERM=xterm-256color
  • Shell: C:\WINDOWS\system32\cmd.exe
  • Install/channel: latest (npm @opencode/cli)
  • Active plugins: none configured in opencode.json

Reproduction

  1. Configure a local stdio MCP server that stays alive after initialize, for example:
    {
      "mcp": {
        "servers": {
          "playwright": {
            "type": "local",
            "command": ["C:\\Program Files\\nodejs\\node.exe", "C:\\path\\to\\mcp\\cli.js"]
          }
        }
      }
    }
  2. Have OpenCode sessions open in two or more directories, so several connections to the same server are spawned (one per directory).
  3. Kill the MCP server process from outside OpenCode. A realistic trigger is another OpenCode session running a broad process cleanup such as:
    Get-Process node,msedge -ErrorAction SilentlyContinue |
      Where-Object { $_.StartTime -gt (Get-Date).AddMinutes(-25) } |
      ForEach-Object { try { $_.Kill() } catch {} }
    Any taskkill, service restart of a parent process, or OOM kill should behave the same way.
  4. Observe that the MCP tools are no longer offered to the agent, and that nothing brings them back.

Expected Behavior

A local MCP server whose process exits unexpectedly should be restarted automatically (ideally with backoff and a cap on retries), so its tools remain available. At minimum, a crash while connected should be retried the same way an initial connect failure is.

Actual Behavior

One warning is logged per connection and then nothing:

timestamp=2026-10-04T20:48:21.035Z level=WARN message="mcp transport error" server=playwright error="MCP server process exited with code 255" role=server directory="<project-a>" connectionID=<id>
timestamp=2026-10-04T20:48:21.051Z level=WARN message="mcp transport error" server=playwright error="MCP server process exited with code 255" role=server directory="<project-b>" connectionID=<id>
timestamp=2026-10-04T20:48:21.067Z level=WARN message="mcp transport error" server=playwright error="MCP server process exited with code 255" role=server directory="<project-c>" connectionID=<id>
timestamp=2026-10-04T20:48:21.082Z level=WARN message="mcp transport error" server=playwright error="MCP server process exited with code 255" role=server directory="<project-d>" connectionID=<id>
timestamp=2026-10-04T20:48:21.099Z level=WARN message="mcp transport error" server=playwright error="MCP server process exited with code 255" role=server directory="<home>" connectionID=<id>

No mcp connected line follows, and no retry is attempted. The agent's tool list loses every tool from that server and does not recover on its own.

Notes on scope and contrast:

  • The shared background service was not restarted during this occurrence (same PID and start time before and after), so this is specific to MCP child processes rather than a service restart.
  • Retry-on-startup does appear to exist: an earlier mcp connect failed was followed by a successful mcp connected roughly 35 seconds later. What is missing is retry-on-crash for a server that was already connected.
  • Writing to the global config file causes all connections to be torn down and respawned immediately (mcp connected ... tools=25 for every directory within ~2s), which currently works as an accidental recovery trigger.

Additional Context

  • Frequency: 4 occurrences within roughly 40 minutes in one session. Always reproducible when the child process is killed.
  • Exit code was 255 in every occurrence, consistent with an external kill rather than a crash inside the server. The MCP server itself worked correctly when spawned manually: initialize succeeded, tools/list returned 25 tools, and tools/call browser_navigate succeeded.
  • An earlier, separate failure mode was also observed and is believed to be a different issue: when the command was npx -y <package>, several connections spawned at once raced on the package manager's shared install cache and died with ENOTEMPTY ... InstallFailed extracting tarball. Working around it by pre-installing the package and using an absolute path made startup reliable. Mentioned here only for context.
  • Workarounds currently used: write to the global opencode.json to force a respawn, or opencode service restart. Both are indirect — the first mutates configuration as a side effect.
  • Suggested direction (if this is a bug rather than intended behavior): treat mcp transport error like a failed connect and re-enter the spawn/retry path, ideally with exponential backoff and a cap so a permanently broken server does not spin.

Activity

  1. github-actions commented on Oct 4, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. ckir commented on Oct 18, 2026

    @ckir

    Confirmed on Linux / v2.0.32 — same failure, plus two new observations and a cleaner workaround

    Corroborating this from a different platform and a later version: Ubuntu Linux x86_64 (kernel 7.0.0-38-generic), opencode v2.0.32 (channel latest), local stdio server configured globally in ~/.config/opencode/opencode.jsonc.

    What happened

    Five connections to the same local server (rtk, one per open directory, per the pattern in the OP) were killed simultaneously by an external signal: all five mcp transport error warnings landed within a ~250 ms window (02:14:01.988Z–02:14:02.231Z), even though the connections had been spawned 6–53 minutes apart. Confirmed external cause, not a crash:

    • service PID unchanged before/after (same run=febb6aed)
    • no OOM records in /var/log/syslog (grep -c "Out of memory" → 0 in both current and rotated logs)
    • config mtime and binary mtime unchanged (days old)
    • child stderr contains no shutdown lines — see observation 2

    New observation 1: a new session does not heal it in v2.0.32

    The OP notes that starting a new session respawns the server. That did not hold once a stored failed status existed for the directory:

    • kill at 02:14:02Z
    • new TUI session started at 02:19Z → no mcp connected logged
    • opencode mcp list (fresh CLI process) at 02:27Z → still ✗ rtk failed, re-displaying the old error
    • zero reconnect attempts for 39 minutes until manually forced (mcp connected finally at 02:53:44Z)

    So the failure is sticky across sessions as well as across tool calls, and mcp list reports the cached status rather than triggering a connect.

    New observation 2: the error text embeds stale startup stderr instead of the exit status

    v2.0.22 reported an informative MCP server process exited with code 255. v2.0.32 reports:

    ✗ rtk  failed: Connection closed: MCP server process exited: m[2m:[0m Service initialized as server ... client initialized
    

    That blob is whatever happened to be in the child's stderr buffer at kill time — in this case only the ANSI-colored initialize logs from up to 53 minutes earlier, with no exit code. It reads exactly like a startup/handshake failure and sent the investigation down the wrong path; the server had connected fine (mcp connected server=rtk tools=4) and had served traffic since. Consider including the exit code/signal and not presenting pre-failure buffered output as the failure reason.

    New observation 3: per-directory reconnect exists without mutating config

    A cleaner workaround than the config-touch / service-restart ones in the OP:

    opencode api post '/api/experimental/mcp/<server>/connect?location[directory]=<dir>'

    Reconnects that directory's connection only (✓ <server> connected immediately, tools usable). The endpoint accepts location[directory] as a deep-object query param; GET /api/mcp lists servers. Looping over the affected directories fully recovered all six connections without restarting the service or touching opencode.json.

    Diagnostic note (possibly useful for the retry logic)

    The server logs input stream terminated + serve finished quit_reason=Closed when it actually processes stdin EOF (verified by hand-driving an initialize/tools/list handshake against the same binary). Those lines being absent from the captured stderr is a reliable signal that the child was signalled rather than disconnected — i.e. exactly the case the retry path should cover.

    Frequency on this occurrence: once, but permanently sticky until manual intervention.

  3. iaduartec commented on Oct 24, 2026

    @iaduartec

    Additional data point on v2.1.4 (Linux/WSL2): the same "not restarted" outcome also happens when the local child exits cleanly (code 0), not only after a crash or external kill. That suggests the dispose/reload path, not a server that died.

    Timeline from ~/.local/share/opencode/log/opencode.log:

    11:57:25.907 mcp connected server=context-mode tools=11
    12:01:55.903 watcher stopped ...                       <- config re-normalization in progress
    12:01:55.915 mcp transport error server=context-mode error="MCP server process exited with code 0"
    12:01:56.187 mcp transport error server=context-mode error="MCP server process exited with code 0"   <- second location
    

    No mcp connected for that server afterwards. Later config resolutions (one per request in this build) did not retry.

    State stayed like that for 45+ minutes:

    $ opencode api get /api/mcp
    {"name":"context-mode","status":{"status":"failed","error":"Connection closed: MCP server process exited with code 0"}}
    
    $ opencode mcp list
    ✗ context-mode      failed: Connection closed: MCP server process exited with code 0
    

    The server itself is healthy. Spawning the exact same command with piped stdio and sending initialize answers in ~200 ms, lists 11 tools, and stays alive:

    {"result":{"protocolVersion":"2024-11-05","capabilities":{...},"serverInfo":{"name":"context-mode","version":"1.0.169"}},"jsonrpc":"2.0","id":1}
    

    Recovery is immediate with the experimental endpoint:

    POST /api/experimental/mcp/context-mode/connect?location[directory]=/home/user/project
    -> {"name":"context-mode","status":{"status":"connected"}}
    

    Note: it is not deterministic. A later explicit config edit plus reload left the server connected, so this looks like a race between dispose and respawn rather than a fixed repro.

    Two things would help:

    1. Treat a clean exit (code 0) as "server went away" and schedule the same reconnect/backoff used for other transport errors, instead of pinning failed forever.
    2. Have mcp list distinguish disposed from failed (or show the failure time), so a healthy binary isn't mistaken for a broken one.

    Related: #50725 (mcp list vs /api/mcp mismatch), #52237 (remote servers not retried).

    Environment: opencode v2.1.4, Linux 6.18.54.1-microsoft-standard-WSL2 (x64), TERM=xterm-256color, bash, installed at ~/.local/bin/opencode, 4 local plugins (adaptive_router, agy_delegate, context-mode.v2, rtk). Paths redacted to /home/user.

  4. opencodejr commented on Oct 27, 2026

    @opencodejr
    Contributor

    Thanks for the detailed report, @TitanLion72! In short: you'd like OpenCode to restart a local (stdio) MCP server automatically, with backoff and a retry limit, when its process exits after it has connected, so its tools don't silently vanish for the rest of the session.

    The current v2 behaviour is intentional, if not ideal. When a connected server's process exits, OpenCode marks the server as failed and removes its tools. Recovery is meant to be manual: tool calls to it return "Reconnect it from /mcps." The only automatic retries are short ones (about 250 ms and 1 s) inside a single connect attempt. Nothing restarts a server that crashes after it has connected. So I've filed this as a feature request rather than a bug, and maintainers will consider it.

    Until then, reconnecting the server from /mcps should be a more direct workaround than editing opencode.json or restarting the service.

    Related issues:

  5. enzoayala32 commented on Nov 9, 2026

    @enzoayala32

    Adding wrapper-level evidence from Windows / v2.2.6 that may help identify what kills the node children. This comes from a separate report (#62841) that was marked as a duplicate of this one.

    Setup: three local stdio servers spawned as node <script>, one connection per open directory (2 directories → 6 processes), plus one non-node local server (a Rust .exe) and one remote server. I put a diagnostic wrapper between OpenCode and each node server that logs stdin EOF, stdout/stderr EPIPE, the child's exit code and its stderr, and watched the process table in parallel.

    Observations:

    • Matches the OP and @ckir: every per-directory connection of all three node servers died in the same second, between ~90 s and ~1 h after connecting, with no client activity — six mcp transport error ... exited with code 1 lines with no stderr attached.
    • The wrapper logged nothing before death: no stdin end, no pipe error, no child error. The processes were terminated from outside, not crashed. Windows Event Viewer has no Application Error / Application Hang entry for node.exe at those timestamps either.
    • Editing the config produced a different, clean teardown in the same wrapper: STDIN END (parent closed stdin) → CHILD EXIT code=0. So the silent exit-1 deaths are not the graceful reload path (consistent with @iaduartec's observation about the dispose/reload path).
    • The Rust .exe server survived the same events and the remote server was unaffected — only the node-spawned children died. The OpenCode server process itself kept the same run id across the deaths, so it was not a service restart (and no desktop app was running, so not the desktop: supervisor respawns a live local service every ~60s (reason: 'missing'), aborting every tool call #61061 supervisor path either).

    The node-selectivity is the part I have not seen in this thread: whatever kills these processes appears to target the node-based MCP children rather than all local MCP processes. Happy to run more targeted experiments (ProcMon/ETW capture, or a watchdog that respawns and dumps stack info) if that would help.

  6. enzoayala32 commented on Nov 9, 2026

    @enzoayala32

    Follow-up with a reproducible trigger, from more experiments today (Windows, v2.2.6).

    The trigger is OpenCode's own location services reloaded path — not an external killer.

    • Reconnects done through the MCP connect endpoint (POST /api/experimental/mcp/<server>/connect?location[directory]=..., no reload): 6 node processes across 2 directories stayed connected for 40+ minutes (still alive when writing this). This is reproducible: three separate reconnects, all healthy.
    • Reconnects done through /reload and through a config-file save (both log location services reloaded, which re-boots the per-directory services): every per-directory node connection died together, ~1m40s–4m30s later, silently. Three reload events observed today, all three followed by the deaths within that window. Yesterday's config-save reconnects behaved the same way (100s, 3m26s, 12m).

    What rules out an external/OS-level killer:

    • Control processes (plain node, stdin/stdout pipes held open, parented outside the OpenCode server) survive indefinitely — 30+ minutes and counting — while the server's node children die.
    • The wrapper still logs nothing before death (no stdin end, no EPIPE, no child error, no stderr): the children are terminated, not crashed.
    • The earlier diagnostics wrapper (between OpenCode and the servers) only ever caught the clean path: on config change, STDIN END (parent closed stdin) → CHILD EXIT code=0.
    • Windows Event Viewer has no Application Error entries for node.exe at the death timestamps.

    The remaining puzzle is node-selectivity: the same reloads spawn the local Rust .exe server too, and its processes survive the entire sequence (same PIDs across the reloads, inJob=True, parent chain to the service). So whatever tears the children down targets the node-spawned processes specifically — e.g. a per-child job/stdio-teardown path that only covers the node spawns, rather than a blanket "kill all local MCP children on reload".

    Happy to add more data if useful (e.g. per-child job-handle limits, or a respawn watchdog that dumps a terminating-process stack).

  7. enzoayala32 commented on Nov 9, 2026

    @enzoayala32

    Correction + a working user-side mitigation.

    Correction to my previous comment: the "40+ minutes, still alive" figure was measured too early. Those node children did eventually die — ~54 minutes after the API reconnect (the /reload reconnects died at 1m40s–4m30s, so the API connect path lasts ~10x longer, but it is not immune). Deaths continue in batches: every per-directory node connection dies together, in the same second, minutes after connecting.

    Working mitigation (user-side watchdog) until #54378 lands: a small loop that polls opencode mcp list every 45s and reconnects any failed local server via POST /api/experimental/mcp/<server>/connect?location[directory]=... in each affected directory. It recovered all three node servers within ~2 minutes of each death batch, repeatedly, over the last hour. Script (bash, ~40 lines):

    #!/usr/bin/env bash
    DIRS=("C:\\path\\to\\dir1" "C:\\path\\to\\dir2")
    SERVERS=(my-server-a my-server-b)
    while true; do
      STATUS="$(opencode mcp list 2>&1)"
      for srv in "${SERVERS[@]}"; do
        if echo "$STATUS" | grep -q "^✗ $srv"; then
          for dir in "${DIRS[@]}"; do
            enc=$(printf '%s' "$dir" | sed 's/ /%20/g')
            opencode api post "/api/experimental/mcp/$srv/connect?location[directory]=$enc" >/dev/null 2>&1
          done
          sleep 25
        fi
      done
      sleep 45
    done

    The reconnect endpoint works and is much faster than waiting for a config write; the only gap is that mcp list output is cached for a moment, so the loop needs the ~45s cadence plus a short settle time for slow servers (rea takes ~20s to initialize).

    One more observation for whoever picks this up: the deaths are not tied to any single interval today — batches died at 4m, 3m, 12m, 19m, 48m and 54m after connecting — but they always hit every per-directory connection of every node-based server at once, and never the Rust .exe server or the remote one. That node-selectivity is still the strongest lead on what is tearing them down.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions