Skip to content

Fix MCP handshake failure with hosts that probe via server/discover - #31

Merged
aniongithub merged 1 commit into
mainfrom
aniongithub-fix-initialize-handshake
Sep 8, 2026
Merged

aniongithub merged 1 commit into
mainfrom
aniongithub-fix-initialize-handshake

Conversation

@aniongithub

Copy link
Copy Markdown
Owner

Problem

Installing the latest release and enabling it in the GitHub Copilot CLI fails at startup:

MCP server process exited before completing the MCP initialize handshake (exit code 1);
last stderr: Error: expect initialized request, but received: Some(Request(JsonRpcRequest {
  ... request: CustomRequest(CustomRequest { method: "server/discover", ... }) }))

Root cause

The Copilot CLI (and similar hosts) send an optimistic server/discover request before the standard initialize. A spec-compliant server is expected to reply to the unknown method with -32601 Method not found and stay alive, after which the host falls back to the normal initialize handshake. The runtime logs confirm this:

[rust:mcp::client] server/discover failed before legacy initialize also failed
  {"error":"...code:-32601, message:\"Method not found\"..."}

rmcp's serve_server handshake instead treats the first non-ping request as the initialize request. When it isn't, serve() returns an error, main exits (code 1), and stdin closes before the host's fallback initialize arrives — the client then hits a broken pipe and the server never starts. (mind-map, which uses the Go SDK, tolerates this and works, which is why other servers were fine.)

Fix

Wrap the stdio transport in a small DiscoveryGuard (crates/devcontainer-mcp/src/discovery_guard.rs) that, before initialization, answers any request that isn't initialize/ping with JSON-RPC -32601 Method not found and keeps waiting. ping and initialize pass through untouched, and once initialized the guard is fully transparent. This lets the host's fallback initialize succeed, matching spec-compliant server behavior.

No dependency changes.

Tests

Adds two end-to-end tests that run the real DevContainerMcp server behind DiscoveryGuard over an in-memory transport (mirroring the reference server's test style):

  • survives_pre_initialize_discover_probe — replays server/discoverinitializeinitializedtools/list, asserting the probe gets -32601, the server stays alive, and tools list successfully.
  • normal_handshake_still_works — a plain handshake is unaffected by the wrapper.

Verification

Built and tested inside the devcontainer:

  • cargo test -p devcontainer-mcp → both tests pass
  • cargo clippy -p devcontainer-mcp-core -p devcontainer-mcp -- -D warnings → clean
  • Manual stdio replay of the Copilot sequence: server/discover returns -32601, then initialize + tools/list return 46 tools, exit 0, empty stderr.

The GitHub Copilot CLI (and similar hosts) send an optimistic
`server/discover` request before the standard `initialize`. rmcp's
handshake treats the first non-ping request as the initialize request;
when it isn't, serve() returns an error and the process exits, closing
stdin before the host's fallback `initialize` arrives (broken pipe), so
the server never starts.

Wrap the stdio transport in a DiscoveryGuard that replies to any
pre-initialize request with JSON-RPC -32601 (Method not found) and keeps
waiting, letting the host's fallback initialize succeed. This mirrors how
spec-compliant servers (e.g. mind-map) already behave.

Add end-to-end tests that replay the probe-then-initialize sequence over
an in-memory transport and confirm a normal handshake is unaffected.
@aniongithub
aniongithub merged commit 29a607c into main Sep 8, 2026
1 check passed
@aniongithub
aniongithub deleted the aniongithub-fix-initialize-handshake branch September 8, 2026 05:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant