75 lines · 2.8 KB · TypeScript
RawDownload
import { beforeAll, describe, expect, it } from "@effect/vitest";import { InsufficientScopeError, SdkErrorCode, SdkHttpError } from "@modelcontextprotocol/client"; import { loadMcpClientSdk } from "./client-module"; // Classification consults the lazily-loaded client module (client-module.ts);// in prod every SDK error is preceded by a connect, which loads it. Mirror// that precondition here — these tests construct SDK errors directly.beforeAll(() => loadMcpClientSdk()); // oxlint-disable executor/no-error-constructor -- boundary: these tests reproduce the MCP SDK's own transport rejections, which are built-in Errorsimport { insufficientScopeFromCause } from "./http-status"; // The MCP SDK surfaces a 403 two ways, and both must classify://   - no authProvider: the transport throws with the response body embedded//     in the message ("Error POSTing to endpoint: <body>");//   - with an authProvider (the production OAuth path): the StreamableHTTP//     transport consumes the insufficient_scope challenge itself, retries//     with the broader scope, and only when THAT fails throws the fixed//     typed `InsufficientScopeError`, or after retry exhaustion the fixed//     `SdkHttpError` step-up message.describe("insufficientScopeFromCause", () => {  it("detects the OAuth error body embedded in a transport message", () => {    expect(      insufficientScopeFromCause(        new Error(          'Error POSTing to endpoint: {"error":"insufficient_scope","error_description":"needs files.read"}',        ),      ),    ).toBe(true);  });   it("detects a Google ErrorInfo body embedded in a transport message", () => {    expect(      insufficientScopeFromCause(        new Error(          'Error POSTing to endpoint: {"error":{"details":[{"reason":"ACCESS_TOKEN_SCOPE_INSUFFICIENT"}]}}',        ),      ),    ).toBe(true);  });   it("detects the SDK's typed insufficient-scope failure", () => {    expect(      insufficientScopeFromCause(new InsufficientScopeError({ requiredScope: "files.read" })),    ).toBe(true);  });   it("detects the SDK's exhausted step-up failure (the authProvider path)", () => {    expect(      insufficientScopeFromCause(        new SdkHttpError(          SdkErrorCode.ClientHttpForbidden,          "Server returned 403 insufficient_scope after step-up re-authorization (retry limit 2 reached)",          { status: 403 },        ),      ),    ).toBe(true);  });   it("ignores prose that merely mentions the tokens", () => {    expect(      insufficientScopeFromCause(        new Error(          "Error POSTing to endpoint: see the OAuth docs about insufficient_scope handling",        ),      ),    ).toBe(false);  });   it("ignores non-error causes", () => {    expect(insufficientScopeFromCause(undefined)).toBe(false);    expect(insufficientScopeFromCause("insufficient_scope")).toBe(false);  });});