88 * @experimental
99 */
1010
11- import { Transport } from '../../shared/transport.js' ;
11+ import { Transport } from '../shared/transport.js' ;
12+ import { EventStore } from '../server/stores.js' ;
13+ import { SessionStore } from './stores.js' ;
1214import {
1315 MessageExtraInfo ,
1416 RequestInfo ,
@@ -19,105 +21,8 @@ import {
1921 JSONRPCMessage ,
2022 JSONRPCMessageSchema ,
2123 RequestId ,
22- SUPPORTED_PROTOCOL_VERSIONS ,
23- DEFAULT_NEGOTIATED_PROTOCOL_VERSION
24- } from '../../types.js' ;
25-
26- export type StreamId = string ;
27- export type EventId = string ;
28-
29- /**
30- * Interface for resumability support via event storage
31- */
32- export interface EventStore {
33- /**
34- * Stores an event for later retrieval
35- * @param streamId ID of the stream the event belongs to
36- * @param message The JSON-RPC message to store
37- * @returns The generated event ID for the stored event
38- */
39- storeEvent ( streamId : StreamId , message : JSONRPCMessage ) : Promise < EventId > ;
40-
41- /**
42- * Get the stream ID associated with a given event ID.
43- * @param eventId The event ID to look up
44- * @returns The stream ID, or undefined if not found
45- *
46- * Optional: If not provided, the SDK will use the streamId returned by
47- * replayEventsAfter for stream mapping.
48- */
49- getStreamIdForEventId ?( eventId : EventId ) : Promise < StreamId | undefined > ;
50-
51- replayEventsAfter (
52- lastEventId : EventId ,
53- {
54- send
55- } : {
56- send : ( eventId : EventId , message : JSONRPCMessage ) => Promise < void > ;
57- }
58- ) : Promise < StreamId > ;
59- }
60-
61- /**
62- * Session state that can be persisted externally for serverless deployments.
63- */
64- export interface SessionState {
65- /** Whether the session has completed initialization */
66- initialized : boolean ;
67- /** The negotiated protocol version */
68- protocolVersion : string ;
69- /** Timestamp when the session was created */
70- createdAt : number ;
71- }
72-
73- /**
74- * Interface for session storage in distributed/serverless deployments.
75- *
76- * In serverless environments (Lambda, Vercel, Cloudflare Workers), each request
77- * may be handled by a different instance with no shared memory. The SessionStore
78- * allows session state to be persisted externally (e.g., Redis, DynamoDB, KV).
79- *
80- * @example
81- * ```typescript
82- * // Cloudflare KV implementation
83- * class KVSessionStore implements SessionStore {
84- * constructor(private kv: KVNamespace) {}
85- *
86- * async get(sessionId: string) {
87- * return this.kv.get(`session:${sessionId}`, 'json');
88- * }
89- * async save(sessionId: string, state: SessionState) {
90- * await this.kv.put(`session:${sessionId}`, JSON.stringify(state), { expirationTtl: 3600 });
91- * }
92- * async delete(sessionId: string) {
93- * await this.kv.delete(`session:${sessionId}`);
94- * }
95- * }
96- * ```
97- */
98- export interface SessionStore {
99- /**
100- * Retrieve session state by ID.
101- * @param sessionId The session ID to look up
102- * @returns The session state, or undefined if not found
103- */
104- get ( sessionId : string ) : Promise < SessionState | undefined > ;
105-
106- /**
107- * Save session state.
108- * Called when a session is initialized or updated.
109- * @param sessionId The session ID
110- * @param state The session state to persist
111- */
112- save ( sessionId : string , state : SessionState ) : Promise < void > ;
113-
114- /**
115- * Delete session state.
116- * Called when a session is explicitly closed via DELETE request.
117- * @param sessionId The session ID to delete
118- */
119- delete ( sessionId : string ) : Promise < void > ;
120- }
24+ SUPPORTED_PROTOCOL_VERSIONS
25+ } from '../types.js' ;
12126
12227/**
12328 * Internal stream mapping for managing SSE connections
@@ -211,19 +116,6 @@ export interface FetchStreamableHTTPServerTransportOptions {
211116 * to work across multiple serverless function invocations or instances.
212117 *
213118 * If not provided, session state is kept in-memory (single-instance mode).
214- *
215- * @example
216- * ```typescript
217- * // Redis session store
218- * const transport = new FetchStreamableHTTPServerTransport({
219- * sessionIdGenerator: () => crypto.randomUUID(),
220- * sessionStore: {
221- * get: async (id) => redis.get(`session:${id}`),
222- * save: async (id, state) => redis.set(`session:${id}`, state, 'EX', 3600),
223- * delete: async (id) => redis.del(`session:${id}`)
224- * }
225- * });
226- * ```
227119 */
228120 sessionStore ?: SessionStore ;
229121}
@@ -675,11 +567,8 @@ export class FetchStreamableHTTPServerTransport implements Transport {
675567
676568 // Persist session state to external store if configured
677569 if ( this . sessionId && this . _sessionStore ) {
678- const protocolVersion = req . headers . get ( 'mcp-protocol-version' ) ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION ;
679570 await this . _sessionStore . save ( this . sessionId , {
680- initialized : true ,
681- protocolVersion,
682- createdAt : Date . now ( )
571+ initialized : true
683572 } ) ;
684573 }
685574
@@ -886,14 +775,27 @@ export class FetchStreamableHTTPServerTransport implements Transport {
886775 return undefined ;
887776 }
888777
778+ /**
779+ * Validates the MCP-Protocol-Version header on incoming requests.
780+ *
781+ * This performs a simple check: if a version header is present, it must be
782+ * in the SUPPORTED_PROTOCOL_VERSIONS list. We do not track the negotiated
783+ * version or enforce version consistency across requests - the SDK handles
784+ * version negotiation during initialization, and we simply reject any
785+ * explicitly unsupported versions.
786+ *
787+ * - Header present and supported: Accept
788+ * - Header present and unsupported: 400 Bad Request
789+ * - Header missing: Accept (version validation is optional)
790+ */
889791 private validateProtocolVersion ( req : Request ) : Response | undefined {
890- const protocolVersion = req . headers . get ( 'mcp-protocol-version' ) ?? DEFAULT_NEGOTIATED_PROTOCOL_VERSION ;
792+ const protocolVersion = req . headers . get ( 'mcp-protocol-version' ) ;
891793
892- if ( ! SUPPORTED_PROTOCOL_VERSIONS . includes ( protocolVersion ) ) {
794+ if ( protocolVersion !== null && ! SUPPORTED_PROTOCOL_VERSIONS . includes ( protocolVersion ) ) {
893795 return this . createJsonErrorResponse (
894796 400 ,
895797 - 32000 ,
896- `Bad Request: Unsupported protocol version (supported versions: ${ SUPPORTED_PROTOCOL_VERSIONS . join ( ', ' ) } )`
798+ `Bad Request: Unsupported protocol version: ${ protocolVersion } (supported versions: ${ SUPPORTED_PROTOCOL_VERSIONS . join ( ', ' ) } )`
897799 ) ;
898800 }
899801 return undefined ;
0 commit comments