@@ -88,9 +88,14 @@ export interface SdkMcpAdapterOptions {
8888 * Translate an SDK/transport error into the CLI error contract.
8989 *
9090 * - `BailianError` (already ours, produced by the fetch interceptor) passes through.
91- * - `McpError`: RequestTimeout → TIMEOUT, ConnectionClosed → NETWORK, otherwise a
92- * server-side JSON-RPC error → GENERAL with the `MCP error (code): message` shape
93- * (message passed through verbatim per AGENTS.md §3).
91+ * - `McpError`: SDK-local failures only — the protocol timer (`Request timed
92+ * out` / `Maximum total timeout exceeded`, carrying its payload in `data`) →
93+ * TIMEOUT, and the transport-close rejection (`Connection closed`) → NETWORK.
94+ * A server-side JSON-RPC error reusing those codes (-32000 / -32001) carries
95+ * the server's own message, so it falls through to GENERAL with the
96+ * `MCP error (code): message` shape (message passed through verbatim per
97+ * AGENTS.md §3) — the exit code must not mislead callers into retrying a
98+ * business error as a network/timeout failure.
9499 * - `StreamableHTTPError` (HTTP status in `.code`) → GENERAL with the
95100 * `MCP request failed: <status>` shape so the fallback detectors still match.
96101 * - `SseError` (classic SSE): when the fetch interceptor captured the failing
@@ -107,10 +112,10 @@ export function mapSdkError(error: unknown, sseGetFailure?: SseGetFailure): unkn
107112
108113 if ( error instanceof McpError ) {
109114 const raw = rawMcpMessage ( error ) ;
110- if ( error . code === ErrorCode . RequestTimeout ) {
115+ if ( error . code === ErrorCode . RequestTimeout && isLocalTimeoutSignature ( error , raw ) ) {
111116 return new BailianError ( raw || "MCP request timed out." , ExitCode . TIMEOUT ) ;
112117 }
113- if ( error . code === ErrorCode . ConnectionClosed ) {
118+ if ( error . code === ErrorCode . ConnectionClosed && raw === LOCAL_CLOSE_MESSAGE ) {
114119 return new BailianError ( raw || "MCP connection closed." , ExitCode . NETWORK ) ;
115120 }
116121 return new BailianError ( `MCP error (${ error . code } ): ${ raw } ` , ExitCode . GENERAL ) ;
@@ -149,6 +154,24 @@ function rawMcpMessage(error: McpError): string {
149154 return error . message . startsWith ( prefix ) ? error . message . slice ( prefix . length ) : error . message ;
150155}
151156
157+ /** Fixed message of the SDK's transport-close rejection of pending requests. */
158+ const LOCAL_CLOSE_MESSAGE = "Connection closed" ;
159+
160+ /**
161+ * True when a `RequestTimeout` McpError came from the SDK's own request timer,
162+ * not from a server JSON-RPC error response reusing code -32001. The protocol
163+ * timer attaches its payload as `data` (`{ timeout }` / `{ maxTotalTimeout, … }`);
164+ * the fixed timer messages are matched as a fallback so the contract survives
165+ * SDK builds that drop the payload.
166+ */
167+ function isLocalTimeoutSignature ( error : McpError , raw : string ) : boolean {
168+ const data = error . data as Record < string , unknown > | undefined ;
169+ if ( data && ( typeof data . timeout === "number" || typeof data . maxTotalTimeout === "number" ) ) {
170+ return true ;
171+ }
172+ return raw === "Request timed out" || raw === "Maximum total timeout exceeded" ;
173+ }
174+
152175/**
153176 * Server-side failure of a classic SSE stream GET, captured by the fetch
154177 * interceptor before `eventsource` discards the body (it only reports
@@ -286,13 +309,11 @@ export class SdkMcpAdapter implements McpConnectedClient {
286309 const client = await this . requireClient ( ) ;
287310 try {
288311 const result = await client . listTools ( undefined , this . requestOptions ( ) ) ;
289- return result . tools . map ( ( tool ) => ( {
290- name : tool . name ,
291- ...( tool . description !== undefined ? { description : tool . description } : { } ) ,
292- ...( tool . inputSchema !== undefined
293- ? { inputSchema : tool . inputSchema as Record < string , unknown > }
294- : { } ) ,
295- } ) ) ;
312+ // Returned verbatim (the SDK-validated Tool objects): callers such as
313+ // `bl mcp tools --output json` keep every standard field the server
314+ // declares (outputSchema / annotations / title / `_meta`, …), matching
315+ // the old hand-rolled client's passthrough.
316+ return result . tools as McpTool [ ] ;
296317 } catch ( error ) {
297318 throw this . mapTransportError ( error ) ;
298319 }
0 commit comments