Summary
When the first message on a stdio connection is a non-initialize request whose _meta is missing part of the MCP 2026-07-28 required key set (e.g. it has io.modelcontextprotocol/protocolVersion but no io.modelcontextprotocol/clientCapabilities), the server sends no bytes at all to the client: no JSON-RPC error, no response to any subsequent — fully valid — request. From the client's perspective the connection is silently wedged.
The same malformed request sent after successful negotiation is correctly answered with -32602, so clients get a diagnostic in one ordering and silence in the other.
Version
rmcp 3.1.2.
Where it happens
serve_server_with_ct_inner (src/service/server.rs): the pre-negotiation loop breaks out with the first non-ping request, and when it is neither InitializeRequest nor a request with the complete 2026 inline-metadata key set, it returns
return Err(ServerInitializeError::ExpectedInitializeRequest(Some(
ClientJsonRpcMessage::request(request, id),
))); // src/service/server.rs:536 (also :521 for non-request messages)
That error goes to the server-side caller of serve(); nothing is written to the transport before the future fails. Compare the sibling paths in the same function, which do reply before proceeding/failing: pre-init ping gets an EmptyResult, and a complete-_meta discover-lifecycle request gets its response (or a JSON-RPC error) via transport.send(...).
Reproduction
Any rmcp stdio server. Pipe these two lines to it (first request is missing clientCapabilities in _meta; the second is fully valid):
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"server/discover","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"t","version":"0"},"io.modelcontextprotocol/clientCapabilities":{}}}}' \
| your-rmcp-stdio-server
Observed: zero bytes of output; the valid server/discover (id 2) is never answered. If the host keeps running despite the failed serve future, the client hangs forever.
Control: send the valid server/discover first and the malformed request second — the discover completes and the malformed request is answered with -32602.
Expected
Before failing the serve future, the server should write a JSON-RPC error for the offending request id (e.g. -32600/-32602 with a message like "expected initialize request or complete 2026 request metadata; missing: io.modelcontextprotocol/clientCapabilities"), so a misconfigured 2026 client learns why it is being disconnected instead of timing out. missing_required_keys already computes the exact missing-key list at the decision point (src/service/server.rs:533), so the error message can name them.
Impact
Any MCP 2026 client whose opening request omits one of the required _meta keys (easy to do while implementing SEP-2575) hangs with no diagnostic on either side of the wire. We hit this in ida-mcp integration testing; on the server side the only symptom is a failed serve future.
Summary
When the first message on a stdio connection is a non-initialize request whose
_metais missing part of the MCP 2026-07-28 required key set (e.g. it hasio.modelcontextprotocol/protocolVersionbut noio.modelcontextprotocol/clientCapabilities), the server sends no bytes at all to the client: no JSON-RPC error, no response to any subsequent — fully valid — request. From the client's perspective the connection is silently wedged.The same malformed request sent after successful negotiation is correctly answered with
-32602, so clients get a diagnostic in one ordering and silence in the other.Version
rmcp 3.1.2.
Where it happens
serve_server_with_ct_inner(src/service/server.rs): the pre-negotiation loop breaks out with the first non-ping request, and when it is neitherInitializeRequestnor a request with the complete 2026 inline-metadata key set, it returnsThat error goes to the server-side caller of
serve(); nothing is written to the transport before the future fails. Compare the sibling paths in the same function, which do reply before proceeding/failing: pre-initpinggets anEmptyResult, and a complete-_metadiscover-lifecycle request gets its response (or a JSON-RPC error) viatransport.send(...).Reproduction
Any rmcp stdio server. Pipe these two lines to it (first request is missing
clientCapabilitiesin_meta; the second is fully valid):Observed: zero bytes of output; the valid
server/discover(id 2) is never answered. If the host keeps running despite the failed serve future, the client hangs forever.Control: send the valid
server/discoverfirst and the malformed request second — the discover completes and the malformed request is answered with-32602.Expected
Before failing the serve future, the server should write a JSON-RPC error for the offending request id (e.g.
-32600/-32602with a message like "expected initialize request or complete 2026 request metadata; missing: io.modelcontextprotocol/clientCapabilities"), so a misconfigured 2026 client learns why it is being disconnected instead of timing out.missing_required_keysalready computes the exact missing-key list at the decision point (src/service/server.rs:533), so the error message can name them.Impact
Any MCP 2026 client whose opening request omits one of the required
_metakeys (easy to do while implementing SEP-2575) hangs with no diagnostic on either side of the wire. We hit this in ida-mcp integration testing; on the server side the only symptom is a failed serve future.