Skip to content

fix(mcp): keep stdio open after a malformed opener - #334

Merged
tenequm merged 1 commit into
mainfrom
fix/mcp-malformed-opener
Oct 2, 2026
Merged

tenequm merged 1 commit into
mainfrom
fix/mcp-malformed-opener

Conversation

@tenequm

@tenequm tenequm commented Oct 2, 2026

Copy link
Copy Markdown
Owner

Summary

Claude Code sometimes opens a pond mcp stdio session with a bare request (e.g. tools/list) carrying no 2026-07-28 _meta. rmcp answers it -32602 and then ends the connection (ServerInitializeError::ExpectedInitializeRequest), so the client never reaches its initialize fallback and the server shows as failed. This is unchanged through rmcp 3.5.0; the upstream fix is the still-open rust-sdk#1248.

  • Bump rmcp 3.2.0 -> 3.5.0 (ServerInfo -> ServerConfig for the 3.4 deprecation; negotiated versions unchanged).
  • RecoverableOpener wraps the stdio transport until the first accepted message: a non-initialize request lacking complete _meta (rmcp's own missing_required_keys check, same error text) is answered -32602 and reading continues. ping, initialize, complete-_meta requests and everything after the opener pass straight to rmcp, so its lifecycle and version gates are untouched.
  • serve_io is shared by serve_stdio and the MCP test helper, so the existing discovery/classic tests now run through the wrapper.
  • New test malformed_opener_leaves_initialize_fallback_open: malformed opener -> -32602 -> initialize (2025-06-18 and 2026-07-28) -> tools/list succeeds. Fails without the wrapper.

Remove the wrapper once rust-sdk#1248 ships.

Release note

pond mcp no longer exits when a client's first request lacks MCP 2026-07-28 request metadata; it answers with an error and keeps the connection open, so Claude Code can fall back to initialize and connect normally.

Bump rmcp 3.2.0 -> 3.5.0 and wrap the stdio transport so a first request
lacking complete 2026-07-28 `_meta` gets -32602 and the connection stays
open for the client's `initialize` fallback, instead of rmcp ending the
process. Remove once rust-sdk#1248 ships.
@tenequm tenequm self-assigned this Oct 2, 2026
@tenequm
tenequm merged commit bb27e33 into main Oct 2, 2026
13 checks passed
@tenequm
tenequm deleted the fix/mcp-malformed-opener branch October 2, 2026 16:21
@tenequm tenequm mentioned this pull request Oct 2, 2026
tenequm added a commit that referenced this pull request Oct 2, 2026
## 🤖 New release

* `pond-db`: 0.19.4 -> 0.19.5

<details><summary><i><b>Changelog</b></i></summary><p>

<blockquote>

## [0.19.5](v0.19.4...v0.19.5) -
2026-10-02

### <!-- 2 -->🐛 Bug Fixes
- **devin:** group sidekick subagents, surface images, variants and
phase ([#322](#322))
([0aa49ba](0aa49ba))
Devin: persistent sidekick subagents now import as one child session
instead of degrading the sync, images become typed file parts, and chat
phase and variant tags appear in options.devin. Run `pond sync`; add
`--verify` to enrich already-synced devin sessions.
- **mcp:** keep stdio open after a malformed opener
([#334](#334))
([bb27e33](bb27e33))
`pond mcp` no longer exits when a client's first request lacks MCP
2026-07-28 request metadata; it answers with an error and keeps the
connection open, so Claude Code can fall back to `initialize` and
connect normally.

**Full Changelog**:
v0.19.4...v0.19.5
</blockquote>


</p></details>

---
This PR was generated with
[release-plz](https://github.com/release-plz/release-plz/).
tenequm added a commit that referenced this pull request Oct 13, 2026
…350)

## What broke

Claude Code intermittently showed pond as `connected - tools fetch
failed`, with no tools and no resources:

```
request _meta is missing or has malformed required fields: io.modelcontextprotocol/protocolVersion, io.modelcontextprotocol/clientCapabilities
```

Claude Code opens a stdio server with a `server/discover` probe. `pond
mcp` opens the store before it reads stdin, so on a slow open (3.3 s
here, against 1.4-2.2 s on the good starts) the probe reply misses
Claude Code's wait and it falls back to a classic `initialize`. Both
requests then reach pond. On rmcp 3.5.0 the discover probe committed the
connection to the 2026-07-28 inline lifecycle, so pond answered
`initialize` at 2025-11-25 and then rejected the classic `tools/list`
and `resources/list` that followed, for lacking 2026-07-28 `_meta`.
Claude Code's MCP log shows the sequence:

```
Connection established ... "protocolEra":"legacy","negotiatedProtocolVersion":"2025-11-25"
stdio server answered the server/discover probe after it had connected legacy ...
Failed to fetch resources: request _meta is missing or has malformed required fields: ...
Failed to fetch tools: request _meta is missing or has malformed required fields: ...
```

Reproduced on the released 0.19.6 two ways: a `claude -p` run against
`pond mcp` behind a 4 s startup delay (no pond tools), and a script that
writes discover, initialize, initialized and tools/list back to back
(`tools/list` gets `-32602`). This is not the #334 failure: there the
first request lacked `_meta`; here it is a complete discover probe.

## Fix

- rmcp 3.5.0 -> 3.5.1. It ships
[rust-sdk#1248](modelcontextprotocol/rust-sdk#1248),
which keeps a discover probe from selecting a lifecycle, so a following
`initialize` selects the classic one. The same change answers a
malformed first request with `-32602` and keeps reading.
- That second half is what `RecoverableOpener` (#334) did by hand, and
its doc said to drop it once #1248 shipped, so it is removed. `serve_io`
passes the plain transport again.

## Tests

- New `discover_probe_then_initialize_serves_classic_requests`:
discover, initialize, initialized and tools/list written back to back.
`tools/list` must return tools.
- `malformed_opener_leaves_initialize_fallback_open` is kept as the
guard for what the wrapper covered, now against rmcp's own handling.
Both tests share an `exchange` helper that writes raw frames and
collects replies by id.

## Verified

- `cargo test --lib transport::mcp::` (12 passed), `cargo clippy
--all-targets -- -D warnings`, `cargo fmt --check`.
- The back-to-back script against this build: `tools/list` returns the 4
tools (`-32602` on 0.19.6).
- `claude -p` (Claude Code 2.1.296) against this build behind the same 4
s startup delay: all 4 pond tools present, and the MCP log reads
"answered the server/discover probe after its bound, so it was only slow
to start and speaks the new protocol".

## Release note

`pond mcp` no longer loses its tools when Claude Code's discovery probe
and its `initialize` fallback both reach a slow-starting server, which
showed pond as connected with "tools fetch failed" until a manual
reconnect. No action needed.
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