fix(server): escape U+2028 / U+2029 in SSE data lines (v2)#1925
Open
tonxxd wants to merge 1 commit intomodelcontextprotocol:mainfrom
Open
fix(server): escape U+2028 / U+2029 in SSE data lines (v2)#1925tonxxd wants to merge 1 commit intomodelcontextprotocol:mainfrom
tonxxd wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
JSON.stringify leaves LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029) unescaped inside JSON strings. Many real-world SSE client parsers — browser EventSource historically, webview-embedded parsers used by Claude Desktop / ChatGPT app widgets, and libraries that split on a broader line-terminator regex — treat those codepoints as line terminators, truncating the `data:` line mid-JSON. The client then silently fails to parse the message, and the tool call never resolves on the client (the widget / UI stays stuck in an "invoking" state). 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 of U+2028/U+2029 is a long-established practice for JSON over SSE. The Python SDK hit the same pathology (see modelcontextprotocol/python-sdk#1356) and needed a fix in its client SSE parser (httpx-sse 0.4.2). Fixed in WebStandardStreamableHTTPServerTransport#writeSSEEvent. Added a regression test that registers a tool which returns text containing both codepoints and asserts the literal characters do not appear on the wire (they are escaped to `\u2028` / `\u2029`) while JSON.parse of the `data:` line still reproduces the original text. Made-with: Cursor
🦋 Changeset detectedLatest commit: 91906db The changes in this PR will be included in the next version bump. This PR includes changesets to release 5 packages
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 |
@modelcontextprotocol/client
@modelcontextprotocol/server
@modelcontextprotocol/express
@modelcontextprotocol/fastify
@modelcontextprotocol/hono
@modelcontextprotocol/node
commit: |
9 tasks
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 V1 equivalent
https://github.com/modelcontextprotocol/typescript-sdk/pull/1926Escape
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).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