Skip to content

Commit 01955fc

Browse files
committed
Merge main into fweinberger/test-ts-sdk-1.23.0-beta.0
2 parents bc47080 + e5cdffe commit 01955fc

26 files changed

+2567
-361
lines changed

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Enforce LF line endings for all text files
2+
* text=auto eol=lf
3+

.github/dependabot.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
version: 2
2+
3+
updates:
4+
- package-ecosystem: npm
5+
directory: /
6+
schedule:
7+
interval: weekly
8+
cooldown:
9+
default-days: 7
10+
11+
- package-ecosystem: github-actions
12+
directory: /
13+
schedule:
14+
interval: weekly
15+
cooldown:
16+
default-days: 7

.prettierrc.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"endOfLine": "lf",
23
"singleQuote": true,
34
"trailingComma": "none",
45
"overrides": [
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* SSE Retry Test Client
5+
*
6+
* Tests that the MCP client respects the SSE retry field when reconnecting.
7+
* This client connects to a test server that closes the SSE stream mid-tool-call,
8+
* then waits for the client to reconnect and sends the tool result.
9+
*/
10+
11+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
12+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
13+
import { CallToolResultSchema } from '@modelcontextprotocol/sdk/types.js';
14+
15+
async function main(): Promise<void> {
16+
const serverUrl = process.argv[2];
17+
18+
if (!serverUrl) {
19+
console.error('Usage: sse-retry-test <server-url>');
20+
process.exit(1);
21+
}
22+
23+
console.log(`Connecting to MCP server at: ${serverUrl}`);
24+
console.log('This test validates SSE retry field compliance (SEP-1699)');
25+
26+
try {
27+
const client = new Client(
28+
{
29+
name: 'sse-retry-test-client',
30+
version: '1.0.0'
31+
},
32+
{
33+
capabilities: {}
34+
}
35+
);
36+
37+
const transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
38+
reconnectionOptions: {
39+
initialReconnectionDelay: 1000,
40+
maxReconnectionDelay: 10000,
41+
reconnectionDelayGrowFactor: 1.5,
42+
maxRetries: 3
43+
}
44+
});
45+
46+
transport.onerror = (error) => {
47+
console.log(`Transport error: ${error.message}`);
48+
};
49+
50+
transport.onclose = () => {
51+
console.log('Transport closed');
52+
};
53+
54+
console.log('Initiating connection...');
55+
await client.connect(transport);
56+
console.log('Connected to MCP server');
57+
58+
console.log('Calling test_reconnection tool...');
59+
console.log(
60+
'Server will close SSE stream mid-call and send result after reconnection'
61+
);
62+
63+
const result = await client.request(
64+
{
65+
method: 'tools/call',
66+
params: {
67+
name: 'test_reconnection',
68+
arguments: {}
69+
}
70+
},
71+
CallToolResultSchema
72+
);
73+
74+
console.log('Tool call completed:', JSON.stringify(result, null, 2));
75+
76+
await transport.close();
77+
console.log('Connection closed successfully');
78+
79+
process.exit(0);
80+
} catch (error) {
81+
console.error('Test failed:', error);
82+
process.exit(1);
83+
}
84+
}
85+
86+
main().catch((error) => {
87+
console.error('Unhandled error:', error);
88+
process.exit(1);
89+
});

examples/servers/typescript/everything-server.ts

Lines changed: 83 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 {
1722
ElicitResultSchema,
1823
ListToolsRequestSchema,
@@ -33,6 +38,41 @@ const watchedResourceContent = 'Watched resource content';
3338
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
3439
const servers: { [sessionId: string]: McpServer } = {};
3540

41+
// In-memory event store for SEP-1699 resumability
42+
const eventStoreData = new Map<
43+
string,
44+
{ eventId: string; message: any; streamId: string }
45+
>();
46+
47+
function createEventStore(): EventStore {
48+
return {
49+
async storeEvent(streamId: StreamId, message: any): Promise<EventId> {
50+
const eventId = `${streamId}::${Date.now()}_${randomUUID()}`;
51+
eventStoreData.set(eventId, { eventId, message, streamId });
52+
return eventId;
53+
},
54+
async replayEventsAfter(
55+
lastEventId: EventId,
56+
{ send }: { send: (eventId: EventId, message: any) => Promise<void> }
57+
): Promise<StreamId> {
58+
const streamId = lastEventId.split('::')[0];
59+
const eventsToReplay: Array<[string, { message: any }]> = [];
60+
for (const [eventId, data] of eventStoreData.entries()) {
61+
if (data.streamId === streamId && eventId > lastEventId) {
62+
eventsToReplay.push([eventId, data]);
63+
}
64+
}
65+
eventsToReplay.sort(([a], [b]) => a.localeCompare(b));
66+
for (const [eventId, { message }] of eventsToReplay) {
67+
if (Object.keys(message).length > 0) {
68+
await send(eventId, message);
69+
}
70+
}
71+
return streamId;
72+
}
73+
};
74+
}
75+
3676
// Sample base64 encoded 1x1 red PNG pixel for testing
3777
const TEST_IMAGE_BASE64 =
3878
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg==';
@@ -309,6 +349,46 @@ function createMcpServer() {
309349
}
310350
);
311351

352+
// SEP-1699: Reconnection test tool - closes SSE stream mid-call to test client reconnection
353+
mcpServer.registerTool(
354+
'test_reconnection',
355+
{
356+
description:
357+
'Tests SSE stream disconnection and client reconnection (SEP-1699). Server will close the stream mid-call and send the result after client reconnects.',
358+
inputSchema: {}
359+
},
360+
async (_args, { sessionId, requestId }) => {
361+
const sleep = (ms: number) =>
362+
new Promise((resolve) => setTimeout(resolve, ms));
363+
364+
console.log(`[${sessionId}] Starting test_reconnection tool...`);
365+
366+
// Get the transport for this session
367+
const transport = sessionId ? transports[sessionId] : undefined;
368+
if (transport && requestId) {
369+
// Close the SSE stream to trigger client reconnection
370+
console.log(
371+
`[${sessionId}] Closing SSE stream to trigger client polling...`
372+
);
373+
transport.closeSSEStream(requestId);
374+
}
375+
376+
// Wait for client to reconnect (should respect retry field)
377+
await sleep(100);
378+
379+
console.log(`[${sessionId}] test_reconnection tool complete`);
380+
381+
return {
382+
content: [
383+
{
384+
type: 'text',
385+
text: 'Reconnection test completed successfully. If you received this, the client properly reconnected after stream closure.'
386+
}
387+
]
388+
};
389+
}
390+
);
391+
312392
// Sampling tool - requests LLM completion from client
313393
mcpServer.registerTool(
314394
'test_sampling',
@@ -1006,6 +1086,8 @@ app.post('/mcp', async (req, res) => {
10061086

10071087
transport = new StreamableHTTPServerTransport({
10081088
sessionIdGenerator: () => randomUUID(),
1089+
eventStore: createEventStore(),
1090+
retryInterval: 5000, // 5 second retry interval for SEP-1699
10091091
onsessioninitialized: (newSessionId) => {
10101092
transports[newSessionId] = transport;
10111093
servers[newSessionId] = mcpServer;

0 commit comments

Comments
 (0)