Skip to content

fix(stdio): strip a leading UTF-8 byte order mark before parsing a message - #2790

Open
errmakov wants to merge 1 commit into
modelcontextprotocol:mainfrom
errmakov:fix/stdio-bom-2775
Open

fix(stdio): strip a leading UTF-8 byte order mark before parsing a message#2790
errmakov wants to merge 1 commit into
modelcontextprotocol:mainfrom
errmakov:fix/stdio-bom-2775

Conversation

@errmakov

@errmakov errmakov commented Sep 11, 2026

Copy link
Copy Markdown

Part of #2775: the byte order mark case from the reporter's follow-up. The issue's other two parts (carrying the transport error into the rejection, and resyncing after an oversized message) are left to the reporter, as noted on the issue.

Problem

When a stdio peer writes a UTF-8 byte order mark before a message (some Windows tools and shell redirections do), ReadBuffer.readMessage() gets a line that starts with U+FEFF. JSON.parse throws a SyntaxError, and ReadBuffer deliberately skips SyntaxError lines so stray debug output on stdout doesn't break the transport. The message is dropped without an onerror, and the pending request waits for its timeout.

With a stdio server that writes a BOM before its tools/list response and a 2 s request timeout:

Package What the caller sees
@modelcontextprotocol/client@2.0.0 Request timed out after 2002 ms, and no onerror
@modelcontextprotocol/sdk@1.30.0 onerror gets the SyntaxError from JSON.parse; the call still times out after 2001 ms

Change

Strip a leading U+FEFF from each line before parsing, next to the existing trailing \r strip. RFC 8259 §8.1 lets parsers ignore a BOM. A JSON-RPC message always starts with {, so this is a no-op for every line that already parsed; it only rescues lines that were being dropped. A U+FEFF inside a message is left alone.

Tests

  • core-internal: a BOM-prefixed message parses; a BOM split across two chunks with a CRLF line ending parses; a U+FEFF inside a string value is preserved.
  • client: a spawned node -e process writes a BOM-prefixed message, and StdioClientTransport delivers it.

The first two unit tests and the client test fail on main and pass with the change. pnpm check:all passes, and so do the core-internal, client and server suites. In the full local run, two SSE protocol:timeout:max-total e2e cases fail on untouched main as well, so they are unrelated.

The v1.x backport is #2796 (a draft only because GitHub lets this account keep one non-draft PR here; it is complete).


AI disclosure: this PR was written with the help of Claude Code.

…ssage

ReadBuffer skips lines that fail JSON.parse so that stray debug output on
stdout does not break the transport. A message that a peer prefixed with
a UTF-8 BOM (as some Windows tools and shell redirections do) failed to
parse for the same reason, so it was dropped without an error and the
pending request waited for its timeout.

Strip a leading U+FEFF from each line before parsing, which RFC 8259
§8.1 allows. A U+FEFF inside the message is left alone.

Refs modelcontextprotocol#2775

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8hmy9JVNHqYDYEX5KBYye
@errmakov
errmakov requested a review from a team as a code owner September 11, 2026 17:51
@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7b48bd9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@modelcontextprotocol/core-internal Patch
@modelcontextprotocol/client Patch
@modelcontextprotocol/server Patch
@modelcontextprotocol/codemod Patch
@modelcontextprotocol/core Patch
@modelcontextprotocol/server-legacy Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Sep 11, 2026

Copy link
Copy Markdown

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/@modelcontextprotocol/client@2790

@modelcontextprotocol/codemod

npm i https://pkg.pr.new/@modelcontextprotocol/codemod@2790

@modelcontextprotocol/core

npm i https://pkg.pr.new/@modelcontextprotocol/core@2790

@modelcontextprotocol/server

npm i https://pkg.pr.new/@modelcontextprotocol/server@2790

@modelcontextprotocol/server-legacy

npm i https://pkg.pr.new/@modelcontextprotocol/server-legacy@2790

@modelcontextprotocol/express

npm i https://pkg.pr.new/@modelcontextprotocol/express@2790

@modelcontextprotocol/fastify

npm i https://pkg.pr.new/@modelcontextprotocol/fastify@2790

@modelcontextprotocol/hono

npm i https://pkg.pr.new/@modelcontextprotocol/hono@2790

@modelcontextprotocol/node

npm i https://pkg.pr.new/@modelcontextprotocol/node@2790

commit: 7b48bd9

@BerkantACUN BerkantACUN left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ran it rather than only reading it: npm i https://pkg.pr.new/@modelcontextprotocol/client@2790, a stdio server that writes EF BB BF before its tools/list response, 2 s request timeout.

caller sees
@modelcontextprotocol/client@2.0.0 Request timed out after 2012 ms, no onerror
this PR 1 tool in 4 ms

The change is where it belongs — next to the strip, before the SyntaxError skip that was hiding the message — and the three unit cases cover the shapes that matter (split across chunks, CRLF, U+FEFF inside a message left alone). Nothing to add from my side.

For the record, the other two parts of #2775 are up as #2792 and #2793; they touch append()/clear() in the same file, not readMessage(), so there is no overlap with this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants