1010 *
1111 * The JS side handles:
1212 * - JSON-RPC message parsing/validation, session logic, MCP protocol
13+ *
14+ * Multi-session: Each CSharpBridgeTransport instance represents one session.
15+ * The SessionManager in main.mts creates/destroys instances as clients connect/disconnect.
1316 */
1417import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' ;
1518import type { JSONRPCMessage , RequestId } from '@modelcontextprotocol/sdk/types.js' ;
@@ -23,7 +26,7 @@ function isJSONRPCRequest(msg: JSONRPCMessage): msg is JSONRPCMessage & { method
2326 return 'method' in msg && 'id' in msg ;
2427}
2528
26- function isInitializeRequest ( msg : JSONRPCMessage ) : boolean {
29+ export function isInitializeRequest ( msg : JSONRPCMessage ) : boolean {
2730 return isJSONRPCRequest ( msg ) && ( msg as any ) . method === 'initialize' ;
2831}
2932
@@ -32,7 +35,7 @@ function isJSONRPCResponse(msg: JSONRPCMessage): msg is JSONRPCMessage & { id: R
3235}
3336
3437/** Simple UUID v4 generator (no dependency on node:crypto). */
35- function generateUUID ( ) : string {
38+ export function generateUUID ( ) : string {
3639 // Use Math.random-based UUID (sufficient for session IDs)
3740 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( / [ x y ] / g, ( c ) => {
3841 const r = ( Math . random ( ) * 16 ) | 0 ;
@@ -48,22 +51,28 @@ function generateUUID(): string {
4851/**
4952 * Interface for the C# McpHttpServer methods exposed to JS.
5053 * These are called from JS to send HTTP responses back through C#.
54+ *
55+ * Multi-session aware: AddSession/RemoveSession manage the set of valid sessions.
5156 */
5257export interface CSharpHttpBridge {
53- /** Set the MCP session ID on the C# server. */
54- SetSession ( sessionId : string ) : void ;
58+ /** Register a new session ID on the C# server. */
59+ AddSession ( sessionId : string ) : void ;
60+ /** Unregister a session ID from the C# server. */
61+ RemoveSession ( sessionId : string ) : void ;
5562 /** Begin SSE streaming for a POST request. */
5663 BeginSseStream ( requestContextId : string ) : void ;
5764 /** Write an SSE event to a POST response stream. */
5865 SendSseEvent ( requestContextId : string , jsonData : string ) : void ;
5966 /** Close a POST SSE stream. */
6067 ClosePostStream ( requestContextId : string ) : void ;
61- /** Write an SSE event to the standalone GET stream. */
62- SendGetSseEvent ( jsonData : string ) : void ;
68+ /** Write an SSE event to the standalone GET stream for a specific session . */
69+ SendGetSseEventForSession ( sessionId : string , jsonData : string ) : void ;
6370 /** Send a plain JSON response and close the connection. */
6471 SendJsonResponse ( requestContextId : string , statusCode : number , jsonBody : string ) : void ;
6572 /** Send a 202 Accepted response. */
6673 Send202 ( requestContextId : string ) : void ;
74+ /** Add the session header to a response for a specific request context. */
75+ AddSessionHeaderForContext ( requestContextId : string , sessionId : string ) : void ;
6776}
6877
6978// ---------------------------------------------------------------------------
@@ -72,13 +81,12 @@ export interface CSharpHttpBridge {
7281
7382export class CSharpBridgeTransport implements Transport {
7483 // --- Transport interface fields ---
75- sessionId ? : string ;
84+ sessionId : string ;
7685 onclose ?: ( ) => void ;
7786 onerror ?: ( error : Error ) => void ;
7887 onmessage ?: ( message : JSONRPCMessage , extra ?: any ) => void ;
7988
8089 private _bridge : CSharpHttpBridge ;
81- private _initialized = false ;
8290 private _started = false ;
8391
8492 /** requestId → requestContextId so we know where to write the response. */
@@ -87,8 +95,9 @@ export class CSharpBridgeTransport implements Transport {
8795 /** requestId → response message (buffered until all responses for a context are ready). */
8896 private _responseBuffer = new Map < string , JSONRPCMessage > ( ) ;
8997
90- constructor ( bridge : CSharpHttpBridge ) {
98+ constructor ( bridge : CSharpHttpBridge , sessionId : string ) {
9199 this . _bridge = bridge ;
100+ this . sessionId = sessionId ;
92101 }
93102
94103 // --- Transport interface ---
@@ -112,9 +121,9 @@ export class CSharpBridgeTransport implements Transport {
112121 requestId = ( message as any ) . id ;
113122 }
114123
115- // Server-initiated message (no related request) → standalone GET stream
124+ // Server-initiated message (no related request) → standalone GET stream for this session
116125 if ( requestId === undefined ) {
117- this . _bridge . SendGetSseEvent ( JSON . stringify ( message ) ) ;
126+ this . _bridge . SendGetSseEventForSession ( this . sessionId , JSON . stringify ( message ) ) ;
118127 return ;
119128 }
120129
@@ -152,78 +161,18 @@ export class CSharpBridgeTransport implements Transport {
152161 }
153162
154163 // -------------------------------------------------------------------
155- // Called from C# via McpHttpServer.OnHttpPost callback
164+ // Called from the SessionManager when routing a POST to this session
156165 // -------------------------------------------------------------------
157166
158167 /**
159- * Handle an incoming HTTP POST request from C#.
160- * This is the main entry point for processing MCP messages.
168+ * Handle an incoming HTTP POST request routed to this session.
161169 *
162170 * @param requestContextId - Unique ID for this HTTP request (from C#)
163171 * @param body - Raw JSON body string
164- * @param sessionIdHeader - The mcp-session-id header value (empty string if absent)
172+ * @param messages - Already-parsed and validated JSON-RPC messages
165173 */
166- handlePost ( requestContextId : string , body : string , sessionIdHeader : string ) : void {
174+ handlePost ( requestContextId : string , messages : JSONRPCMessage [ ] ) : void {
167175 try {
168- // Parse body
169- let rawMessage : unknown ;
170- try {
171- rawMessage = JSON . parse ( body ) ;
172- } catch {
173- this . _bridge . SendJsonResponse ( requestContextId , 400 ,
174- JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32700 , message : 'Parse error: Invalid JSON' } , id : null } ) ) ;
175- return ;
176- }
177-
178- // Validate JSON-RPC
179- let messages : JSONRPCMessage [ ] ;
180- try {
181- if ( Array . isArray ( rawMessage ) ) {
182- messages = rawMessage . map ( m => JSONRPCMessageSchema . parse ( m ) ) ;
183- } else {
184- messages = [ JSONRPCMessageSchema . parse ( rawMessage ) ] ;
185- }
186- } catch {
187- this . _bridge . SendJsonResponse ( requestContextId , 400 ,
188- JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32700 , message : 'Parse error: Invalid JSON-RPC message' } , id : null } ) ) ;
189- return ;
190- }
191-
192- const isInit = messages . some ( isInitializeRequest ) ;
193-
194- // --- Initialization ---
195- if ( isInit ) {
196- // If already initialized, reset state for the new session.
197- // This handles the case where a client reconnects without sending DELETE first.
198- if ( this . _initialized ) {
199- this . _requestToContext . clear ( ) ;
200- this . _responseBuffer . clear ( ) ;
201- }
202- this . sessionId = generateUUID ( ) ;
203- this . _initialized = true ;
204- // Tell C# about the session ID so it can add headers and validate future requests
205- this . _bridge . SetSession ( this . sessionId ) ;
206- }
207-
208- // --- Session validation (non-init) ---
209- if ( ! isInit ) {
210- if ( ! this . _initialized ) {
211- this . _bridge . SendJsonResponse ( requestContextId , 400 ,
212- JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32000 , message : 'Server not initialized' } , id : null } ) ) ;
213- return ;
214- }
215- if ( ! sessionIdHeader ) {
216- this . _bridge . SendJsonResponse ( requestContextId , 400 ,
217- JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32000 , message : 'Mcp-Session-Id header is required' } , id : null } ) ) ;
218- return ;
219- }
220- if ( sessionIdHeader !== this . sessionId ) {
221- this . _bridge . SendJsonResponse ( requestContextId , 404 ,
222- JSON . stringify ( { jsonrpc : '2.0' , error : { code : - 32001 , message : 'Session not found' } , id : null } ) ) ;
223- return ;
224- }
225- }
226-
227176 // --- If no requests (only notifications / responses) → 202 ---
228177 const hasRequests = messages . some ( isJSONRPCRequest ) ;
229178 if ( ! hasRequests ) {
@@ -250,7 +199,7 @@ export class CSharpBridgeTransport implements Transport {
250199 }
251200 } catch ( err : any ) {
252201 const errMsg = err instanceof Error ? `${ err . message } \n${ err . stack } ` : String ( err ) ;
253- console . error ( `[CSharpBridgeTransport] handlePost error: ${ errMsg } ` ) ;
202+ console . error ( `[CSharpBridgeTransport] handlePost error (session= ${ this . sessionId } ) : ${ errMsg } ` ) ;
254203 this . onerror ?.( err instanceof Error ? err : new Error ( String ( err ) ) ) ;
255204 try {
256205 this . _bridge . SendJsonResponse ( requestContextId , 500 ,
@@ -260,11 +209,9 @@ export class CSharpBridgeTransport implements Transport {
260209 }
261210
262211 /**
263- * Handle a DELETE request — reset session state.
212+ * Handle a DELETE request — clean up this session's state.
264213 */
265214 handleDelete ( ) : void {
266- this . _initialized = false ;
267- this . sessionId = undefined ;
268215 this . _requestToContext . clear ( ) ;
269216 this . _responseBuffer . clear ( ) ;
270217 }
0 commit comments