|
| 1 | +/** |
| 2 | + * Custom StreamableHTTPServerTransport that supports session recreation. |
| 3 | + * |
| 4 | + * The MCP SDK's transport sets `_initialized` and `sessionId` only during |
| 5 | + * the initialize request handshake. When recreating a transport from Redis |
| 6 | + * (e.g., in serverless environments), we need to set these values directly. |
| 7 | + * |
| 8 | + * This class extends the MCP SDK's transport to expose a public API for |
| 9 | + * session recreation, avoiding the need to access private properties. |
| 10 | + */ |
| 11 | +import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; |
| 12 | +import type { ServerResponse } from 'http'; |
| 13 | +import type { JSONRPCMessage, RequestId } from '@modelcontextprotocol/sdk/types.js'; |
| 14 | +import type { AuthenticatedServerRequest } from '../../server/server.types'; |
| 15 | + |
| 16 | +export interface StreamableHTTPServerTransportOptions { |
| 17 | + /** |
| 18 | + * A function that generates a session ID for the transport. |
| 19 | + * If provided, sessions are stateful and require the mcp-session-id header. |
| 20 | + * If undefined, the transport operates in stateless mode. |
| 21 | + */ |
| 22 | + sessionIdGenerator?: () => string; |
| 23 | + |
| 24 | + /** |
| 25 | + * If true, responses are sent as JSON instead of SSE. |
| 26 | + * Default: false (SSE streaming mode) |
| 27 | + */ |
| 28 | + enableJsonResponse?: boolean; |
| 29 | + |
| 30 | + /** |
| 31 | + * Event store for resumability support. |
| 32 | + * Uses any to avoid complex type extraction from MCP SDK's optional options type. |
| 33 | + */ |
| 34 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 35 | + eventStore?: any; |
| 36 | + |
| 37 | + /** |
| 38 | + * Callback when a session is initialized. |
| 39 | + */ |
| 40 | + onsessioninitialized?: (sessionId: string) => void | Promise<void>; |
| 41 | + |
| 42 | + /** |
| 43 | + * Callback when a session is closed. |
| 44 | + */ |
| 45 | + onsessionclosed?: (sessionId?: string) => void | Promise<void>; |
| 46 | +} |
| 47 | + |
| 48 | +/** |
| 49 | + * StreamableHTTPServerTransport with session recreation support. |
| 50 | + * |
| 51 | + * This is a drop-in replacement for the MCP SDK's StreamableHTTPServerTransport |
| 52 | + * that adds the ability to recreate a session without replaying the initialization |
| 53 | + * handshake. This is essential for serverless environments where the transport |
| 54 | + * may be evicted from memory and needs to be recreated from a stored session. |
| 55 | + * |
| 56 | + * It extends StreamableHTTPServerTransport to maintain full compatibility while |
| 57 | + * adding public methods to set initialization state. |
| 58 | + */ |
| 59 | +export class RecreateableStreamableHTTPServerTransport extends StreamableHTTPServerTransport { |
| 60 | + private _preInitializedSessionId: string | undefined; |
| 61 | + |
| 62 | + constructor(options: StreamableHTTPServerTransportOptions = {}) { |
| 63 | + super(options); |
| 64 | + } |
| 65 | + |
| 66 | + /** |
| 67 | + * Returns whether the transport has been initialized. |
| 68 | + */ |
| 69 | + get isInitialized(): boolean { |
| 70 | + // Access the internal WebStandardTransport's _initialized flag |
| 71 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 72 | + return (this as any)._webStandardTransport?._initialized ?? false; |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Sets the transport to an initialized state with the given session ID. |
| 77 | + * Use this when recreating a transport from a stored session. |
| 78 | + * |
| 79 | + * This method allows you to "restore" a session without replaying the |
| 80 | + * initialization handshake. After calling this method, the transport |
| 81 | + * will accept requests with the given session ID. |
| 82 | + * |
| 83 | + * @param sessionId - The session ID that was previously assigned to this session |
| 84 | + */ |
| 85 | + setInitializationState(sessionId: string): void { |
| 86 | + this._preInitializedSessionId = sessionId; |
| 87 | + |
| 88 | + // Access the internal WebStandardTransport and set both flags |
| 89 | + // eslint-disable-next-line @typescript-eslint/no-explicit-any |
| 90 | + const webTransport = (this as any)._webStandardTransport; |
| 91 | + if (webTransport) { |
| 92 | + webTransport._initialized = true; |
| 93 | + webTransport.sessionId = sessionId; |
| 94 | + } |
| 95 | + } |
| 96 | + |
| 97 | + /** |
| 98 | + * Override handleRequest to handle session recreation scenario. |
| 99 | + * When we've set initialization state manually, we need to ensure |
| 100 | + * the transport operates correctly. |
| 101 | + */ |
| 102 | + override async handleRequest( |
| 103 | + req: AuthenticatedServerRequest, |
| 104 | + res: ServerResponse, |
| 105 | + parsedBody?: unknown, |
| 106 | + ): Promise<void> { |
| 107 | + return super.handleRequest(req, res, parsedBody); |
| 108 | + } |
| 109 | + |
| 110 | + /** |
| 111 | + * Sends a JSON-RPC message through the transport. |
| 112 | + */ |
| 113 | + override async send(message: JSONRPCMessage, options?: { relatedRequestId?: RequestId }): Promise<void> { |
| 114 | + return super.send(message, options); |
| 115 | + } |
| 116 | +} |
0 commit comments