Skip to content

Native MCP connector sends non-standard server/discover request before initialize, violating MCP lifecycle spec and crashing spec-compliant servers #4809

Description

@jacob-schmier-sndk

Summary

Copilot CLI's native MCP client sends a proprietary server/discover JSON-RPC request to locally-spawned stdio MCP servers before the standard initialize handshake, whenever it has no cached tool-list snapshot for that server. This violates the MCP lifecycle spec and causes any server that correctly enforces "initialize must be first" to terminate — which the CLI then reports as a permanent "broken pipe" connection failure, silently losing access to that server's tools.

Environment

  • Copilot CLI version: 1.0.83 (Windows x64)
  • Affected server: any strictly spec-compliant local stdio MCP server (reproduced with CodeScene's cs-mcp, but the issue is in the CLI's connector, not specific to that server)

Expected behavior

Per the MCP specification, Lifecycle:

"The initialization phase MUST be the first interaction between client and server... The client MUST initiate this phase by sending an initialize request."

"The client SHOULD NOT send requests other than pings before the server has responded to the initialize request."

There is no server/discover method defined anywhere in the MCP spec, and no provision permitting a client to send a probe request ahead of initialize.

Actual behavior

When the CLI's local per-server MCP tool-cache is cold for a given server (e.g. right after a CLI update bumps the tool-cache schema version, invalidating previously cached snapshots), the CLI's native connector sends server/discover as the very first message on the child process's stdin — not initialize. CLI logs show:

[rust:mcp::client] server/discover timed out; retrying with legacy initialize
[rust:copilot_runtime::mcp::native_connector] MCP server connection failed {"server_name":"<name>", ... the server closed its input stream (broken pipe) ...}

By the time the CLI retries with initialize alone, the server process has already exited in response to the unexpected first message — there is no successful fallback, and the server's tools never become available.

Reproduction (minimal)

  1. Configure a local stdio MCP server that (per spec) treats any non-initialize first message as a protocol violation and terminates.
  2. Ensure the CLI has no valid cached tool snapshot for that server (delete %LOCALAPPDATA%\copilot\mcp-tools\<server>.json, or update across a cache-schema version bump).
  3. Start a Copilot CLI session that references the server (or simply let it enumerate tools).
  4. Observe the server's stdin receives server/discover first, and the server process exits before initialize is ever sent.

We independently verified the root cause with the following evidence:

  • Manual stdio test bypassing the CLI entirely: driving the server's JSON-RPC protocol directly (sending initialize first, as the spec requires) works correctly and returns valid tool results. The server only fails when server/discover (or any other message) precedes initialize.
  • Full version bisection of the server: tested its entire public release history (7 versions spanning over a year of releases) against the same server/discover-before-initialize input — every version exhibits the identical fatal behavior, ruling out a recent server-side regression as the explanation for why this "started happening now."
  • CLI log correlation: the same session that first exhibited the failure also logged Unsupported MCP tool cache schema version: 1 for essentially all configured MCP servers, indicating the local tool-cache schema had just been bumped and invalidated. This aligns with the CLI's own v1.0.76 changelog entry: "MCP tools load faster from definition-scoped snapshots, with process-wide and per-server cache opt-outs" — which appears to be when this discovery-probe-on-cache-miss behavior was introduced.

Suggested fix

  • Never send anything other than initialize as the first message to a freshly spawned local stdio server. If a discovery/caching optimization is desired, negotiate it via the standard experimental capabilities object during the initialize handshake, not as a message preceding it.
  • At minimum, if a speculative pre-initialize probe is sent and the server closes its pipe / never responds, treat that as "this server doesn't support the probe" and immediately respawn the server sending only initialize — rather than treating the dead process as a permanent, unrecoverable connection failure.

Impact

Any spec-compliant local stdio MCP server that doesn't tolerate an unexpected pre-initialize message becomes silently and permanently unusable from Copilot CLI whenever the local tool-cache is cold for that server (e.g., immediately after a CLI upgrade) — with no user-facing indication of why, beyond a generic "broken pipe" log line buried in debug logs.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    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