Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
33 changes: 16 additions & 17 deletions examples/clients/typescript/sse-retry-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
* SSE Retry Test Client
*
* Tests that the MCP client respects the SSE retry field when reconnecting.
* This client connects to a test server that sends retry: field and closes
* the connection, then validates that the client waits the appropriate time.
* 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];
Expand Down Expand Up @@ -42,7 +43,6 @@ async function main(): Promise<void> {
}
});

// Track reconnection events
transport.onerror = (error) => {
console.log(`Transport error: ${error.message}`);
};
Expand All @@ -55,24 +55,23 @@ async function main(): Promise<void> {
await client.connect(transport);
console.log('Connected to MCP server');

// Keep connection alive to observe reconnection behavior
// The server will close the POST SSE stream and the client should reconnect via GET
console.log('Waiting for reconnection cycle...');
console.log('Calling test_reconnection tool...');
console.log(
'Server will send priming event with retry field, then close POST SSE stream'
);
console.log(
'Client should wait for retry period (2000ms) then reconnect via GET with Last-Event-ID'
'Server will close SSE stream mid-call and send result after reconnection'
);

// Wait long enough for:
// 1. Server to send priming event with retry field on POST SSE stream (100ms)
// 2. Server closes POST stream to trigger reconnection
// 3. Client waits for retry period (2000ms expected)
// 4. Client reconnects via GET with Last-Event-ID header
await new Promise((resolve) => setTimeout(resolve, 6000));
const result = await client.request(
{
method: 'tools/call',
params: {
name: 'test_reconnection',
arguments: {}
}
},
CallToolResultSchema
);

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

await transport.close();
console.log('Connection closed successfully');
Expand Down
40 changes: 40 additions & 0 deletions examples/servers/typescript/everything-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,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
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,14 @@ program
'Suite to run: "active" (default, excludes pending), "all", or "pending"',
'active'
)
.option('--verbose', 'Show verbose output (JSON instead of pretty print)')
.action(async (options) => {
try {
// Validate options with Zod
const validated = ServerOptionsSchema.parse(options);

const verbose = options.verbose ?? false;

// If a single scenario is specified, run just that one
if (validated.scenario) {
const result = await runServerConformanceTest(
Expand All @@ -218,7 +221,8 @@ program

const { failed } = printServerResults(
result.checks,
result.scenarioDescription
result.scenarioDescription,
verbose
);
process.exit(failed > 0 ? 1 : 0);
} else {
Expand Down
11 changes: 8 additions & 3 deletions src/runner/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import { ConformanceCheck } from '../types';
import { getClientScenario } from '../scenarios';
import { ensureResultsDir, createResultDir } from './utils';
import { ensureResultsDir, createResultDir, formatPrettyChecks } from './utils';

/**
* Format markdown-style text for terminal output using ANSI codes
Expand Down Expand Up @@ -54,7 +54,8 @@ export async function runServerConformanceTest(

export function printServerResults(
checks: ConformanceCheck[],
scenarioDescription: string
scenarioDescription: string,
verbose: boolean = false
): {
passed: number;
failed: number;
Expand All @@ -68,7 +69,11 @@ export function printServerResults(
const failed = checks.filter((c) => c.status === 'FAILURE').length;
const warnings = checks.filter((c) => c.status === 'WARNING').length;

console.log(`Checks:\n${JSON.stringify(checks, null, 2)}`);
if (verbose) {
console.log(JSON.stringify(checks, null, 2));
} else {
console.log(`Checks:\n${formatPrettyChecks(checks)}`);
}

console.log(`\nTest Results:`);
console.log(
Expand Down
Loading
Loading