From ca7a7874f6762e0dc348a35dd00a9ba8e5677d11 Mon Sep 17 00:00:00 2001 From: LuckTerence Date: Thu, 27 Aug 2026 01:58:07 +0800 Subject: [PATCH 1/4] fix(client): surface underlying network error via Error.cause on probe failures (#2657) --- packages/client/src/client/probeClassifier.ts | 9 ++++++--- packages/client/test/client/probeClassifier.test.ts | 9 +++++++++ packages/core-internal/src/errors/sdkErrors.ts | 5 +++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/client/src/client/probeClassifier.ts b/packages/client/src/client/probeClassifier.ts index e23d011a2a..cbe3233051 100644 --- a/packages/client/src/client/probeClassifier.ts +++ b/packages/client/src/client/probeClassifier.ts @@ -311,9 +311,12 @@ function classifyNetworkError(error: unknown, context: ProbeClassifierContext): } return { kind: 'error', - error: new SdkError(SdkErrorCode.EraNegotiationFailed, `Version negotiation probe failed: ${describeError(error)}`, { - cause: error - }) + error: new SdkError( + SdkErrorCode.EraNegotiationFailed, + `Version negotiation probe failed: ${describeError(error)}`, + undefined, + { cause: error } + ) }; } diff --git a/packages/client/test/client/probeClassifier.test.ts b/packages/client/test/client/probeClassifier.test.ts index 3a65f240b7..420a446d0c 100644 --- a/packages/client/test/client/probeClassifier.test.ts +++ b/packages/client/test/client/probeClassifier.test.ts @@ -270,6 +270,15 @@ describe('row: network outage → typed connect error (Node)', () => { const verdict = classify({ kind: 'network-error', error: new TypeError('fetch failed') }, { environment: 'node' }); expect(verdict.kind).toBe('error'); }); + + test('the underlying network error is reachable via Error.cause (#2657)', () => { + const cause = Object.assign(new Error('fetch failed'), { code: 'ECONNREFUSED' }); + const verdict = classify({ kind: 'network-error', error: cause }); + expect(verdict.kind).toBe('error'); + if (verdict.kind === 'error') { + expect(verdict.error.cause).toBe(cause); + } + }); }); describe('row: timeout — transport-aware verdict', () => { diff --git a/packages/core-internal/src/errors/sdkErrors.ts b/packages/core-internal/src/errors/sdkErrors.ts index 0bc8f9a1ad..645068586f 100644 --- a/packages/core-internal/src/errors/sdkErrors.ts +++ b/packages/core-internal/src/errors/sdkErrors.ts @@ -147,9 +147,10 @@ export class SdkError extends Error { constructor( public readonly code: SdkErrorCode, message: string, - public readonly data?: unknown + public readonly data?: unknown, + options?: ErrorOptions ) { - super(message); + super(message, options); this.name = 'SdkError'; stampErrorBrands(this, new.target); } From 5211a075ccd34500c5a4bd01d22571114e90eeeb Mon Sep 17 00:00:00 2001 From: LuckTerence Date: Thu, 27 Aug 2026 14:19:33 +0800 Subject: [PATCH 2/4] chore: add changeset for SdkError cause fix --- .changeset/brave-donkeys-listen.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/brave-donkeys-listen.md diff --git a/.changeset/brave-donkeys-listen.md b/.changeset/brave-donkeys-listen.md new file mode 100644 index 0000000000..4583423da6 --- /dev/null +++ b/.changeset/brave-donkeys-listen.md @@ -0,0 +1,6 @@ +--- +'@modelcontextprotocol/core-internal': patch +'@modelcontextprotocol/client': patch +--- + +`SdkError` now accepts standard `ErrorOptions`, and version-negotiation probe failures surface the underlying network error via `Error.cause` (#2657). From 689f95a84e5d64a91448ccb65bfc8c686f6dead4 Mon Sep 17 00:00:00 2001 From: LuckTerence Date: Fri, 28 Aug 2026 00:41:18 +0800 Subject: [PATCH 3/4] fix(client): keep SdkError data.cause populated alongside Error.cause The probeAuthSeam control test reads the underlying network error via SdkError.data.cause; passing it only as ErrorOptions left data undefined and crashed the suite. Populate both slots so the legacy data.cause contract and the standard Error.cause chain (#2657) both hold. --- packages/client/src/client/probeClassifier.ts | 4 +++- packages/client/test/client/probeClassifier.test.ts | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/client/src/client/probeClassifier.ts b/packages/client/src/client/probeClassifier.ts index cbe3233051..f73d57b393 100644 --- a/packages/client/src/client/probeClassifier.ts +++ b/packages/client/src/client/probeClassifier.ts @@ -314,7 +314,9 @@ function classifyNetworkError(error: unknown, context: ProbeClassifierContext): error: new SdkError( SdkErrorCode.EraNegotiationFailed, `Version negotiation probe failed: ${describeError(error)}`, - undefined, + // Keep data.cause for existing consumers while also exposing the + // standard Error.cause chain (#2657). + { cause: error }, { cause: error } ) }; diff --git a/packages/client/test/client/probeClassifier.test.ts b/packages/client/test/client/probeClassifier.test.ts index 420a446d0c..5c80efb09b 100644 --- a/packages/client/test/client/probeClassifier.test.ts +++ b/packages/client/test/client/probeClassifier.test.ts @@ -277,6 +277,8 @@ describe('row: network outage → typed connect error (Node)', () => { expect(verdict.kind).toBe('error'); if (verdict.kind === 'error') { expect(verdict.error.cause).toBe(cause); + // The legacy data.cause slot stays populated too. + expect((verdict.error.data as { cause?: unknown }).cause).toBe(cause); } }); }); From 73f81d9534ad26d710a42e7abeef43aabb9d9a06 Mon Sep 17 00:00:00 2001 From: LuckTerence Date: Fri, 28 Aug 2026 00:53:43 +0800 Subject: [PATCH 4/4] test(client): narrow SdkError before reading data.cause in cause-chain test --- packages/client/test/client/probeClassifier.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/test/client/probeClassifier.test.ts b/packages/client/test/client/probeClassifier.test.ts index 5c80efb09b..d6e447407d 100644 --- a/packages/client/test/client/probeClassifier.test.ts +++ b/packages/client/test/client/probeClassifier.test.ts @@ -278,7 +278,7 @@ describe('row: network outage → typed connect error (Node)', () => { if (verdict.kind === 'error') { expect(verdict.error.cause).toBe(cause); // The legacy data.cause slot stays populated too. - expect((verdict.error.data as { cause?: unknown }).cause).toBe(cause); + expect(((verdict.error as SdkError).data as { cause?: unknown }).cause).toBe(cause); } }); });