@@ -24,18 +24,25 @@ type PendingResolver = {
2424 reject : ( reason : unknown ) => void ;
2525} ;
2626
27+ /** 用字符串键匹配 JSON-RPC id(兼容 number / string 回传)。 */
28+ function pendingKey ( id : number | string ) : string {
29+ return String ( id ) ;
30+ }
31+
2732export class McpSseClient {
2833 private sseUrl : string ;
2934 private messageUrl : string | undefined ;
3035 private nextId = 1 ;
3136 private deps : HttpDeps ;
3237 private authToken : string | undefined ;
3338 private abortController : AbortController | undefined ;
34- private pending = new Map < number , PendingResolver > ( ) ;
39+ private pending = new Map < string , PendingResolver > ( ) ;
3540 private endpointReady : Promise < void > ;
3641 private resolveEndpoint : ( ( ) => void ) | undefined ;
3742 private rejectEndpoint : ( ( reason : unknown ) => void ) | undefined ;
3843 private closed = false ;
44+ /** SSE GET 已结束(非主动 close)时置位,后续 RPC 立即失败。 */
45+ private streamEnded = false ;
3946
4047 constructor ( deps : HttpDeps , sseUrl : string , authToken ?: string ) {
4148 this . deps = deps ;
@@ -87,12 +94,23 @@ export class McpSseClient {
8794 if ( this . closed ) return ;
8895 this . closed = true ;
8996 this . abortController ?. abort ( ) ;
97+ this . failPending ( new BailianError ( "MCP SSE session closed." , ExitCode . GENERAL ) ) ;
98+ this . messageUrl = undefined ;
99+ }
100+
101+ private failPending ( reason : unknown ) : void {
90102 for ( const [ , waiter ] of this . pending ) {
91- waiter . reject ( new BailianError ( "MCP SSE session closed." , ExitCode . GENERAL ) ) ;
103+ waiter . reject ( reason ) ;
92104 }
93105 this . pending . clear ( ) ;
94106 }
95107
108+ private markStreamEnded ( reason : BailianError ) : void {
109+ this . streamEnded = true ;
110+ this . messageUrl = undefined ;
111+ this . failPending ( reason ) ;
112+ }
113+
96114 private async openSse ( ) : Promise < void > {
97115 if ( this . abortController ) return ;
98116
@@ -145,10 +163,10 @@ export class McpSseClient {
145163 ExitCode . GENERAL ,
146164 ) ;
147165 this . rejectEndpoint ?.( reason ) ;
148- for ( const [ , waiter ] of this . pending ) {
149- waiter . reject ( reason ) ;
166+ // consumeSse 在正常结束路径已 markStreamEnded;此处覆盖解析/读取异常。
167+ if ( ! this . streamEnded ) {
168+ this . markStreamEnded ( reason ) ;
150169 }
151- this . pending . clear ( ) ;
152170 } ) ;
153171
154172 const timeoutMs = this . deps . settings . timeout * 1000 ;
@@ -167,7 +185,8 @@ export class McpSseClient {
167185 for await ( const event of parseSSE ( response ) ) {
168186 if ( this . closed ) break ;
169187
170- if ( event . event === "endpoint" || ( ! event . event && ! this . messageUrl ) ) {
188+ // 规范要求首事件为 event: endpoint;不接受无名事件以免误把 JSON 当 URL。
189+ if ( event . event === "endpoint" ) {
171190 const raw = event . data . trim ( ) ;
172191 if ( ! raw ) continue ;
173192 // Only accept same-origin message URLs so we never forward the Bearer token cross-origin.
@@ -178,21 +197,25 @@ export class McpSseClient {
178197 continue ;
179198 }
180199
200+ // 缺省 event 类型在 SSE 中等同 message。
181201 if ( event . event === "message" || event . event === undefined ) {
182202 let payload : JsonRpcResponse ;
183203 try {
184204 payload = JSON . parse ( event . data ) as JsonRpcResponse ;
185205 } catch {
186206 continue ;
187207 }
188- if ( typeof payload . id !== "number" ) continue ;
189- const waiter = this . pending . get ( payload . id ) ;
208+ if ( typeof payload . id !== "number" && typeof payload . id !== "string" ) continue ;
209+ const key = pendingKey ( payload . id ) ;
210+ const waiter = this . pending . get ( key ) ;
190211 if ( ! waiter ) continue ;
191- this . pending . delete ( payload . id ) ;
212+ this . pending . delete ( key ) ;
192213 waiter . resolve ( payload ) ;
193214 }
194215 }
195216
217+ if ( this . closed ) return ;
218+
196219 if ( ! this . messageUrl ) {
197220 const error = new BailianError (
198221 "MCP SSE stream ended before endpoint event." ,
@@ -201,10 +224,19 @@ export class McpSseClient {
201224 this . rejectEndpoint ?.( error ) ;
202225 throw error ;
203226 }
227+
228+ // 已拿到 endpoint 后流仍结束:标记会话死亡并唤醒 pending;不再 throw,
229+ // 避免 void consumeSse().catch 之外再冒出未处理 rejection。
230+ this . markStreamEnded ( new BailianError ( "MCP SSE stream ended unexpectedly." , ExitCode . GENERAL ) ) ;
204231 }
205232
206233 private async rpc ( method : string , params ?: Record < string , unknown > ) : Promise < unknown > {
234+ if ( this . closed || this . streamEnded ) {
235+ throw new BailianError ( "MCP SSE stream ended unexpectedly." , ExitCode . GENERAL ) ;
236+ }
237+
207238 const id = this . nextId ++ ;
239+ const key = pendingKey ( id ) ;
208240 const body = {
209241 jsonrpc : "2.0" as const ,
210242 id,
@@ -214,15 +246,20 @@ export class McpSseClient {
214246
215247 const timeoutMs = this . deps . settings . timeout * 1000 ;
216248 const responsePromise = new Promise < JsonRpcResponse > ( ( resolve , reject ) => {
217- this . pending . set ( id , { resolve, reject } ) ;
249+ this . pending . set ( key , { resolve, reject } ) ;
218250 } ) ;
251+ // 流可能在 Promise.race 之前结束并 reject pending,先挂上 catch 避免 unhandledRejection。
252+ void responsePromise . catch ( ( ) => undefined ) ;
219253 const responseTimeout = cancellableTimeoutReject (
220254 timeoutMs ,
221255 `MCP SSE timed out waiting for response to ${ method } .` ,
222256 ) ;
223257
224258 try {
225259 await this . postMessage ( body ) ;
260+ if ( this . closed || this . streamEnded ) {
261+ throw new BailianError ( "MCP SSE stream ended unexpectedly." , ExitCode . GENERAL ) ;
262+ }
226263 const data = await Promise . race ( [ responsePromise , responseTimeout . promise ] ) ;
227264 if ( data . error ) {
228265 throw new BailianError (
@@ -232,7 +269,7 @@ export class McpSseClient {
232269 }
233270 return data . result ;
234271 } catch ( error ) {
235- this . pending . delete ( id ) ;
272+ this . pending . delete ( key ) ;
236273 throw error ;
237274 } finally {
238275 responseTimeout . cancel ( ) ;
@@ -249,6 +286,9 @@ export class McpSseClient {
249286 }
250287
251288 private async postMessage ( body : unknown ) : Promise < void > {
289+ if ( this . closed || this . streamEnded ) {
290+ throw new BailianError ( "MCP SSE stream ended unexpectedly." , ExitCode . GENERAL ) ;
291+ }
252292 if ( ! this . messageUrl ) {
253293 throw new BailianError ( "MCP SSE message endpoint is not ready." , ExitCode . GENERAL ) ;
254294 }
0 commit comments