1313 * - `MCP request failed: <status> <statusText> - <body>` message contract for
1414 * HTTP transport failures, which `isStreamableHttpUnsupported`,
1515 * `isUrlOverrideSseFallbackCandidate` and the MCP-not-activated hint rely on.
16+ * Classic SSE stream GET failures carry the same contract: `eventsource`
17+ * drops non-200 GET bodies, so the interceptor captures status/body and
18+ * `mapSdkError` rebuilds the message from the SDK's `SseError`.
1619 * - `MCP error (<code>): <message>` for JSON-RPC business errors (passthrough).
1720 * - Raw `TypeError` (e.g. ENOTFOUND) propagation so runtime can surface errno.
1821 */
@@ -22,7 +25,7 @@ import {
2225 StreamableHTTPClientTransport ,
2326 StreamableHTTPError ,
2427} from "@modelcontextprotocol/sdk/client/streamableHttp.js" ;
25- import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js" ;
28+ import { SSEClientTransport , SseError } from "@modelcontextprotocol/sdk/client/sse.js" ;
2629import { ErrorCode , McpError } from "@modelcontextprotocol/sdk/types.js" ;
2730import type { FetchLike , Transport } from "@modelcontextprotocol/sdk/shared/transport.js" ;
2831
@@ -90,10 +93,16 @@ export interface SdkMcpAdapterOptions {
9093 * (message passed through verbatim per AGENTS.md §3).
9194 * - `StreamableHTTPError` (HTTP status in `.code`) → GENERAL with the
9295 * `MCP request failed: <status>` shape so the fallback detectors still match.
96+ * - `SseError` (classic SSE): when the fetch interceptor captured the failing
97+ * stream GET (`sseGetFailure` with a matching status), rebuild the
98+ * `MCP request failed: <status> <statusText> - <body>` contract — the SDK's
99+ * own message only carries `Non-200 status code (<status>)` and would lose
100+ * the server body the MCP-not-activated hint matches on. Without a captured
101+ * failure (e.g. a mid-stream drop) the error passes through unchanged.
93102 * - Anything else (e.g. a `TypeError` from `fetch`) is returned unchanged so the
94103 * runtime keeps its errno-specific handling.
95104 */
96- export function mapSdkError ( error : unknown ) : unknown {
105+ export function mapSdkError ( error : unknown , sseGetFailure ?: SseGetFailure ) : unknown {
97106 if ( error instanceof BailianError ) return error ;
98107
99108 if ( error instanceof McpError ) {
@@ -115,6 +124,15 @@ export function mapSdkError(error: unknown): unknown {
115124 } ) ;
116125 }
117126
127+ if ( error instanceof SseError && sseGetFailure && sseGetFailure . status === error . code ) {
128+ let message = `MCP request failed: ${ sseGetFailure . status } ${ sseGetFailure . statusText } ` ;
129+ if ( sseGetFailure . body ) message += ` - ${ sseGetFailure . body . slice ( 0 , 500 ) } ` ;
130+ return new BailianError ( message , ExitCode . GENERAL , undefined , {
131+ api : { httpStatus : sseGetFailure . status } ,
132+ cause : error ,
133+ } ) ;
134+ }
135+
118136 return error ;
119137}
120138
@@ -131,17 +149,35 @@ function rawMcpMessage(error: McpError): string {
131149 return error . message . startsWith ( prefix ) ? error . message . slice ( prefix . length ) : error . message ;
132150}
133151
152+ /**
153+ * Server-side failure of a classic SSE stream GET, captured by the fetch
154+ * interceptor before `eventsource` discards the body (it only reports
155+ * `Non-200 status code (<status>)` and never reads the payload).
156+ * `mapSdkError` rebuilds the HTTP-failure contract from it.
157+ */
158+ export interface SseGetFailure {
159+ status : number ;
160+ statusText : string ;
161+ body : string ;
162+ }
163+
134164/**
135165 * Build the transport-level fetch. It reproduces the old client's error contract
136166 * for message POSTs and mirrors its `--verbose` wire logging. GET responses are
137167 * passed through untouched so the SDK's own SSE-stream handling (including the
138- * tolerated 405 on a standalone Streamable GET) stays intact. Redirect responses
139- * are likewise passed through: the SDK wraps this fetch with its same-origin
140- * redirect policy (`fetchWithinOrigin`) and must see the 3xx to follow or
141- * reject it itself — intercepting it here would break same-origin redirects
142- * the old client's default `redirect: "follow"` used to honor.
168+ * tolerated 405 on a standalone Streamable GET) stays intact — except that a
169+ * failing classic SSE stream GET has its status/body captured first, since
170+ * `eventsource` would drop them. Redirect responses are likewise passed through:
171+ * the SDK wraps this fetch with its same-origin redirect policy
172+ * (`fetchWithinOrigin`) and must see the 3xx to follow or reject it itself —
173+ * intercepting it here would break same-origin redirects the old client's
174+ * default `redirect: "follow"` used to honor.
143175 */
144- function createMcpFetch ( deps : HttpDeps ) : FetchLike {
176+ function createMcpFetch (
177+ deps : HttpDeps ,
178+ kind : SdkMcpTransportKind ,
179+ onSseGetFailure ?: ( failure : SseGetFailure ) => void ,
180+ ) : FetchLike {
145181 return async ( url , init ) => {
146182 const method = init ?. method ?? "GET" ;
147183 const target = typeof url === "string" ? url : url . href ;
@@ -175,6 +211,28 @@ function createMcpFetch(deps: HttpDeps): FetchLike {
175211 throw new BailianError ( message , ExitCode . GENERAL ) ;
176212 }
177213
214+ if (
215+ kind === "sse" &&
216+ onSseGetFailure &&
217+ ! response . ok &&
218+ method === "GET" &&
219+ ! isRedirectStatus ( response . status )
220+ ) {
221+ // Classic SSE: this GET *is* the connection stream. `eventsource` fails
222+ // non-200 responses with `Non-200 status code (<status>)` without ever
223+ // reading the body, and the SDK rewraps that as `SseError` — losing the
224+ // server payload (e.g. MCP_IS_INVALID) the not-activated hint matches on.
225+ // Drain it here and hand it to the adapter; returning the drained
226+ // response is safe because neither transport reads a non-2xx GET body.
227+ let body = "" ;
228+ try {
229+ body = await response . text ( ) ;
230+ } catch {
231+ /* body already consumed or unreadable */
232+ }
233+ onSseGetFailure ( { status : response . status , statusText : response . statusText , body } ) ;
234+ }
235+
178236 return response ;
179237 } ;
180238}
@@ -198,6 +256,8 @@ export class SdkMcpAdapter implements McpConnectedClient {
198256 private client : Client | undefined ;
199257 private transport : Transport | undefined ;
200258 private connecting : Promise < void > | undefined ;
259+ /** Last failed classic SSE stream GET, for `SseError` contract reconstruction. */
260+ private sseGetFailure : SseGetFailure | undefined ;
201261
202262 constructor ( options : SdkMcpAdapterOptions ) {
203263 this . deps = options . deps ;
@@ -216,7 +276,7 @@ export class SdkMcpAdapter implements McpConnectedClient {
216276 this . connecting = this . connect ( ) . catch ( ( error ) => {
217277 // Allow a later initialize() to retry on the same instance.
218278 this . connecting = undefined ;
219- throw mapSdkError ( error ) ;
279+ throw this . mapTransportError ( error ) ;
220280 } ) ;
221281 }
222282 return this . connecting ;
@@ -234,7 +294,7 @@ export class SdkMcpAdapter implements McpConnectedClient {
234294 : { } ) ,
235295 } ) ) ;
236296 } catch ( error ) {
237- throw mapSdkError ( error ) ;
297+ throw this . mapTransportError ( error ) ;
238298 }
239299 }
240300
@@ -257,7 +317,7 @@ export class SdkMcpAdapter implements McpConnectedClient {
257317 // lives in the business adapter (`aigc-mcp.ts`), not here.
258318 return result ;
259319 } catch ( error ) {
260- throw mapSdkError ( error ) ;
320+ throw this . mapTransportError ( error ) ;
261321 }
262322 }
263323
@@ -270,21 +330,29 @@ export class SdkMcpAdapter implements McpConnectedClient {
270330 this . client = undefined ;
271331 this . transport = undefined ;
272332 this . connecting = undefined ;
333+ this . sseGetFailure = undefined ;
273334 if ( transport ) {
274335 void transport . close ( ) . catch ( ( ) => undefined ) ;
275336 }
276337 }
277338
278339 // ---- Internal ----
279340
341+ /** `mapSdkError` with this adapter's captured classic SSE GET failure. */
342+ private mapTransportError ( error : unknown ) : unknown {
343+ return mapSdkError ( error , this . sseGetFailure ) ;
344+ }
345+
280346 private async connect ( ) : Promise < void > {
281347 const headers : Record < string , string > = {
282348 "User-Agent" : `${ this . deps . identity . clientName } /${ this . deps . identity . version } ` ,
283349 ...trackingHeaders ( this . deps . identity , this . serviceSource ) ,
284350 Authorization : `Bearer ${ this . authToken } ` ,
285351 } ;
286352
287- const mcpFetch = createMcpFetch ( this . deps ) ;
353+ const mcpFetch = createMcpFetch ( this . deps , this . kind , ( failure ) => {
354+ this . sseGetFailure = failure ;
355+ } ) ;
288356 const endpoint = new URL ( this . url ) ;
289357 const transport =
290358 this . kind === "sse"
0 commit comments