import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { createMcpExpressApp } from '../../server/express.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; import cors from 'cors'; // Factory to create a new MCP server per session. // Each session needs its own server+transport pair to avoid cross-session contamination. const getServer = () => { const server = new McpServer({ name: 'sse-polling-example', version: '1.0.0' }, { capabilities: { logging: {} } }); // Register a long-running tool that demonstrates server-initiated disconnect server.tool('long-task', 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.', {}, async (_args, extra) => { const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); console.log(`[${extra.sessionId}] Starting long-task...`); // Send first progress notification await server.sendLoggingMessage({ level: 'info', data: 'Progress: 25% - Starting work...' }, extra.sessionId); await sleep(1000); // Send second progress notification await server.sendLoggingMessage({ level: 'info', data: 'Progress: 50% - Halfway there...' }, extra.sessionId); await sleep(1000); // Server decides to disconnect the client to free resources // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval // Use extra.closeSSEStream callback - available when eventStore is configured if (extra.closeSSEStream) { console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`); extra.closeSSEStream(); } // Continue processing while client is disconnected // Events are stored in eventStore and will be replayed on reconnect await sleep(500); await server.sendLoggingMessage({ level: 'info', data: 'Progress: 75% - Almost done (sent while client disconnected)...' }, extra.sessionId); await sleep(500); await server.sendLoggingMessage({ level: 'info', data: 'Progress: 100% - Complete!' }, extra.sessionId); console.log(`[${extra.sessionId}] Task complete`); return { content: [ { type: 'text', text: 'Long task completed successfully!' } ] }; }); return server; }; // Set up Express app const app = createMcpExpressApp(); app.use(cors()); // Create event store for resumability const eventStore = new InMemoryEventStore(); // Close sessions that have been idle for IDLE_MS, and keep at most MAX_SESSIONS open const IDLE_MS = 30 * 60000; const MAX_SESSIONS = 1000; const sessions = new Map(); // Count open responses so a long-running request or a listening SSE stream is not treated as idle const trackResponse = (session, res) => { if (!res.socket || res.destroyed) return; session.open++; res.on('close', () => { session.open--; session.lastActive = Date.now(); }); }; // Close sessions with nothing open and no activity for IDLE_MS setInterval(() => { const cutoff = Date.now() - IDLE_MS; for (const { transport, open, lastActive } of sessions.values()) { if (open === 0 && lastActive < cutoff) transport.close().catch(console.error); } }, 60000).unref(); // Handle all MCP requests app.all('/mcp', async (req, res) => { const sessionId = req.headers['mcp-session-id']; // Reuse the existing transport for a known session const session = sessionId ? sessions.get(sessionId) : undefined; if (session) { trackResponse(session, res); await session.transport.handleRequest(req, res, req.body); return; } if (sessionId) { // Unknown or expired session ID - the client should start a new session res.status(404).json({ jsonrpc: '2.0', error: { code: -32001, message: 'Session not found' }, id: null }); return; } if (!isInitializeRequest(req.body)) { // Invalid request - no session ID and not an initialization request res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null }); return; } if (sessions.size >= MAX_SESSIONS) { res.status(503).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Too many open sessions' }, id: null }); return; } // New initialization request - create a transport for the session const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), eventStore, retryInterval: 2000, // Default retry interval for priming events onsessioninitialized: id => { console.log(`[${id}] Session initialized`); sessions.set(id, { transport, open: 0, lastActive: Date.now() }); } }); // Set up onclose handler to clean up transport when closed transport.onclose = () => { const sid = transport.sessionId; if (sid && sessions.has(sid)) { console.log(`[${sid}] Transport closed, removing from sessions map`); sessions.delete(sid); } }; // Create a new server per session and connect it to the transport const server = getServer(); await server.connect(transport); await transport.handleRequest(req, res, req.body); }); // Start the server const PORT = 3001; app.listen(PORT, () => { console.log(`SSE Polling Example Server running on http://localhost:${PORT}/mcp`); console.log(''); console.log('This server demonstrates SEP-1699 SSE polling:'); console.log('- retryInterval: 2000ms (client waits 2s before reconnecting)'); console.log('- eventStore: InMemoryEventStore (events are persisted for replay)'); console.log(''); console.log('Try calling the "long-task" tool to see server-initiated disconnect in action.'); }); //# sourceMappingURL=ssePollingExample.js.map