Skip to content

Commit fbe40ef

Browse files
committed
mcp支持多会话
1 parent 23b922d commit fbe40ef

File tree

5 files changed

+445
-357
lines changed

5 files changed

+445
-357
lines changed

unity/mcp_proj/src/csharp-bridge-transport.mts

Lines changed: 26 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
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
*/
1417
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
1518
import 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(/[xy]/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
*/
5257
export 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

7382
export 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

Comments
 (0)