Repository navigation
mcp: keep the stdio session alive on a malformed JSON frame - #1210
blackwell-systems wants to merge 1 commit into
Conversation
The stdio transport feeds os.Stdin into a single streaming json.Decoder. A syntactically malformed frame makes Decode return a *json.SyntaxError, and the read goroutine returned on that error, so one bad frame terminated the whole session. A streaming decoder also cannot resynchronize on its own: a syntax error poisons its buffered stream state. Per JSON-RPC 2.0, a parse error should be answered with a -32700 response and the session should continue. That is what mark3labs/mcp-go does (it reads newline-delimited frames and unmarshals each independently), and it is the same recoverable-decode direction taken for empty-method requests in modelcontextprotocol#1000. The stdio read loop now: - replies with a -32700 parse-error response (id: null) for a malformed frame, - resynchronizes to the next newline-delimited frame and keeps reading, - terminates only on a genuine EOF or I/O error, as before. An EOF immediately after a malformed frame (with or without a trailing newline) still ends the session cleanly. Only *json.SyntaxError is recovered; the existing "invalid trailing data" handling is unchanged. Tests cover a malformed frame followed by a valid request (the request is still delivered and a -32700 is written), consecutive malformed frames, and malformed frames at end-of-stream. Verified with -race. Surfaced by an MCP conformance study run against a downstream server (blackwell-systems/agent-lsp#14). The behavior is a property of this transport, shared by every server built on the SDK's stdio transport. Fixes modelcontextprotocol#1209
…nd give six entries their summary rows (#739) A catch-up of `docs/development/upstream-bugs.md` against the upstream trackers as of today. Two more of the field-review merge requests to client-go landed on 2026-09-11 and the register still listed them as open: `!3053` (the four `Snippet` fields) shipped in v3.5.0 and `!3049` (`LastUsedAt` and `UsageType` on both deploy key structs) in v3.6.0, each version read from the tags that contain the merge commit rather than from the newest tag. The six that remain open all have a reviewer as of today, and `!3041` has its one review comment applied. On the documentation side, `!254519` is the second page merged into `master`, and the seven still open are in review since today; `!254542` has the technical writer's approval and waits on a pipeline whose only failures are the fork's `get_sources` step. Two defects of the register itself, found while reading it against the trackers: row 38 said the job token scope entry was neither reported nor in review, while its own entry has carried `gitlab-org/gitlab!254698` since 2026-09-10; and six entries that carry the five fields had no row in the summary table at all (the two project group listings, the ten modelled fields no entity exposes, `IssueRelation`, `MemberRole`, `PipelineInfo`, and the Group, Project and Issue pairs). The table is the index a reader scans first, so an entry it does not list is one the reader never reaches; they are rows 39 to 44 now, each read from its entry. The umbrella issue on client-go carries the same two merges in its table since today. The Go MCP SDK section moves too. The cancellation reason that the canceller discarded is now reported as modelcontextprotocol/go-sdk#1254 and fixed in modelcontextprotocol/go-sdk#1255, the first contribution to that SDK, chosen because the maintainers had already accepted the cause plumbing it builds on, it adds no exported API and it answers a SHOULD of the specification; the other SDK entries wait on its reception. Two entries were overtaken: the `Mcp-Name` decode was fixed upstream by another contributor in modelcontextprotocol/go-sdk#1242 (unreleased), and the malformed stdio frame was reported by another user in modelcontextprotocol/go-sdk#1209 with their own modelcontextprotocol/go-sdk#1210 open, so no second pull request goes there.
|
My server keeps stdio sessions alive with a line filter in front of the SDK's transport, and its end-to-end cases might help here: not JSON, a truncated frame, valid JSON that is not a message ( Each case checks that the next call is still answered. The refusals carry I can port them to a test against this branch if that helps. |
|
I ran the census that #1209 mentions. Here is the census data for this transport, since it shows the behaviour across the ecosystem rather than in one server. The census found 11 registry servers written in Go that complete a handshake over stdio. Each one got a syntactically malformed frame after the handshake, and then a valid request. The result split by library with no exceptions:
The five on this SDK are goreleaser/mcp, stackql, blackwell-systems/agent-lsp, x7even/cloudcostsmcp and w31r4/codex-mcp-go. Two of them, GoReleaser and StackQL, have since added their own workarounds downstream. Each server that keeps using this transport would need to do the same, so a fix here would reach all of them at once. Across all 3,685 servers that responded in the census (every SDK and language), 0.5% failed this check, against 5 of 5 on this SDK. The method and data are in the preprint: https://doi.org/10.5281/zenodo.22996025. On scope, I agree with @serverplumber and @jmrplens. Valid JSON that is not a JSON-RPC message ( |
The stdio transport feeds os.Stdin into a single streaming json.Decoder. A
syntactically malformed frame makes Decode return a *json.SyntaxError, and the
read goroutine returned on that error, so one bad frame terminated the whole
session. A streaming decoder also cannot resynchronize on its own: a syntax
error poisons its buffered stream state.
Per JSON-RPC 2.0, a parse error should be answered with a -32700 response and
the session should continue. That is what mark3labs/mcp-go does (it reads
newline-delimited frames and unmarshals each independently), and it is the same
recoverable-decode direction taken for empty-method requests in #1000.
The stdio read loop now:
An EOF immediately after a malformed frame (with or without a trailing newline)
still ends the session cleanly. Only *json.SyntaxError is recovered; the
existing "invalid trailing data" handling is unchanged.
Tests cover a malformed frame followed by a valid request (the request is still
delivered and a -32700 is written), consecutive malformed frames, and malformed
frames at end-of-stream. Verified with -race.
Surfaced by an MCP conformance study run against a downstream server
(blackwell-systems/agent-lsp#14). The behavior is a property of this transport,
shared by every server built on the SDK's stdio transport.
Fixes #1209