Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8a4601d
Add conformance tests for SEP-1699 SSE polling
felixweinberger Nov 14, 2025
be3d929
fix: correct SSE retry timing test to track GET requests only
felixweinberger Nov 17, 2025
de5ebab
fix: improve SSE conformance test validation (SEP-1699)
felixweinberger Nov 17, 2025
12de2ce
fix: capture SSE retry field via onRetry callback
felixweinberger Nov 17, 2025
32fb397
style: fix Prettier formatting
felixweinberger Nov 17, 2025
bc2298d
fix: handle 400 response in SSE polling scenario
felixweinberger Nov 17, 2025
bc7c649
feat: update SEP-1699 conformance tests for POST stream priming events
felixweinberger Nov 18, 2025
2b1245d
feat: add conformance test for multiple concurrent GET SSE streams
felixweinberger Nov 18, 2025
e022392
fix: test POST-initiated concurrent streams instead of standalone GET
felixweinberger Nov 20, 2025
9cbf0fa
style: apply prettier formatting
felixweinberger Nov 20, 2025
6c2646b
Merge main and resolve conflict in everything-server.ts
felixweinberger Nov 20, 2025
03a4c0f
Merge main (CIMD conformance test) and resolve conflict
felixweinberger Nov 20, 2025
acb22cf
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 20, 2025
2af3a13
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 21, 2025
d305d3a
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 23, 2025
608793b
some logging things, and fix for warning check for bare GET
pcarleton Nov 24, 2025
f58370a
switch to tool call to make more representative
pcarleton Nov 24, 2025
c385462
server pretty printing, add server tool, adjust server test scenario
pcarleton Nov 24, 2025
8c14b80
fix nested package-lock
pcarleton Nov 24, 2025
e6898e7
return error if tool call fails, and update package locks
pcarleton Nov 24, 2025
4982c12
mark polling as pending
pcarleton Nov 24, 2025
f9f863d
fix: exclude nested node_modules from vitest tests
pcarleton Nov 24, 2025
085b9e0
Merge branch 'main' into fweinberger/sep-1699-test
felixweinberger Nov 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions examples/clients/typescript/sse-retry-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env node

/**
* SSE Retry Test Client
*
* Tests that the MCP client respects the SSE retry field when reconnecting.
* This client connects to a test server that closes the SSE stream mid-tool-call,
* then waits for the client to reconnect and sends the tool result.
*/

import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';

async function main(): Promise<void> {
const serverUrl = process.argv[2];

if (!serverUrl) {
console.error('Usage: sse-retry-test <server-url>');
process.exit(1);
}

console.log(`Connecting to MCP server at: ${serverUrl}`);
console.log('This test validates SSE retry field compliance (SEP-1699)');

try {
const client = new Client(
{
name: 'sse-retry-test-client',
version: '1.0.0'
},
{
capabilities: {}
}
);

const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
reconnectionOptions: {
initialReconnectionDelay: 1000,
maxReconnectionDelay: 10000,
reconnectionDelayGrowFactor: 1.5,
maxRetries: 3
}
});

transport.onerror = (error) => {
console.log(`Transport error: ${error.message}`);
};

transport.onclose = () => {
console.log('Transport closed');
};

console.log('Initiating connection...');
await client.connect(transport);
console.log('Connected to MCP server');

console.log('Calling test_reconnection tool...');
console.log(
'Server will close SSE stream mid-call and send result after reconnection'
);

const result = await client.request(
{
method: 'tools/call',
params: {
name: 'test_reconnection',
arguments: {}
}
},
CallToolResultSchema
);

console.log('Tool call completed:', JSON.stringify(result, null, 2));

await transport.close();
console.log('Connection closed successfully');

process.exit(0);
} catch (error) {
console.error('Test failed:', error);
process.exit(1);
}
}

main().catch((error) => {
console.error('Unhandled error:', error);
process.exit(1);
});
84 changes: 83 additions & 1 deletion examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import {
McpServer,
ResourceTemplate
} from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
StreamableHTTPServerTransport,
EventStore,
EventId,
StreamId
} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import {
ElicitResultSchema,
ListToolsRequestSchema,
Expand All @@ -33,6 +38,41 @@ const watchedResourceContent = 'Watched resource content';
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const servers: { [sessionId: string]: McpServer } = {};

// In-memory event store for SEP-1699 resumability
const eventStoreData = new Map<
string,
{ eventId: string; message: any; streamId: string }
>();

function createEventStore(): EventStore {
return {
async storeEvent(streamId: StreamId, message: any): Promise<EventId> {
const eventId = `${streamId}::${Date.now()}_${randomUUID()}`;
eventStoreData.set(eventId, { eventId, message, streamId });
return eventId;
},
async replayEventsAfter(
lastEventId: EventId,
{ send }: { send: (eventId: EventId, message: any) => Promise<void> }
): Promise<StreamId> {
const streamId = lastEventId.split('::')[0];
const eventsToReplay: Array<[string, { message: any }]> = [];
for (const [eventId, data] of eventStoreData.entries()) {
if (data.streamId === streamId && eventId > lastEventId) {
eventsToReplay.push([eventId, data]);
}
}
eventsToReplay.sort(([a], [b]) => a.localeCompare(b));
for (const [eventId, { message }] of eventsToReplay) {
if (Object.keys(message).length > 0) {
await send(eventId, message);
}
}
return streamId;
}
};
}

// Sample base64 encoded 1x1 red PNG pixel for testing
const TEST_IMAGE_BASE64 =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';
Expand Down Expand Up @@ -309,6 +349,46 @@ function createMcpServer() {
}
);

// SEP-1699: Reconnection test tool - closes SSE stream mid-call to test client reconnection
mcpServer.registerTool(
'test_reconnection',
{
description:
'Tests SSE stream disconnection and client reconnection (SEP-1699). Server will close the stream mid-call and send the result after client reconnects.',
inputSchema: {}
},
async (_args, { sessionId, requestId }) => {
const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));

console.log(`[${sessionId}] Starting test_reconnection tool...`);

// Get the transport for this session
const transport = sessionId ? transports[sessionId] : undefined;
if (transport && requestId) {
// Close the SSE stream to trigger client reconnection
console.log(
`[${sessionId}] Closing SSE stream to trigger client polling...`
);
transport.closeSSEStream(requestId);
}

// Wait for client to reconnect (should respect retry field)
await sleep(100);

console.log(`[${sessionId}] test_reconnection tool complete`);

return {
content: [
{
type: 'text',
text: 'Reconnection test completed successfully. If you received this, the client properly reconnected after stream closure.'
}
]
};
}
);

// Sampling tool - requests LLM completion from client
mcpServer.registerTool(
'test_sampling',
Expand Down Expand Up @@ -1003,6 +1083,8 @@ app.post('/mcp', async (req, res) => {

transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
eventStore: createEventStore(),
retryInterval: 5000, // 5 second retry interval for SEP-1699
onsessioninitialized: (newSessionId) => {
transports[newSessionId] = transport;
servers[newSessionId] = mcpServer;
Expand Down
Loading
Loading