Skip to content

Commit 05c26b3

Browse files
fix: disable priming events to fix backwards compatibility
Priming events (SEP-1699) have empty SSE data which clients in the 1.23.x line cannot handle - they crash trying to JSON.parse(""). This patch disables priming events entirely to prevent breaking existing clients. Priming events will be re-enabled in 1.24.x with proper protocol version gating to only send them to clients that can handle empty SSE data.
1 parent e6c71bb commit 05c26b3

File tree

2 files changed

+18
-136
lines changed

2 files changed

+18
-136
lines changed

src/server/streamableHttp.test.ts

Lines changed: 10 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1592,12 +1592,13 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
15921592
});
15931593

15941594
// Test SSE priming events for POST streams
1595-
describe('StreamableHTTPServerTransport POST SSE priming events', () => {
1595+
// NOTE: Priming events are DISABLED in 1.23.1 to fix backwards compatibility issues.
1596+
// Clients in the 1.23.x line cannot handle empty SSE data (they crash on JSON.parse("")).
1597+
describe('StreamableHTTPServerTransport POST SSE priming events (DISABLED in 1.23.1)', () => {
15961598
let server: Server;
15971599
let transport: StreamableHTTPServerTransport;
15981600
let baseUrl: URL;
15991601
let sessionId: string;
1600-
let mcpServer: McpServer;
16011602

16021603
// Simple eventStore for priming event tests
16031604
const createEventStore = (): EventStore => {
@@ -1641,7 +1642,7 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
16411642
}
16421643
});
16431644

1644-
it('should send priming event with retry field on POST SSE stream', async () => {
1645+
it('should NOT send priming events (disabled in 1.23.1 for backwards compatibility)', async () => {
16451646
const result = await createTestServer({
16461647
sessionIdGenerator: () => randomUUID(),
16471648
eventStore: createEventStore(),
@@ -1650,7 +1651,6 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
16501651
server = result.server;
16511652
transport = result.transport;
16521653
baseUrl = result.baseUrl;
1653-
mcpServer = result.mcpServer;
16541654

16551655
// Initialize to get session ID
16561656
const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
@@ -1671,136 +1671,23 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => {
16711671
'Content-Type': 'application/json',
16721672
Accept: 'text/event-stream, application/json',
16731673
'mcp-session-id': sessionId,
1674-
'mcp-protocol-version': '2025-03-26'
1674+
'mcp-protocol-version': '2025-06-18'
16751675
},
16761676
body: JSON.stringify(toolCallRequest)
16771677
});
16781678

16791679
expect(postResponse.status).toBe(200);
16801680
expect(postResponse.headers.get('content-type')).toBe('text/event-stream');
16811681

1682-
// Read the priming event
1682+
// Read the first chunk - should be the actual response, not a priming event
16831683
const reader = postResponse.body?.getReader();
16841684
const { value } = await reader!.read();
16851685
const text = new TextDecoder().decode(value);
16861686

1687-
// Verify priming event has id and retry field
1688-
expect(text).toContain('id: ');
1689-
expect(text).toContain('retry: 5000');
1690-
expect(text).toContain('data: ');
1691-
});
1692-
1693-
it('should send priming event without retry field when retryInterval is not configured', async () => {
1694-
const result = await createTestServer({
1695-
sessionIdGenerator: () => randomUUID(),
1696-
eventStore: createEventStore()
1697-
// No retryInterval
1698-
});
1699-
server = result.server;
1700-
transport = result.transport;
1701-
baseUrl = result.baseUrl;
1702-
mcpServer = result.mcpServer;
1703-
1704-
// Initialize to get session ID
1705-
const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
1706-
sessionId = initResponse.headers.get('mcp-session-id') as string;
1707-
expect(sessionId).toBeDefined();
1708-
1709-
// Send a tool call request
1710-
const toolCallRequest: JSONRPCMessage = {
1711-
jsonrpc: '2.0',
1712-
id: 100,
1713-
method: 'tools/call',
1714-
params: { name: 'greet', arguments: { name: 'Test' } }
1715-
};
1716-
1717-
const postResponse = await fetch(baseUrl, {
1718-
method: 'POST',
1719-
headers: {
1720-
'Content-Type': 'application/json',
1721-
Accept: 'text/event-stream, application/json',
1722-
'mcp-session-id': sessionId,
1723-
'mcp-protocol-version': '2025-03-26'
1724-
},
1725-
body: JSON.stringify(toolCallRequest)
1726-
});
1727-
1728-
expect(postResponse.status).toBe(200);
1729-
1730-
// Read the priming event
1731-
const reader = postResponse.body?.getReader();
1732-
const { value } = await reader!.read();
1733-
const text = new TextDecoder().decode(value);
1734-
1735-
// Priming event should have id field but NOT retry field
1736-
expect(text).toContain('id: ');
1737-
expect(text).toContain('data: ');
1738-
expect(text).not.toContain('retry:');
1739-
});
1740-
1741-
it('should close POST SSE stream when closeSseStream is called', async () => {
1742-
const result = await createTestServer({
1743-
sessionIdGenerator: () => randomUUID(),
1744-
eventStore: createEventStore(),
1745-
retryInterval: 1000
1746-
});
1747-
server = result.server;
1748-
transport = result.transport;
1749-
baseUrl = result.baseUrl;
1750-
mcpServer = result.mcpServer;
1751-
1752-
// Track tool execution state
1753-
let toolResolve: () => void;
1754-
const toolPromise = new Promise<void>(resolve => {
1755-
toolResolve = resolve;
1756-
});
1757-
1758-
// Register a blocking tool
1759-
mcpServer.tool('blocking-tool', 'A blocking tool', {}, async () => {
1760-
await toolPromise;
1761-
return { content: [{ type: 'text', text: 'Done' }] };
1762-
});
1763-
1764-
// Initialize to get session ID
1765-
const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
1766-
sessionId = initResponse.headers.get('mcp-session-id') as string;
1767-
expect(sessionId).toBeDefined();
1768-
1769-
// Send a tool call request
1770-
const toolCallRequest: JSONRPCMessage = {
1771-
jsonrpc: '2.0',
1772-
id: 100,
1773-
method: 'tools/call',
1774-
params: { name: 'blocking-tool', arguments: {} }
1775-
};
1776-
1777-
const postResponse = await fetch(baseUrl, {
1778-
method: 'POST',
1779-
headers: {
1780-
'Content-Type': 'application/json',
1781-
Accept: 'text/event-stream, application/json',
1782-
'mcp-session-id': sessionId,
1783-
'mcp-protocol-version': '2025-03-26'
1784-
},
1785-
body: JSON.stringify(toolCallRequest)
1786-
});
1787-
1788-
expect(postResponse.status).toBe(200);
1789-
1790-
const reader = postResponse.body?.getReader();
1791-
1792-
// Read the priming event
1793-
await reader!.read();
1794-
1795-
// Close the SSE stream
1796-
transport.closeSSEStream(100);
1797-
1798-
// Stream should now be closed
1799-
const { done } = await reader!.read();
1800-
expect(done).toBe(true);
1801-
1802-
// Clean up - resolve the tool promise
1803-
toolResolve!();
1687+
// Should NOT contain a priming event (which would have empty data)
1688+
// The first message should be the actual tool result with event: message
1689+
expect(text).toContain('event: message');
1690+
expect(text).toContain('"result"');
18041691
});
18051692
});
18061693

src/server/streamableHttp.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -270,20 +270,15 @@ export class StreamableHTTPServerTransport implements Transport {
270270

271271
/**
272272
* Writes a priming event to establish resumption capability.
273-
* Only sends if eventStore is configured (opt-in for resumability).
273+
*
274+
* DISABLED in 1.23.1: Priming events have empty SSE data which clients
275+
* in the 1.23.x line cannot handle (they crash trying to JSON.parse("")).
276+
* This feature is disabled to prevent breaking existing clients.
277+
* Priming events will be re-enabled in 1.24.x with proper protocol version gating.
274278
*/
275-
private async _maybeWritePrimingEvent(res: ServerResponse, streamId: string): Promise<void> {
276-
if (!this._eventStore) {
277-
return;
278-
}
279-
280-
const primingEventId = await this._eventStore.storeEvent(streamId, {} as JSONRPCMessage);
281-
282-
let primingEvent = `id: ${primingEventId}\ndata: \n\n`;
283-
if (this._retryInterval !== undefined) {
284-
primingEvent = `id: ${primingEventId}\nretry: ${this._retryInterval}\ndata: \n\n`;
285-
}
286-
res.write(primingEvent);
279+
private async _maybeWritePrimingEvent(_res: ServerResponse, _streamId: string): Promise<void> {
280+
// Priming events disabled in 1.23.x - see docstring above
281+
return;
287282
}
288283

289284
/**

0 commit comments

Comments
 (0)