From ce20212e597a1d88e92205911ac900b77f83ed41 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 16 Aug 2026 15:18:48 +0000 Subject: [PATCH] feat(mcp): re-export UnauthorizedError so hosts can detect auth failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A host that wants to react to a mid-session authorization failure — the one case where the transport gives up and needs a human — had no way to recognise it. The SDK's UnauthorizedError never assigns `this.name`, so it reads as "Error"; matching on the name silently never fires, and matching on the message would also catch a tool whose own error text happens to mention "unauthorized". Exporting the class from the subpath gives an identity check that works against the same module instance the connector uses. Found by review of the React bindings, where exactly this check was dead code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011FKop4At26QqqkwVGEjJur --- package.json | 2 +- src/mcp/index.ts | 5 +++++ tests/mcp.test.ts | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 939bda6..a1394b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@dudko.dev/agent-web", - "version": "0.0.11", + "version": "0.0.12", "description": "Headless, configurable in-browser LLM agent on the Vercel AI SDK: multi-provider bring-your-own-key (OpenAI, Anthropic, Google, xAI, DeepSeek, gateway, openai-compatible) AND local WebGPU/WebLLM, native + prompted tool-calling, structured output, an encrypted IndexedDB token vault, and a plan \u2192 execute \u2192 replan \u2192 synthesize loop. UI-agnostic.", "type": "module", "sideEffects": false, diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 6140c15..957d815 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -1,4 +1,9 @@ export { connectMcpHttp, flattenContent } from './http.js' +// Re-exported so a host can identify an authorization failure by identity. +// The SDK's class does not set `name`, so `err.name === 'UnauthorizedError'` +// is always false — string sniffing is not an option, and a host that only +// depends on this package has no other handle on the class. +export { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' export type { McpHttpServerConfig, McpCatalogEntry, diff --git a/tests/mcp.test.ts b/tests/mcp.test.ts index 77d685e..404881e 100644 --- a/tests/mcp.test.ts +++ b/tests/mcp.test.ts @@ -9,6 +9,7 @@ import { flattenContent, MemoryOAuthStorage, readOAuthCallback, + UnauthorizedError, VaultOAuthStorage, } from '../dist/mcp.js' @@ -849,3 +850,20 @@ test('OAuth: a dead refresh token falls back to a fresh authorization', async () // the same doomed refresh. assert.equal(await provider.tokens(), undefined) }) + +test('the subpath re-exports UnauthorizedError so a host can identify auth failures', async () => { + const mock = createMockServer({ requireAuth: true }) + const provider = makeProvider() + const mcp = await connectMcpHttp({ + docs: { url: MCP_URL, authProvider: provider, fetch: mock.fetchFn }, + }) + assert.equal(mcp.results[0].needsAuthorization, true) + + // The SDK's class never assigns `this.name`, so it reads as "Error" — + // a host checking err.name would silently never match. Identity is the + // only reliable handle, and it has to be reachable from this package. + const err = new UnauthorizedError() + assert.equal(err.name, 'Error', 'guards the assumption this export exists for') + assert.ok(err instanceof UnauthorizedError) + assert.ok(err instanceof Error) +})