Skip to content

Commit bc7c649

Browse files
feat: update SEP-1699 conformance tests for POST stream priming events
- Rewrite server tests to validate priming events on POST SSE streams - Update client tests for POST→GET reconnection flow with retry timing - Add eventStore and retryInterval to example server - All 3 server conformance checks now pass
1 parent bc2298d commit bc7c649

File tree

4 files changed

+409
-177
lines changed

4 files changed

+409
-177
lines changed

examples/clients/typescript/sse-retry-test.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,20 @@ async function main(): Promise<void> {
5656
console.log('Connected to MCP server');
5757

5858
// Keep connection alive to observe reconnection behavior
59-
// The server will disconnect and the client should reconnect
59+
// The server will close the POST SSE stream and the client should reconnect via GET
6060
console.log('Waiting for reconnection cycle...');
6161
console.log(
62-
'Server will close connection and client should wait for retry field timing'
62+
'Server will send priming event with retry field, then close POST SSE stream'
63+
);
64+
console.log(
65+
'Client should wait for retry period (2000ms) then reconnect via GET with Last-Event-ID'
6366
);
6467

6568
// Wait long enough for:
66-
// 1. Server to send retry field and close (100ms)
67-
// 2. Client to wait for retry period (2000ms expected)
68-
// 3. Client to reconnect (100ms)
69-
// 4. Second disconnect cycle (optional)
69+
// 1. Server to send priming event with retry field on POST SSE stream (100ms)
70+
// 2. Server closes POST stream to trigger reconnection
71+
// 3. Client waits for retry period (2000ms expected)
72+
// 4. Client reconnects via GET with Last-Event-ID header
7073
await new Promise((resolve) => setTimeout(resolve, 6000));
7174

7275
console.log('Test duration complete');

examples/servers/typescript/everything-server.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import {
1212
McpServer,
1313
ResourceTemplate
1414
} from '@modelcontextprotocol/sdk/server/mcp.js';
15-
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
15+
import {
16+
StreamableHTTPServerTransport,
17+
EventStore,
18+
EventId,
19+
StreamId
20+
} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
1621
import { z } from 'zod';
1722
import express from 'express';
1823
import cors from 'cors';
@@ -26,6 +31,41 @@ const watchedResourceContent = 'Watched resource content';
2631
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
2732
const servers: { [sessionId: string]: McpServer } = {};
2833

34+
// In-memory event store for SEP-1699 resumability
35+
const eventStoreData = new Map<
36+
string,
37+
{ eventId: string; message: any; streamId: string }
38+
>();
39+
40+
function createEventStore(): EventStore {
41+
return {
42+
async storeEvent(streamId: StreamId, message: any): Promise<EventId> {
43+
const eventId = `${streamId}::${Date.now()}_${randomUUID()}`;
44+
eventStoreData.set(eventId, { eventId, message, streamId });
45+
return eventId;
46+
},
47+
async replayEventsAfter(
48+
lastEventId: EventId,
49+
{ send }: { send: (eventId: EventId, message: any) => Promise<void> }
50+
): Promise<StreamId> {
51+
const streamId = lastEventId.split('::')[0];
52+
const eventsToReplay: Array<[string, { message: any }]> = [];
53+
for (const [eventId, data] of eventStoreData.entries()) {
54+
if (data.streamId === streamId && eventId > lastEventId) {
55+
eventsToReplay.push([eventId, data]);
56+
}
57+
}
58+
eventsToReplay.sort(([a], [b]) => a.localeCompare(b));
59+
for (const [eventId, { message }] of eventsToReplay) {
60+
if (Object.keys(message).length > 0) {
61+
await send(eventId, message);
62+
}
63+
}
64+
return streamId;
65+
}
66+
};
67+
}
68+
2969
// Sample base64 encoded 1x1 red PNG pixel for testing
3070
const TEST_IMAGE_BASE64 =
3171
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';
@@ -872,6 +912,8 @@ app.post('/mcp', async (req, res) => {
872912

873913
transport = new StreamableHTTPServerTransport({
874914
sessionIdGenerator: () => randomUUID(),
915+
eventStore: createEventStore(),
916+
retryInterval: 5000, // 5 second retry interval for SEP-1699
875917
onsessioninitialized: (newSessionId) => {
876918
transports[newSessionId] = transport;
877919
servers[newSessionId] = mcpServer;

0 commit comments

Comments
 (0)