Skip to content

Commit 3a3f8a7

Browse files
feat: Add RecreateableStreamableHTTPServerTransport for session recreation support
1 parent a718e27 commit 3a3f8a7

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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

Comments
 (0)