Skip to content

mcp: initial reconciliation can duplicate stdio servers #52741

Description

@everlastingQAQ

Description

In packages/core/src/mcp/index.ts, initial reconciliation populates the server entries before scheduling asynchronous startup. It can yield during registration or status-event publication, allowing connect() or disconnect() to finish first. The subsequent initial startup does not recheck the entry's current state under the server lock.

Deterministic tests show two stdio children starting for the same entry after connect(), or a child starting despite an earlier disconnect(). From the implementation, the duplicate startup overwrites entry.scope / entry.client; the next disconnect can close only the currently recorded scope. The earlier scope remains attached to the enclosing location scope, so its child may remain alive until that scope closes unless it exits independently.

Expected: initial startup should not duplicate a connection already established for the same entry, or undo a disconnect. This is not a request to share MCP processes across different locations.

Plugins

None required by the isolated reproduction.

OpenCode version

v2.0.22; affected code also present at v2 commit 134d0ede4dfe4b509dea58baa261e8a307816ac7.

Steps to reproduce

This is a test-level reproduction; the Deferred barrier below controls timing rather than representing a user-facing operation.

  1. Start initial reconciliation with a disabled gate entry followed by an enabled local stdio server.
  2. Use a Deferred barrier in the test bus to pause publication of the gate's status event, after registration but before the local server's startup task is scheduled.
  3. Call connect() for the local server, release the barrier, let reconciliation finish, then disconnect the server. The fixture records two child PIDs instead of one.
  4. Repeat with disconnect() in step 3: the fixture records one child starting instead of zero.

The linked PR adds both regressions in packages/core/test/mcp.test.ts, using the existing stdio fixture. Run them with:

cd packages/core
bun test test/mcp.test.ts -t 'preserves (connect|disconnect) while initial MCP' --timeout 10000

On the unpatched implementation, both fail on the startup-count assertion. With the fix, both pass, and the connect test also verifies the recorded child is gone after disconnect, before disposing the surrounding layer. No mailbox, model calls, or live server are required. A separate isolated test with 20 sequential reloads did not reproduce accumulation; this report concerns the initial-startup race, not every reload.

Operating System

Debian Linux, x86_64; kernel 6.12.107+deb13-amd64.

Terminal

OpenChamber web interface with a local OpenCode release backend; shell zsh. Reproduction runs as a Bun core-package test, independently of the terminal.

Activity

  1. github-actions commented on Oct 2, 2026

    @github-actions
    Contributor

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

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