Skip to content

fix(sse): escape U+2028 / U+2029 in SSE data lines (V1)#1926

Open
tonxxd wants to merge 2 commits intomodelcontextprotocol:v1.xfrom
tonxxd:fix-sse-u2028-u2029-escaping-v1
Open

fix(sse): escape U+2028 / U+2029 in SSE data lines (V1)#1926
tonxxd wants to merge 2 commits intomodelcontextprotocol:v1.xfrom
tonxxd:fix-sse-u2028-u2029-escaping-v1

Conversation

@tonxxd
Copy link
Copy Markdown

@tonxxd tonxxd commented Apr 17, 2026

This PR has a v2 equivalent #1925

Escape U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR) in SSE data: lines emitted by WebStandardStreamableHTTPServerTransport.

Motivation and Context

JSON.stringify leaves U+2028 / U+2029 unescaped, they are valid inside JSON strings. But many SSE client parsers (including in Claude Desktop and ChatGPT) treat them as line terminators. When a tool response contains either codepoint, the receiver truncates the data: line mid-JSON, fails to parse silently, and the tool call appears to hang forever on the client side.
The SSE spec (WHATWG HTML) only defines LF/CR/CRLF as line terminators, so strictly this is a client parser bug. The Python SDK hit the same issue (modelcontextprotocol/python-sdk#1356) and shipped a fix in its client parser (httpx-sse 0.4.2).
Reproduced end-to-end against an mcp-use server whose Algolia-backed tool returned a candidate whose skills field contained a literal U+2028

How Has This Been Tested?

  • New regression test in packages/server/test/server/streamableHttp.test.ts: registers a tool returning "before\u2028middle\u2029after", asserts the literal codepoints do not appear on the wire, asserts the escaped forms do, and asserts JSON.parse of the data: line round-trips to the original string.
  • pnpm --filter @modelcontextprotocol/server test → 41/41 in streamableHttp.test.ts, 56/56 across the package.
  • pnpm test:all → green across all packages and the 422-test integration suite. (One pre-existing better-sqlite3 native-binding failure in examples/shared on my machine is reproducible on clean origin/main and unrelated to this PR.)
  • pnpm lint:all clean.
  • Manually reproduced end-to-end against the affected mcp-use server: before → widget hangs at isPending: true; after → tool result surfaces and widget renders normally.

Breaking Changes

None. The escape is invisible to SSE-spec-compliant clients (JSON.parse reinflates \u2028 / \u2029 back to the original codepoints). Non-compliant clients that previously truncated on these codepoints now receive the full payload.

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

Scope intentionally narrow: only the SSE framing in WebStandardStreamableHTTPServerTransport.writeSSEEvent is touched. The JSON-response path (enableJsonResponse) is unaffected

Related:

JSON.stringify leaves LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR
(U+2029) unescaped inside JSON strings. Many real-world SSE client
parsers treat those codepoints as line terminators, truncating the
`data:` line mid-JSON. Tool calls whose responses contain either
character then hang forever on the client because the truncated JSON
fails to parse silently.

The SSE spec (WHATWG HTML) only defines LF/CR/CRLF as line
terminators, so strictly this is a client bug, but defensive
server-side escaping is a long-established practice for JSON over
SSE. Related: modelcontextprotocol/python-sdk#1356.

Applied in both transports that write SSE frames:
- WebStandardStreamableHTTPServerTransport.writeSSEEvent
- SSEServerTransport.send

Added a regression test exercising a tool that returns text containing
both codepoints and asserting the literal characters do not appear on
the wire while JSON.parse of the `data:` line still reproduces the
original text.

Made-with: Cursor
@tonxxd tonxxd requested a review from a team as a code owner April 17, 2026 17:34
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

🦋 Changeset detected

Latest commit: ef7b04b

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

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
Copy Markdown

pkg-pr-new bot commented Apr 17, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@modelcontextprotocol/sdk@1926

commit: ef7b04b

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.

1 participant