fix!: answer invalid ids and stop a bad line ending the server - #17
Merged
Martin Bens (SpiGAndromeda) merged 2 commits intoSep 12, 2026
Merged
Conversation
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
jq's `//` operator substitutes for `false` as well as for `null` and for an absent key, so `jq -c '.id // null'` collapsed three message classes onto one value and the notification gate treated all of them as notifications. A request carrying `"id": null` was never answered and its client waited on a reply that could not arrive. The id is now read by key presence and carried with `tojson`, which keeps a present id's JSON type and leaves an absent key as the empty sentinel the notification gate reads. An id that is present but is not a string or an integer is answered `-32600` with a null response id. For `true`, `false`, an object and an array this is the only legal output rather than a strictness preference: a response must carry its request's own id, and a response id may only be a String, Number or Null, so no valid success response for such a request can be built. A fractional id is rejected by the same gate, which restates `validate_tool_arguments`' integer test. A bare `floor` comparison cannot see a fraction at or above 2^52, where the double spacing reaches 1. Two line shapes ended the server outright. A line holding more than one JSON document passed the old `jq -e '.'` gate, which reports only its last output, and the id extraction then emitted one line per document. A document that was valid JSON but not an object made every field extraction yield an empty string. Both reached `--argjson` with an id it could not parse, and under `set -euo pipefail` that status propagated out of `process_request` into `run_mcp_server`'s plain `response=$(process_request "$line")` assignment. A client that omits one trailing newline produces the first shape. The parse gate now admits exactly one JSON document, and an object gate runs before any field is read, so the extractions cannot fail and the fix no longer depends on the dispatch site clearing errexit. A `notifications/cancelled` that omits `params.requestId` cancelled an in-flight call whose id was `null`, because jq reads the absent key as `null` and `null == null` holds. The matcher now requires the key to be present. BREAKING CHANGE: a request whose `id` is not a string or an integer now returns `-32600` where it previously received a normal result (`true`, an object, an array, a fractional number) or no response at all (`null`, `false`). A line holding more than one JSON document, or one document that is not a JSON object, is answered instead of ending the server, and a bare `null` or `false` document answers `-32600` where it previously answered `-32700`. A bad-`jsonrpc` message carrying an invalid id answers a null response id instead of echoing that id. Check that clients send one JSON object per line and a string or integer `id` before bumping the pin. Co-Authored-By: Claude <noreply@anthropic.com>
Martin Bens (SpiGAndromeda)
deleted the
fix/request-envelope-validation
branch
September 13, 2026 10:50
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #7.
process_requestnow validates the message envelope before dispatch: a line must hold exactly one JSON object, and a presentidmust be a string or an integer. That started as the fix for #7 and grew twice, each time because a review found the previous step had left something reachable.What was wrong
The reported defect.
jq's//operator substitutes forfalseas well as fornulland an absent key, sojq -c '.id // null'collapsed three message classes onto one value. A request carrying"id": nullwas read as a notification and never answered, and its client waited on a reply that could not arrive.Invalid ids were answered as though valid.
"id": true, an object and an array received a normal result. That is not just permissive, it is unanswerable: a response must carry its request's own id and a response id may only be a String, Number or Null, so no valid success response for such a request can be built. The server was emitting malformed responses.One client line could end the server. Two shapes reached it. A line holding two or more JSON documents passed the old
jq -e '.'gate, which reports only its last output, so the id extraction emitted one line per document. A document that was valid JSON but not an object made every field extraction yield an empty string. Both reached--argjsonwith an id it could not parse, and underset -euo pipefailthat status propagated out ofprocess_requestintorun_mcp_server's plainresponse=$(process_request "$line")assignment. A client that drops one trailing newline produces the first shape, so this needed no malice.A malformed cancellation stopped an unrelated call. A
notifications/cancelledomittingparams.requestIdmatched an in-flightnullid, because jq reads the absent key asnull.What changed
The parse gate admits exactly one JSON document of any type, using the same
jq -cs+ length idiomread_json_filealready uses. An object gate runs before any field is read, so the four extractions only ever see an object and none can fail — that is what makes the crash fix a property ofprocess_requestrather than of its caller, which matters because bash clearserrexitinside command substitution only outside POSIX mode.The id is read by key presence and carried with
tojson, so an absent key leaves an empty sentinel and a present id keeps its JSON type. An id that is not a string or an integer answers-32600with a null response id. The integer test restatesvalidate_tool_arguments' existing one rather than introducing a new. == floor, which cannot see a fraction at or above 2^52 where the double spacing reaches 1 — a defect this repository has already fixed once. Thejsonrpc != "2.0"arm now reflects the id only when it is one a response may carry.The cancellation matcher requires
params.requestIdto be present before comparing it.Behavior table
"id": null,"id": false-32600"id": true, object, array-32600-32600-32700-32600null/falsedocument-32700-32600jsonrpcwith invalid idnotifications/cancelledwithoutrequestIdnull-id callInteger, float-free number, string, empty-string and
"null"-string ids all round-trip byte-identically, as do malformed JSON, both bad-version forms, in-flight cancellation for ordinary ids, and unknown methods.