vMCP streamable-http returns 400 on valid client responses to server-initiated requests
@jstar0 is already working on this.
Since Jun 29, 2026.
- Dominant language
- Go
- Stars
- 2.2k
- Forks
- 300
- Avg merge
- 1d 15h
- Merged PRs (30d)
- 184
Description
Summary
The vMCP streamable-http transport returns 400 Bad Request — Invalid or malformed MCP request when an MCP client POSTs a JSON-RPC response to a server-initiated request. Per the MCP streamable HTTP spec, the server MUST return 202 Accepted for accepted responses/notifications, not 400.
This breaks clients that respond to server-initiated pings — VS Code's MCP extension, for example, tears down the session on the 400 and the log shows will retry with new session ID.
Reproduction
-
Connect any MCP client that sends a ping on a server-initiated request (e.g. VS Code 1.116.x with a vMCP-backed MCP server).
-
After a successful
initialize+tools/list, when the server emits a ping request on the SSE backchannel, the client POSTs its response back:POST /mcp HTTP/1.1 Mcp-Session-Id: <session-id> Content-Type: application/json {"jsonrpc":"2.0","id":1,"result":{}} -
vMCP responds:
HTTP/1.1 400 Bad Request Content-Type: text/plain; charset=utf-8 Invalid or malformed MCP request -
Client treats this as a fatal transport error and tears down the session.
Expected
Per MCP spec (streamable HTTP, "Sending Messages to the Server", item 4):
If the input is a JSON-RPC response or notification: If the server accepts the input, the server MUST return HTTP status code 202 Accepted with no body. If the server cannot accept the input, it MUST return an HTTP error status code (e.g., 400 Bad Request).
So the server should:
- Look up the response by
idagainst the pending server-initiated request table for that session. - If it matches, complete the internal bookkeeping and return 202.
- Only return 400 if no matching pending request exists.
Root cause
We do emit server-initiated pings — see pkg/transport/proxy/httpsse/pinger.go:47 and pkg/transport/proxy/transparent/pinger.go:122. But the POST handler in the streamable transport doesn't correlate inbound responses against pending server requests; any response-shaped body is rejected.
Impact
- Any MCP client that responds to server-initiated requests (pings, future elicitation/create, sampling/createMessage) will have its sessions torn down.
- VS Code specifically logs this as
Connection state: Error 400 status ... will retry with new session ID. - End users see the connection flap every ~30s (our ping interval) and no tool calls work reliably.
Where to fix
The streamable-http POST handler needs a response-vs-request branch. Relevant directories:
pkg/transport/proxy/streamable/— the transport.pkg/mcp/parser.go— JSON-RPC parsing.- Wherever pending server-initiated requests are tracked per session (may not yet exist — likely needs to be introduced).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.