Skip to content

fix(stdio): skip non-JSON lines in ReadBuffer#1762

Merged
felixweinberger merged 3 commits intomainfrom
fweinberger/stdio-skip-non-json
Mar 27, 2026
Merged

fix(stdio): skip non-JSON lines in ReadBuffer#1762
felixweinberger merged 3 commits intomainfrom
fweinberger/stdio-skip-non-json

Conversation

@felixweinberger
Copy link
Contributor

ReadBuffer.readMessage() now silently skips lines that throw SyntaxError (e.g., debug output from hot-reload tools) instead of propagating them to onerror. Lines that parse as JSON but fail JSONRPC schema validation still throw.

Motivation and Context

Fixes #700. When running an MCP server under tsx watch or nodemon, the hot-reload tool writes lines like Gracefully restarting... to stdout, which the stdio transport tries to parse as JSON-RPC. This triggers SyntaxErroronerror on every reload, generating noise in telemetry and logs.

This supersedes #1225 with a simpler implementation. That PR rewrote ReadBuffer to a string-array architecture, which introduced a UTF-8 chunk-boundary corruption bug and a breaking change to the exported deserializeMessage signature. This PR keeps the existing Buffer-based architecture and just adds a loop + try/catch in readMessage().

Test cases adapted from #1225 — thanks @saharis9988 for the thorough coverage.

How Has This Been Tested?

  • 7 new tests in packages/core/test/shared/stdio.test.ts covering: non-JSON prefix lines, interleaved non-JSON, incomplete JSON preservation, unbalanced braces, invalid-JSON-like lines, whitespace tolerance, and schema-validation-still-throws
  • pnpm --filter @modelcontextprotocol/core test — 461 tests pass

Breaking Changes

None. deserializeMessage(line: string) signature unchanged. Callers of readMessage() see the same JSONRPCMessage | null contract; they just no longer get SyntaxError thrown for non-JSON lines.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Per review feedback on #1225: only SyntaxError (from JSON.parse) is caught and skipped. Zod schema validation errors from JSONRPCMessageSchema.parse still propagate, so genuinely malformed JSON-RPC messages continue to surface via onerror.

ReadBuffer.readMessage() now loops past lines that throw SyntaxError
(e.g., debug output from tsx/nodemon writing to stdout) instead of
propagating them to onerror. Lines that parse as JSON but fail JSONRPC
schema validation still throw, so genuinely malformed messages still
surface.

Keeps the existing Buffer-based architecture to preserve UTF-8
correctness across chunk boundaries and the deserializeMessage(string)
public signature.

Fixes #700
Closes #1225

Co-authored-by: Sahar Shemesh <sahar.shemesh@zoominfo.com>
@felixweinberger felixweinberger requested a review from a team as a code owner March 25, 2026 20:51
@changeset-bot
Copy link

changeset-bot bot commented Mar 25, 2026

🦋 Changeset detected

Latest commit: 577fe05

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

This PR includes changesets to release 1 package
Name Type
@modelcontextprotocol/core 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
Copy link

pkg-pr-new bot commented Mar 25, 2026

Open in StackBlitz

@modelcontextprotocol/client

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/client@1762

@modelcontextprotocol/server

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/server@1762

@modelcontextprotocol/express

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/express@1762

@modelcontextprotocol/hono

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/hono@1762

@modelcontextprotocol/node

npm i https://pkg.pr.new/modelcontextprotocol/typescript-sdk/@modelcontextprotocol/node@1762

commit: 577fe05

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM — small, well-tested fix that cleanly skips non-JSON lines while preserving schema validation errors for callers.

Extended reasoning...

Overview

This PR modifies ReadBuffer.readMessage() in packages/core/src/shared/stdio.ts to silently skip lines that throw SyntaxError (from JSON.parse) while still propagating Zod schema validation errors. The implementation converts the existing single-pass if block into a while loop with a targeted try/catch. A changeset and 7 new tests are included.

Security risks

None. The change only affects how non-parseable lines are handled in the stdio read buffer. It does not touch authentication, permissions, cryptographic operations, or external network calls. The narrowly-scoped instanceof SyntaxError check ensures only JSON parse failures are swallowed — schema validation errors still surface via onerror.

Level of scrutiny

This is a low-risk bug fix. The change is small (net +18 lines of logic), touches only one function in one file, maintains the existing JSONRPCMessage | null contract, and does not alter the deserializeMessage signature. Both callers (StdioServerTransport.processReadBuffer and StdioClientTransport.processReadBuffer) use the same while/try/catch/onerror pattern that remains fully compatible. The tests cover the key scenarios: non-JSON prefix/interleaved lines, incomplete JSON preservation, unbalanced braces, invalid-JSON-like lines, whitespace tolerance, and schema-validation-still-throws.

Other factors

No outstanding reviewer comments. The PR cleanly supersedes a previous more invasive attempt (#1225) that had UTF-8 chunk-boundary bugs and a breaking API change. This approach is minimal and conservative — it keeps the existing Buffer-based architecture and just adds the skip logic. The changeset is correctly typed as a patch release.

bhosmer-ant
bhosmer-ant previously approved these changes Mar 25, 2026
Copy link
Contributor

@bhosmer-ant bhosmer-ant left a comment

Choose a reason for hiding this comment

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

LGTM. The SyntaxError-only catch is the right discriminator — distinguishes "not JSON at all" (skip) from "JSON but not JSON-RPC" (throw, surfaces via onerror). Verified the caller loop in both client and server stdio.ts handles the continue/return null contract correctly.

One tradeoff worth being aware of (noted in the changeset): a server emitting genuinely truncated JSON ({"jsonrpc":"2.0","id":1,"resul + newline) now gets silently dropped instead of surfacing. Unlikely in practice since JSON.stringify()+'\n' is a single write, and schema-validation failures still throw.

Nit: consider adding a test for consecutive empty lines (\n\n) — JSON.parse('') throws SyntaxError so they're skipped correctly, but explicit coverage would be nice.

@felixweinberger
Copy link
Contributor Author

LGTM. The SyntaxError-only catch is the right discriminator — distinguishes "not JSON at all" (skip) from "JSON but not JSON-RPC" (throw, surfaces via onerror). Verified the caller loop in both client and server stdio.ts handles the continue/return null contract correctly.

One tradeoff worth being aware of (noted in the changeset): a server emitting genuinely truncated JSON ({"jsonrpc":"2.0","id":1,"resul + newline) now gets silently dropped instead of surfacing. Unlikely in practice since JSON.stringify()+'\n' is a single write, and schema-validation failures still throw.

Nit: consider adding a test for consecutive empty lines (\n\n) — JSON.parse('') throws SyntaxError so they're skipped correctly, but explicit coverage would be nice.

Good idea, added a test for this!

Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM. Clean, minimal fix with thorough test coverage.

Extended reasoning...

Overview

This PR modifies a single method (ReadBuffer.readMessage()) in packages/core/src/shared/stdio.ts to silently skip non-JSON lines by wrapping the parsing logic in a while loop with a SyntaxError-specific catch. It adds 8 new tests and a changeset file. The deserializeMessage signature is unchanged.

Security risks

None. The change only affects how non-JSON lines are handled in the stdio buffer — they are skipped instead of throwing. Schema validation errors (Zod) still propagate, so malformed JSON-RPC messages continue to surface. No auth, crypto, or permissions code is touched.

Level of scrutiny

Low scrutiny warranted. This is a small, focused bug fix in a utility class. The SyntaxError-only catch is the correct discriminator between "not JSON at all" (skip) and "valid JSON but invalid JSON-RPC" (throw). Both callers (client/stdio.ts:193 and server/stdio.ts:67) already call readMessage() in a while(true) loop with try/catch, so the null/throw contract is fully compatible.

Other factors

  • 8 new tests cover empty lines, non-JSON prefixes, interleaved debug output, incomplete JSON preservation, unbalanced braces, invalid-JSON-like lines, whitespace tolerance, and schema validation still throwing.
  • The CODEOWNERS file has a default * catch-all for the typescript-sdk team (no specific owner for stdio files beyond the default).
  • The author addressed the earlier review nit about adding a consecutive-empty-lines test.
  • No outstanding reviewer comments remain unaddressed.

Copy link
Contributor

@bhosmer-ant bhosmer-ant left a comment

Choose a reason for hiding this comment

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

Empty-line test added — thanks! LGTM.

@felixweinberger felixweinberger enabled auto-merge (squash) March 27, 2026 13:31
@felixweinberger felixweinberger merged commit 64897f7 into main Mar 27, 2026
16 checks passed
@felixweinberger felixweinberger deleted the fweinberger/stdio-skip-non-json branch March 27, 2026 13:32
@github-actions github-actions bot mentioned this pull request Mar 27, 2026
Copy link

@claude claude bot left a comment

Choose a reason for hiding this comment

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

LGTM, clean bug fix with good test coverage.

Extended reasoning...

Overview

This PR modifies ReadBuffer.readMessage() in packages/core/src/shared/stdio.ts to skip non-JSON lines (catching only SyntaxError) instead of propagating them. It adds 8 tests and a changeset file. Total scope: ~30 lines of logic change plus ~80 lines of tests across 3 files.

Security risks

None. The change only affects error handling for malformed input on stdio. Schema validation errors (Zod) still propagate, so valid-JSON-but-invalid-JSON-RPC messages continue to surface via onerror. No auth, crypto, or permissions code is touched.

Level of scrutiny

Low scrutiny warranted. This is a focused bug fix in a non-security-critical parsing layer. The change is mechanical — wrapping existing logic in a while loop with a targeted SyntaxError catch. Both callers (client/stdio.ts and server/stdio.ts) already use a while(true) + try/catch loop around readMessage(), so the contract (JSONRPCMessage | null, throws on schema errors) is fully preserved.

Other factors

  • The author addressed the previous review nit (empty lines test was added).
  • No outstanding reviewer comments remain.
  • No CODEOWNERS file conflicts visible.
  • The approach is explicitly simpler than the superseded PR #1225, avoiding the UTF-8 chunk-boundary bug and signature-breaking changes from that PR.
  • No bugs were found by the automated bug hunting system.

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.

SDK Crashes on Hot Reload Due to Invalid JSON in Stdio Stream

2 participants