From 6bcf0fa3679148a7b24edc274835997e2335e6c6 Mon Sep 17 00:00:00 2001 From: John Howard Date: Mon, 6 Jul 2026 09:33:47 -0700 Subject: [PATCH 1/2] test_streaming_elicitation: use proper SSE instead of NDJSON NDJSON is not acceptable in the spec. Before: ``` $ curl -iN --max-time 5 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' -H 'Mcp-Name: test_streaming_elicitation' --data '{ "jsonrpc":"2.0", "id":1, "method":"tools/call", "params":{ "name":"test_streaming_elicitation", "arguments":{}, "_meta":{ "io.modelcontextprotocol/protocolVersion":"2026-07-28", "io.modelcontextprotocol/clientInfo":{"name":"conformance-client","version":"1.0.0"}, "io.modelcontextprotocol/clientCapabilities":{} } } }' http://localhost:3001/mcp HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Mcp-Session-Id Content-Type: application/json Transfer-Encoding: chunked Date: Mon, 06 Jul 2026 16:30:12 GMT Connection: keep-alive Keep-Alive: timeout=5 {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"token-abc","total":100,"value":50}} {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Streaming complete"}]}} ``` After: ``` $ curl -iN --max-time 5 -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -H 'MCP-Protocol-Version: 2026-07-28' -H 'Mcp-Method: tools/call' -H 'Mcp-Name: test_streaming_elicitation' --data '{ "jsonrpc":"2.0", "id":1, "method":"tools/call", "params":{ "name":"test_streaming_elicitation", "arguments":{}, "_meta":{ "io.modelcontextprotocol/protocolVersion":"2026-07-28", "io.modelcontextprotocol/clientInfo":{"name":"conformance-client","version":"1.0.0"}, "io.modelcontextprotocol/clientCapabilities":{} } } }' http://localhost:3001/mcp HTTP/1.1 200 OK X-Powered-By: Express Access-Control-Allow-Origin: * Access-Control-Expose-Headers: Mcp-Session-Id Content-Type: text/event-stream Cache-Control: no-cache Date: Mon, 06 Jul 2026 16:32:49 GMT Connection: keep-alive Keep-Alive: timeout=5 Transfer-Encoding: chunked event: message data: {"jsonrpc":"2.0","method":"notifications/progress","params":{"progressToken":"token-abc","total":100,"value":50}} event: message data: {"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"Streaming complete"}]}} ``` --- .../servers/typescript/everything-server.ts | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index 39be2f06..35af2396 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -1235,6 +1235,10 @@ const LEGACY_SESSION_PROTOCOL_VERSIONS = [ '2025-11-25' ]; +function writeSseMessage(res: import('express').Response, msg: unknown) { + res.write(`event: message\ndata: ${JSON.stringify(msg)}\n\n`); +} + // Handle POST requests - stateful mode app.post('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; @@ -2128,55 +2132,49 @@ app.post('/mcp', async (req, res) => { // Progressive IncompleteResult Stream Generator Handling if (name === 'test_streaming_elicitation') { res.writeHead(200, { - 'Content-Type': 'application/json', - 'Transfer-Encoding': 'chunked' + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache' }); - res.write( - JSON.stringify({ - jsonrpc: '2.0', - method: 'notifications/progress', // Emits standard progress notice - params: { progressToken: 'token-abc', total: 100, value: 50 } - }) + '\n' - ); + writeSseMessage(res, { + jsonrpc: '2.0', + method: 'notifications/progress', // Emits standard progress notice + params: { progressToken: 'token-abc', total: 100, value: 50 } + }); - return res.end( - JSON.stringify({ - jsonrpc: '2.0', - id, - result: { content: [{ type: 'text', text: 'Streaming complete' }] } - }) - ); + writeSseMessage(res, { + jsonrpc: '2.0', + id, + result: { content: [{ type: 'text', text: 'Streaming complete' }] } + }); + return res.end(); } // Contextual Logging Constraints Verification Handler if (name === 'test_logging_tool') { res.writeHead(200, { - 'Content-Type': 'application/json', - 'Transfer-Encoding': 'chunked' + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache' }); // RULE: No logs allowed if meta configuration lacks explicit log level bounds if (meta && meta['io.modelcontextprotocol/logLevel']) { - res.write( - JSON.stringify({ - jsonrpc: '2.0', - method: 'notifications/message', - params: { - level: 'info', - text: 'Diagnostic trace logging activated' - } - }) + '\n' - ); + writeSseMessage(res, { + jsonrpc: '2.0', + method: 'notifications/message', + params: { + level: 'info', + text: 'Diagnostic trace logging activated' + } + }); } - return res.end( - JSON.stringify({ - jsonrpc: '2.0', - id, - result: { content: [{ type: 'text', text: 'Logging evaluated' }] } - }) - ); + writeSseMessage(res, { + jsonrpc: '2.0', + id, + result: { content: [{ type: 'text', text: 'Logging evaluated' }] } + }); + return res.end(); } // Helper mutation hooks used by dynamic tests to force stream activity @@ -2212,19 +2210,21 @@ app.post('/mcp', async (req, res) => { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' }); - const write = (msg: unknown) => - res.write(`event: message\ndata: ${JSON.stringify(msg)}\n\n`); const dispatch = await getStatelessDispatchClient(); try { const result = await dispatch.client.request( { method, params }, ResultSchema as any ); - for (const n of dispatch.drainNotifications()) write(n); - write({ jsonrpc: '2.0', id, result }); + for (const n of dispatch.drainNotifications()) { + writeSseMessage(res, n); + } + writeSseMessage(res, { jsonrpc: '2.0', id, result }); } catch (e: any) { - for (const n of dispatch.drainNotifications()) write(n); - write({ + for (const n of dispatch.drainNotifications()) { + writeSseMessage(res, n); + } + writeSseMessage(res, { jsonrpc: '2.0', id, error: { code: e.code ?? -32603, message: e.message, data: e.data } From f2fa81ff75ad07d908edf0aa97220fc753fb021d Mon Sep 17 00:00:00 2001 From: John Howard Date: Mon, 6 Jul 2026 10:45:19 -0700 Subject: [PATCH 2/2] Also do it for notifications --- .../servers/typescript/everything-server.ts | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/examples/servers/typescript/everything-server.ts b/examples/servers/typescript/everything-server.ts index 35af2396..c929d559 100644 --- a/examples/servers/typescript/everything-server.ts +++ b/examples/servers/typescript/everything-server.ts @@ -1202,17 +1202,15 @@ function notifyListenStreams( for (const stream of activeListenStreams) { const wants = type === 'tools' ? stream.wantsTools : stream.wantsPrompts; if (!wants) continue; - stream.res.write( - JSON.stringify({ - jsonrpc: '2.0', - method: notificationMethod, - params: { - _meta: { - 'io.modelcontextprotocol/subscriptionId': stream.subscriptionId - } + writeSseMessage(stream.res, { + jsonrpc: '2.0', + method: notificationMethod, + params: { + _meta: { + 'io.modelcontextprotocol/subscriptionId': stream.subscriptionId } - }) + '\n' - ); + } + }); } } @@ -1315,13 +1313,12 @@ app.post('/mcp', async (req, res) => { }); } - // Subscriptions Listening Endpoint Stream Handler (SSE/Chunked Line) + // Subscriptions Listening Endpoint Stream Handler (SSE) if (method === 'subscriptions/listen') { res.writeHead(200, { - 'Content-Type': 'application/json', + 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', - Connection: 'keep-alive', - 'Transfer-Encoding': 'chunked' + Connection: 'keep-alive' }); const requestedNotifications = params.notifications || {}; @@ -1344,7 +1341,7 @@ app.post('/mcp', async (req, res) => { } } }; - res.write(JSON.stringify(ackFrame) + '\n'); + writeSseMessage(res, ackFrame); // Keep the stream open and register it so list-changed notifications // triggered by later requests are delivered to it. The stream ends when