fix(sse): escape U+2028 / U+2029 in SSE data lines (V1)#1926
Open
tonxxd wants to merge 2 commits intomodelcontextprotocol:v1.xfrom
Open
fix(sse): escape U+2028 / U+2029 in SSE data lines (V1)#1926tonxxd wants to merge 2 commits intomodelcontextprotocol:v1.xfrom
tonxxd wants to merge 2 commits intomodelcontextprotocol:v1.xfrom
Conversation
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
🦋 Changeset detectedLatest 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 |
commit: |
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.
This PR has a
v2equivalent #1925Escape
U+2028(LINE SEPARATOR) andU+2029(PARAGRAPH SEPARATOR) in SSEdata:lines emitted byWebStandardStreamableHTTPServerTransport.Motivation and Context
JSON.stringifyleavesU+2028/U+2029unescaped, 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 thedata: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-sse0.4.2).Reproduced end-to-end against an mcp-use server whose Algolia-backed tool returned a candidate whose
skillsfield contained a literalU+2028How Has This Been Tested?
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 assertsJSON.parseof thedata:line round-trips to the original string.pnpm --filter @modelcontextprotocol/server test→ 41/41 instreamableHttp.test.ts, 56/56 across the package.pnpm test:all→ green across all packages and the 422-test integration suite. (One pre-existingbetter-sqlite3native-binding failure inexamples/sharedon my machine is reproducible on cleanorigin/mainand unrelated to this PR.)pnpm lint:allclean.isPending: true; after → tool result surfaces and widget renders normally.Breaking Changes
None. The escape is invisible to SSE-spec-compliant clients (
JSON.parsereinflates\u2028/\u2029back to the original codepoints). Non-compliant clients that previously truncated on these codepoints now receive the full payload.Types of changes
Checklist
Additional context
Scope intentionally narrow: only the SSE framing in
WebStandardStreamableHTTPServerTransport.writeSSEEventis touched. The JSON-response path (enableJsonResponse) is unaffectedRelated:
httpx-sseclient-side fix: Incorrect newline parsing florimondmanca/httpx-sse#34