Repository navigation
Conversation
## Motivation and Context `StdioTransport#open` generates each reply with `JSON.generate` and nothing rescued what that raised, so a reply holding a string that is not valid UTF-8 ended the read loop and the server process with `JSON::GeneratorError`. A request can carry such a string without being malformed: the json gem's 2.x releases, which Ruby 3.3, 3.4 and 4.0 ship as their default, decode an unpaired low surrogate escape such as `\udfff` into bytes that are not valid UTF-8, and a JavaScript client writes one whenever a string was cut in the middle of a surrogate pair. Any reply repeating it then fails: the `Tool not found` error data for such a tool name, a resource read echoing its URI, or a tool echoing an argument. The json gem's 3.x releases refuse the escape as a parse error instead. The Python server refuses it at parse time as well, and the TypeScript server writes it back as an escape, so neither stops on it. The frame handling of `#open` now rescues a failure past the server's own error handling, reports it to the exception reporter, and answers the frame with `-32603` Internal error, carrying the request id when it can be written back, as for any other request the server cannot complete; the loop goes on with the next frame. A notification still gets no response. The parsed frame is reset for each frame, so a frame whose parse fails this way is not answered with the id of the frame before it. ## How Has This Been Tested? `test/mcp/server/transports/stdio_transport_test.rb`: a tool whose reply cannot be encoded, as a single request and in a batch, is answered with Internal error and the next request is served. Against the previous library both raise `JSON::GeneratorError` out of `#open`. A frame whose parse raises past the parser's own error, after a request and after a notification, is answered without an id; against the previous library it raises out of `#open`. ## Breaking Changes None.
atesgoral
approved these changes
Oct 20, 2026
This branch has not been deployed
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.
Motivation and Context
StdioTransport#opengenerates each reply withJSON.generateand nothing rescued what that raised, so a reply holding a string that is not valid UTF-8 ended the read loop and the server process withJSON::GeneratorError. A request can carry such a string without being malformed: the json gem's 2.x releases, which Ruby 3.3, 3.4 and 4.0 ship as their default, decode an unpaired low surrogate escape such as\udfffinto bytes that are not valid UTF-8, and a JavaScript client writes one whenever a string was cut in the middle of a surrogate pair. Any reply repeating it then fails: theTool not founderror data for such a tool name, a resource read echoing its URI, or a tool echoing an argument. The json gem's 3.x releases refuse the escape as a parse error instead. The Python server refuses it at parse time as well, and the TypeScript server writes it back as an escape, so neither stops on it.The frame handling of
#opennow rescues a failure past the server's own error handling, reports it to the exception reporter, and answers the frame with-32603Internal error, carrying the request id when it can be written back, as for any other request the server cannot complete; the loop goes on with the next frame. A notification still gets no response.The parsed frame is reset for each frame, so a frame whose parse fails this way is not answered with the id of the frame before it.
How Has This Been Tested?
test/mcp/server/transports/stdio_transport_test.rb: a tool whose reply cannot be encoded, as a single request and in a batch, is answered with Internal error and the next request is served. Against the previous library both raiseJSON::GeneratorErrorout of#open.A frame whose parse raises past the parser's own error, after a request and after a notification, is answered without an id; against the previous library it raises out of
#open.Breaking Changes
None.
Types of changes
Checklist