@dudko.dev/agent-web 0.0.12 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -232,13 +232,43 @@ navigates on its own — the SDK can demand authorization from inside a running
232
232
  tool call, and evicting the user then would discard the run. The URL is recorded
233
233
  on `provider.authorizationUrl` and the app decides when to use it.
234
234
 
235
- Two things to check on the **server** side, both invisible until they bite:
235
+ ### CORS: what the server must send
236
236
 
237
- - CORS must allow `Authorization` and expose `WWW-Authenticate` and
238
- `mcp-session-id` (`Access-Control-Expose-Headers`). Without the first, the
239
- browser cannot read the 401 challenge and discovery never starts.
240
- - The authorization server's `/register`, metadata and token endpoints must be
241
- CORS-enabled too — a browser client calls them directly.
237
+ A browser tells JavaScript nothing about why it blocked a request — a CORS
238
+ rejection and a server that is down both arrive as `TypeError: Failed to fetch`,
239
+ with the real reason printed only to the devtools console. So these are worth
240
+ checking first rather than last:
241
+
242
+ ```
243
+ Access-Control-Allow-Origin: <your origin>
244
+ Access-Control-Allow-Methods: GET, POST, DELETE, OPTIONS
245
+ Access-Control-Allow-Headers: Content-Type, Authorization, mcp-session-id,
246
+ MCP-Protocol-Version, Last-Event-ID
247
+ Access-Control-Expose-Headers: mcp-session-id, WWW-Authenticate
248
+ ```
249
+
250
+ Each line earns its place:
251
+
252
+ - **`MCP-Protocol-Version` in Allow-Headers** is the one that bites hardest. The
253
+ spec has required that header on every request *after* `initialize` since
254
+ 2025-06-18, and CORS lists written before that date omit it. The first request
255
+ does not carry it, so discovery, the whole OAuth dance and `initialize` all
256
+ succeed — and then everything afterwards is blocked. It reads exactly like the
257
+ consent step failing, and sends you to debug the client.
258
+ - **`WWW-Authenticate` in Expose-Headers**, or the browser hides the 401
259
+ challenge from JS and OAuth discovery never starts.
260
+ - **`Last-Event-ID`** for resuming a dropped SSE stream.
261
+ - The authorization server's `/register`, metadata and token endpoints need CORS
262
+ too, `OPTIONS` included — a browser client calls them directly.
263
+ - If the server uses the MCP SDK's DNS-rebinding protection, its `allowedOrigins`
264
+ must contain your origin. Left empty, it answers **403 `Origin not allowed`**
265
+ to any request that carries an `Origin` header — which is every request a
266
+ browser makes.
267
+
268
+ When a connection does fail this way, `connectMcpHttp` no longer just repeats the
269
+ browser's silence: it re-probes the endpoint with a plain request and, if that
270
+ gets through, says which header is being refused. `diagnoseMcpCors(url)` is
271
+ exported so a UI can show the same sentence.
242
272
 
243
273
  ## Configuration (highlights)
244
274
 
package/dist/mcp.cjs CHANGED
@@ -26,6 +26,7 @@ __export(mcp_exports, {
26
26
  VaultOAuthStorage: () => VaultOAuthStorage,
27
27
  beginMcpOAuth: () => beginMcpOAuth,
28
28
  connectMcpHttp: () => connectMcpHttp,
29
+ diagnoseMcpCors: () => diagnoseMcpCors,
29
30
  finishMcpOAuth: () => finishMcpOAuth,
30
31
  flattenContent: () => flattenContent,
31
32
  readOAuthCallback: () => readOAuthCallback
@@ -452,6 +453,28 @@ var refreshIfExpired = async (provider, serverUrl, fetchFn, log, name) => {
452
453
  );
453
454
  }
454
455
  };
456
+ var isNetworkLevelFailure = (err) => err instanceof TypeError || /failed to fetch|networkerror|load failed/i.test(String(err));
457
+ var diagnoseMcpCors = async (url, fetchFn = globalThis.fetch) => {
458
+ let plain;
459
+ try {
460
+ plain = await fetchFn(url, {
461
+ method: "POST",
462
+ headers: { "content-type": "application/json" },
463
+ body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "ping" })
464
+ });
465
+ } catch {
466
+ return `the browser blocked every request to ${url}, including a plain one. The server is unreachable, or it does not answer the CORS preflight for this origin: it must return Access-Control-Allow-Origin (and handle OPTIONS) on the MCP endpoint, the OAuth endpoints and the /.well-known documents.`;
467
+ }
468
+ const hints = [
469
+ `a plain request to ${url} succeeded (HTTP ${plain.status}) but the transport's did not, so the origin is allowed and a request HEADER is being refused. Add MCP-Protocol-Version (required on every request after initialize) and Last-Event-ID to the server's Access-Control-Allow-Headers, alongside Content-Type, Authorization and mcp-session-id.`
470
+ ];
471
+ if (plain.status === 401 && !plain.headers.get("www-authenticate")) {
472
+ hints.push(
473
+ "The server also answers 401 without exposing WWW-Authenticate: add it to Access-Control-Expose-Headers, or a browser client cannot read the challenge and OAuth discovery never starts."
474
+ );
475
+ }
476
+ return hints.join(" ");
477
+ };
455
478
  var openConnection = async (name, cfg, opts, log) => {
456
479
  let client;
457
480
  try {
@@ -492,7 +515,11 @@ var openConnection = async (name, cfg, opts, log) => {
492
515
  if (client) await client.close().catch(() => {
493
516
  });
494
517
  const needsAuthorization = err instanceof import_auth2.UnauthorizedError;
495
- const message = err instanceof Error ? err.message : String(err);
518
+ let message = err instanceof Error ? err.message : String(err);
519
+ if (!needsAuthorization && isNetworkLevelFailure(err)) {
520
+ const hint = await diagnoseMcpCors(cfg.url, cfg.fetch).catch(() => void 0);
521
+ if (hint) message = `${message} - ${hint}`;
522
+ }
496
523
  log(
497
524
  "error",
498
525
  needsAuthorization ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl` : `[mcp] ${name}: failed to connect - ${message}`
@@ -602,6 +629,7 @@ var import_auth3 = require("@modelcontextprotocol/sdk/client/auth.js");
602
629
  VaultOAuthStorage,
603
630
  beginMcpOAuth,
604
631
  connectMcpHttp,
632
+ diagnoseMcpCors,
605
633
  finishMcpOAuth,
606
634
  flattenContent,
607
635
  readOAuthCallback
package/dist/mcp.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp/index.ts","../src/mcp/http.ts","../src/mcp/oauth.ts","../src/storage/db.ts","../src/secrets/crypto.ts","../src/secrets/vault.ts"],"sourcesContent":["export { connectMcpHttp, flattenContent } from './http.js'\n// Re-exported so a host can identify an authorization failure by identity.\n// The SDK's class does not set `name`, so `err.name === 'UnauthorizedError'`\n// is always false — string sniffing is not an option, and a host that only\n// depends on this package has no other handle on the class.\nexport { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'\nexport type {\n McpHttpServerConfig,\n McpCatalogEntry,\n McpServerResult,\n ConnectedMcp,\n ConnectMcpOptions,\n} from './http.js'\nexport {\n BrowserOAuthProvider,\n MemoryOAuthStorage,\n VaultOAuthStorage,\n beginMcpOAuth,\n finishMcpOAuth,\n readOAuthCallback,\n} from './oauth.js'\nexport type {\n BrowserOAuthProviderOptions,\n McpOAuthCallback,\n McpOAuthRequestOptions,\n McpOAuthStorage,\n} from './oauth.js'\n","import {\n auth,\n UnauthorizedError,\n type OAuthClientProvider,\n} from '@modelcontextprotocol/sdk/client/auth.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPReconnectionOptions,\n} from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\nimport { assertSecureOAuthUrl } from './oauth.js'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n *\n * Three ways to authenticate, in ascending order of capability:\n * 1. `headers` — a static token you already hold.\n * 2. `getHeaders` — resolved once per connect, for tokens you mint yourself.\n * 3. `authProvider` — full OAuth 2.1: the SDK discovers the authorization\n * server, dynamically registers the client (DCR), runs PKCE, and — the\n * part 1 and 2 cannot do — transparently REFRESHES the access token when\n * the server answers 401 mid-run. See `./oauth`.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n /**\n * OAuth client provider (see `./oauth` for a browser implementation). The\n * transport sends its access token, refreshes on 401 and retries the request.\n * Do NOT also set an `Authorization` entry in `headers`: request headers are\n * merged last and would shadow the OAuth token.\n */\n authProvider?: OAuthClientProvider\n /** Custom fetch for every request: proxying, instrumentation, retries. */\n fetch?: FetchLike\n /** Overrides for the SSE reconnect backoff. Merged over the defaults below. */\n reconnection?: Partial<StreamableHTTPReconnectionOptions>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface McpServerResult {\n name: string\n connected: boolean\n error?: string\n /**\n * The server demands OAuth and we have no usable token. When the config\n * carried a `BrowserOAuthProvider`, its `authorizationUrl` now holds the page\n * to send the user to; reconnect once they are back.\n */\n needsAuthorization?: boolean\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: McpServerResult[]\n /**\n * Re-list one server's tools and rewrite its entries in `tools`/`catalog`.\n * Both are mutated in place, so anything holding a reference (including a\n * live agent config) sees the new set. Throws if the server isn't connected.\n */\n refreshServer: (name: string) => Promise<void>\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n /**\n * Fires when a server pushes `notifications/tools/list_changed`. Call\n * `refreshServer(name)` from a point where no run is reading `tools` — this\n * connector deliberately does not refresh behind a running agent's back.\n */\n onToolsChanged?: (server: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// Browsers drop long-lived SSE streams on tab-throttling and flaky networks far\n// more often than a server does; the SDK's default (2 retries) gives up too\n// early for an agent that may idle between steps.\nconst DEFAULT_RECONNECTION: StreamableHTTPReconnectionOptions = {\n maxReconnectionDelay: 30_000,\n initialReconnectionDelay: 1_000,\n reconnectionDelayGrowFactor: 1.5,\n maxRetries: 5,\n}\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Sanitizing can map two distinct MCP names onto one key (\"a.b\" and \"a-b\" both\n * become \"a_b\"). Suffix instead of dropping: a tool the model can't see is a\n * silent capability loss, and the disambiguated name still resolves back to the\n * original on dispatch.\n */\nconst uniqueKey = (base: string, taken: ToolSet): string => {\n // hasOwn, not truthiness: a tool named so that the key lands on \"__proto__\"\n // would otherwise read as already taken through the prototype chain.\n if (!Object.hasOwn(taken, base)) return base\n for (let n = 2; n < 100; n++) {\n const suffix = `_${n}`\n const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix\n if (!Object.hasOwn(taken, candidate)) return candidate\n }\n return ''\n}\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface McpToolDescriptor {\n name: string\n description?: string\n inputSchema: unknown\n}\n\ninterface ServerConnect {\n name: string\n client?: Client\n listed?: McpToolDescriptor[]\n error?: string\n needsAuthorization?: boolean\n}\n\n/** Thrown by the refresh-only view when the SDK reaches for a redirect. */\nclass RefreshOnlyAbort extends Error {}\n\n/**\n * A view of the provider that can renew a token but can never start an\n * interactive authorization.\n *\n * `auth()` does not have a refresh-only mode: when a refresh fails for a reason\n * it deems recoverable (a 5xx from the token endpoint, a network blip) it falls\n * through to a brand-new authorization — issuing a state and a PKCE verifier,\n * overwriting the stored pair, and calling `redirectToAuthorization`. The\n * transport then does its own `auth()` on the first 401, overwriting them\n * again, so the user comes back holding a code the second pair can't verify.\n * Blocking the three redirect-path methods aborts the escalation before\n * anything is overwritten; the transport's 401 handling stays the single owner\n * of interactive authorization.\n */\nconst refreshOnly = (p: OAuthClientProvider): OAuthClientProvider => ({\n // Must stay truthy: `auth()` reads a missing redirectUrl as \"non-interactive\n // grant\" and would try to fetch a token without one.\n get redirectUrl() {\n return p.redirectUrl\n },\n get clientMetadata() {\n return p.clientMetadata\n },\n clientInformation: () => p.clientInformation(),\n saveClientInformation: p.saveClientInformation?.bind(p),\n tokens: () => p.tokens(),\n saveTokens: (t) => p.saveTokens(t),\n invalidateCredentials: p.invalidateCredentials?.bind(p),\n addClientAuthentication: p.addClientAuthentication?.bind(p),\n validateResourceURL: p.validateResourceURL?.bind(p),\n codeVerifier: () => p.codeVerifier(),\n state: () => {\n throw new RefreshOnlyAbort()\n },\n saveCodeVerifier: () => {\n throw new RefreshOnlyAbort()\n },\n redirectToAuthorization: () => {\n throw new RefreshOnlyAbort()\n },\n})\n\n/**\n * An access token that is already expired produces a guaranteed 401 on the\n * first request. When the provider tells us so AND we hold a refresh token,\n * renew before connecting: one round-trip saved, and a run that starts on a\n * stale token doesn't surface a spurious auth error to the model.\n *\n * Best-effort by design — anything that goes wrong here is left to the\n * transport's own 401 handling, which is the path that knows how to ask the\n * user for a new authorization.\n */\nconst refreshIfExpired = async (\n provider: OAuthClientProvider,\n serverUrl: string,\n fetchFn: FetchLike | undefined,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n name: string,\n): Promise<void> => {\n const check = (provider as { isAccessTokenExpired?: () => boolean | Promise<boolean> })\n .isAccessTokenExpired\n if (typeof check !== 'function') return\n if (!(await check.call(provider))) return\n const tokens = await provider.tokens()\n // Nothing to refresh with: let the 401 path ask for a new authorization.\n if (!tokens?.refresh_token) return\n try {\n await auth(refreshOnly(provider), { serverUrl, fetchFn })\n } catch (err) {\n log(\n 'info',\n err instanceof RefreshOnlyAbort\n ? `[mcp] ${name}: the stored token could not be refreshed; a new authorization is needed`\n : `[mcp] ${name}: proactive token refresh failed (${\n err instanceof Error ? err.message : String(err)\n }); continuing`,\n )\n }\n}\n\nconst openConnection = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerConnect> => {\n let client: Client | undefined\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n if (cfg.authProvider) {\n if (headers && 'Authorization' in headers) {\n log(\n 'warn',\n `[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`,\n )\n }\n assertSecureOAuthUrl(cfg.url, name)\n await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name)\n }\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n authProvider: cfg.authProvider,\n fetch: cfg.fetch,\n reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection },\n })\n // Without these, a dropped SSE stream or a transport-level protocol error\n // is swallowed and the agent just stops getting results.\n transport.onerror = (err) => log('warn', `[mcp] ${name}: transport error - ${err.message}`)\n // info, not warn: an ordinary close() ends here too, and a routine\n // teardown logged as a warning trains people to ignore warnings.\n transport.onclose = () => log('info', `[mcp] ${name}: transport closed`)\n\n client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n // Subscribe BEFORE the first list call: a server that mutates its tool set\n // during init would otherwise lose the notification in the window between\n // connect() and listTools().\n if (opts.onToolsChanged) {\n const notify = opts.onToolsChanged\n client.setNotificationHandler(ToolListChangedNotificationSchema, () => notify(name))\n }\n\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n return { name, client, listed }\n } catch (err) {\n // The client may be live even though we ended up here (listTools() failing\n // after a successful connect). Close it, or the SSE stream leaks for the\n // lifetime of the tab.\n if (client) await client.close().catch(() => {})\n const needsAuthorization = err instanceof UnauthorizedError\n const message = err instanceof Error ? err.message : String(err)\n log(\n 'error',\n needsAuthorization\n ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl`\n : `[mcp] ${name}: failed to connect - ${message}`,\n )\n return { name, error: message, needsAuthorization }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: McpServerResult[] = []\n // server name → the tool keys it currently owns, so a refresh can remove\n // exactly its own entries.\n const mountedKeys = new Map<string, string[]>()\n\n const mountTools = (name: string, client: Client, listed: McpToolDescriptor[]): void => {\n const prefix = `${sanitizeName(name)}__`\n // Drop this server's existing entries first, mutating in place so external\n // references (an agent's live ToolSet) stay valid. We delete the keys this\n // server actually mounted rather than everything matching its prefix:\n // sanitizing can make two server names share a prefix (\"docs.v1\" and\n // \"docs/v1\"), and one server's prefix can nest inside another's (\"a\" and\n // \"a__b\") — either way a prefix sweep would silently unmount a DIFFERENT\n // server's tools.\n for (const k of mountedKeys.get(name) ?? []) {\n delete tools[k]\n }\n for (let i = catalog.length - 1; i >= 0; i--) {\n if (catalog[i].server === name) catalog.splice(i, 1)\n }\n const keys: string[] = []\n\n let mounted = 0\n for (const t of listed) {\n const prefixed = prefix + sanitizeName(t.name)\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const key = uniqueKey(prefixed, tools)\n if (!key) {\n log('warn', `[mcp] ${name}: cannot find a free name for tool \"${t.name}\"; skipping`)\n continue\n }\n if (key !== prefixed) {\n log('warn', `[mcp] ${name}: tool name \"${prefixed}\" already taken; mounted as \"${key}\"`)\n }\n const description = t.description ?? ''\n tools[key] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n })\n catalog.push({ name: key, description, server: name })\n keys.push(key)\n mounted += 1\n }\n mountedKeys.set(name, keys)\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n }\n\n const connects = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log)),\n )\n\n for (const server of connects) {\n if (!server.client || !server.listed) {\n results.push({\n name: server.name,\n connected: false,\n error: server.error,\n ...(server.needsAuthorization ? { needsAuthorization: true } : {}),\n })\n continue\n }\n clients.set(server.name, server.client)\n mountTools(server.name, server.client, server.listed)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n refreshServer: async (name: string) => {\n const client = clients.get(name)\n if (!client) throw new Error(`MCP server \"${name}\" is not connected`)\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n mountTools(name, client, listed)\n },\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n","import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'\nimport type {\n OAuthClientInformationMixed,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { IndexedDBVault, type VaultOptions } from '../secrets/vault.js'\n\n/**\n * Browser-side OAuth 2.1 for remote MCP servers: Dynamic Client Registration\n * (RFC 7591), PKCE authorization-code flow, and refresh-token rotation.\n *\n * The heavy lifting lives in the MCP SDK — `auth()` discovers the protected\n * resource (RFC 9728), registers the client if we have no client_id yet,\n * exchanges the code, and refreshes expired tokens; `StreamableHTTPClient-\n * Transport` retries any 401 through it. This module supplies the missing\n * half: WHERE the tokens, the registration, and the PKCE verifier are kept,\n * and HOW the user is sent to the authorization server.\n *\n * ── WHY NO AUTOMATIC NAVIGATION ──\n * `redirectToAuthorization` deliberately does NOT navigate by default. The SDK\n * can call it from deep inside a tool call, and throwing the user out of a\n * running agent would discard in-flight state. Instead the URL is recorded on\n * `authorizationUrl`, `connectMcpHttp` reports `needsAuthorization: true`, and\n * the app decides when to send the user there (a button, a popup, a redirect).\n * Pass `onRedirect` to opt into eager navigation.\n */\n\n/** Where an OAuth provider keeps its tokens / registration / PKCE verifier. */\nexport interface McpOAuthStorage {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * Default storage: the same encrypted IndexedDB vault that holds provider API\n * keys (AES-GCM, non-extractable key). Read the threat model in\n * `secrets/vault.ts` — this protects tokens at rest, not against active XSS.\n */\nexport class VaultOAuthStorage implements McpOAuthStorage {\n private readonly vault: IndexedDBVault\n\n constructor(opts: VaultOptions = {}) {\n this.vault = new IndexedDBVault(opts)\n }\n\n get(key: string): Promise<string | undefined> {\n return this.vault.getSecret(key)\n }\n set(key: string, value: string): Promise<void> {\n return this.vault.setSecret(key, value)\n }\n delete(key: string): Promise<void> {\n return this.vault.deleteSecret(key)\n }\n}\n\n/** Non-persistent storage for tests / SSR. Tokens die with the tab. */\nexport class MemoryOAuthStorage implements McpOAuthStorage {\n private readonly map = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.map.get(key)\n }\n async set(key: string, value: string): Promise<void> {\n this.map.set(key, value)\n }\n async delete(key: string): Promise<void> {\n this.map.delete(key)\n }\n}\n\nexport interface BrowserOAuthProviderOptions {\n /** The MCP endpoint this provider authorizes against (the resource server). */\n serverUrl: string\n /**\n * Where the authorization server sends the user back. Must be a page of your\n * app that calls `readOAuthCallback()` + `finishMcpOAuth()`. Registered with\n * the AS via DCR, so it has to match byte-for-byte on the way back.\n */\n redirectUrl: string | URL\n /** `client_name` submitted during dynamic registration. */\n clientName?: string\n /** Fallback scope when the server's metadata advertises none. */\n scope?: string\n /** Merged over the defaults; use it for `software_id`, `contacts`, … */\n clientMetadata?: Partial<OAuthClientMetadata>\n /** Defaults to the encrypted IndexedDB vault. */\n storage?: McpOAuthStorage\n /** Treat an access token as expired this many seconds early. Default 30. */\n expirySkewSeconds?: number\n /** Called when the SDK wants the user at the authorization server. */\n onRedirect?: (url: URL) => void | Promise<void>\n}\n\ninterface StoredTokens {\n tokens: OAuthTokens\n /** Absolute ms timestamp derived from `expires_in` at save time. */\n expiresAt?: number\n}\n\nconst DEFAULT_SKEW_SECONDS = 30\n\n// Namespacing by origin+path+query keeps two MCP servers on the same host\n// (/mcp/docs vs /mcp/crm, or ?tenant=a vs ?tenant=b) from sharing a\n// registration or a token — one tenant's bearer must never be sent to another.\nconst namespaceFor = (serverUrl: string): string => {\n const u = new URL(serverUrl)\n return `mcp-oauth:${u.origin}${u.pathname.replace(/\\/+$/, '')}${u.search}`\n}\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])\n\n/**\n * OAuth over plaintext hands the bearer token to anyone on the path, so only\n * https — or a loopback host, where there is no network to sniff — is allowed.\n * Parsed rather than pattern-matched: `http://localhost.attacker.example/` is a\n * registrable host that a prefix match would wave through.\n */\nexport const assertSecureOAuthUrl = (url: string, label = 'MCP server'): void => {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`${label}: \"${url}\" is not a valid URL`)\n }\n if (parsed.protocol === 'https:' || LOOPBACK_HOSTS.has(parsed.hostname)) return\n throw new Error(\n `${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to \"${url}\"`,\n )\n}\n\nconst randomToken = (): string => {\n const bytes = new Uint8Array(32)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * An `OAuthClientProvider` (MCP SDK) that persists everything it is handed and\n * never navigates behind your back. One instance per MCP server.\n */\nexport class BrowserOAuthProvider implements OAuthClientProvider {\n /** Set when the SDK asks for an authorization redirect; read it in your UI. */\n authorizationUrl?: URL\n\n readonly serverUrl: string\n\n private readonly storage: McpOAuthStorage\n private readonly ns: string\n private readonly _redirectUrl: string\n private readonly _clientMetadata: OAuthClientMetadata\n private readonly skewMs: number\n private readonly onRedirect?: (url: URL) => void | Promise<void>\n\n constructor(opts: BrowserOAuthProviderOptions) {\n assertSecureOAuthUrl(opts.serverUrl)\n this.serverUrl = opts.serverUrl\n this.storage = opts.storage ?? new VaultOAuthStorage()\n this.ns = namespaceFor(opts.serverUrl)\n this._redirectUrl = String(opts.redirectUrl)\n this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1000\n this.onRedirect = opts.onRedirect\n this._clientMetadata = {\n client_name: opts.clientName ?? 'agent-web',\n redirect_uris: [this._redirectUrl],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n // Public client: a browser cannot hold a client_secret. PKCE is what\n // actually protects the code exchange.\n token_endpoint_auth_method: 'none',\n ...(opts.scope ? { scope: opts.scope } : {}),\n ...opts.clientMetadata,\n } as OAuthClientMetadata\n }\n\n get redirectUrl(): string {\n return this._redirectUrl\n }\n\n get clientMetadata(): OAuthClientMetadata {\n return this._clientMetadata\n }\n\n private key(suffix: string): string {\n return `${this.ns}:${suffix}`\n }\n\n private async readJSON<T>(suffix: string): Promise<T | undefined> {\n // The read itself is inside the guard on purpose: the default storage\n // decrypts, and an AES-GCM blob written under a key that no longer exists\n // (a partially cleared IndexedDB, a rotated vault key) throws. Treating\n // that as \"no value\" lets the flow re-authorize instead of every call\n // rejecting with an opaque OperationError forever.\n try {\n const raw = await this.storage.get(this.key(suffix))\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n } catch {\n await this.storage.delete(this.key(suffix)).catch(() => {})\n return undefined\n }\n }\n\n async state(): Promise<string> {\n const value = randomToken()\n await this.storage.set(this.key('state'), value)\n return value\n }\n\n /**\n * CSRF check for the redirect back. Single-use: the stored value is dropped\n * whether or not it matched, so a replayed callback can't pass twice.\n */\n async verifyState(returned: string | undefined): Promise<boolean> {\n const expected = await this.storage.get(this.key('state'))\n await this.storage.delete(this.key('state'))\n return expected !== undefined && returned !== undefined && expected === returned\n }\n\n async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {\n return this.readJSON<OAuthClientInformationMixed>('client')\n }\n\n async saveClientInformation(info: OAuthClientInformationMixed): Promise<void> {\n await this.storage.set(this.key('client'), JSON.stringify(info))\n }\n\n async tokens(): Promise<OAuthTokens | undefined> {\n return (await this.readJSON<StoredTokens>('tokens'))?.tokens\n }\n\n async saveTokens(tokens: OAuthTokens): Promise<void> {\n const stored: StoredTokens = {\n tokens,\n expiresAt:\n typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined,\n }\n await this.storage.set(this.key('tokens'), JSON.stringify(stored))\n }\n\n async saveCodeVerifier(verifier: string): Promise<void> {\n await this.storage.set(this.key('verifier'), verifier)\n }\n\n async codeVerifier(): Promise<string> {\n const v = await this.storage.get(this.key('verifier'))\n if (!v) {\n throw new Error(\n 'No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow',\n )\n }\n return v\n }\n\n async redirectToAuthorization(url: URL): Promise<void> {\n this.authorizationUrl = url\n await this.onRedirect?.(url)\n }\n\n /**\n * Called by the SDK when the server rejects what we hold: `'tokens'` after an\n * `invalid_grant` (dead refresh token), `'client'` / `'all'` after an\n * `invalid_client` (the AS forgot our dynamic registration). Clearing lets\n * the SDK re-register and re-authorize without the user clearing site data.\n */\n async invalidateCredentials(\n scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery',\n ): Promise<void> {\n const targets = scope === 'all' ? ['tokens', 'client', 'verifier', 'state'] : [scope]\n await Promise.all(targets.map((t) => this.storage.delete(this.key(t))))\n }\n\n /** True when there is no access token, or it expires within the skew window. */\n async isAccessTokenExpired(): Promise<boolean> {\n const stored = await this.readJSON<StoredTokens>('tokens')\n if (!stored?.tokens?.access_token) return true\n // No expires_in means the AS didn't say; assume it is still good and let a\n // 401 drive the refresh.\n if (stored.expiresAt === undefined) return false\n return Date.now() + this.skewMs >= stored.expiresAt\n }\n\n /** True once an authorization has completed for this server. */\n async isAuthorized(): Promise<boolean> {\n return (await this.tokens()) !== undefined\n }\n\n /** Forget tokens, registration, verifier and state for this server. */\n async reset(): Promise<void> {\n this.authorizationUrl = undefined\n await this.invalidateCredentials('all')\n }\n}\n\nexport interface McpOAuthRequestOptions {\n /**\n * Custom fetch for the discovery / registration / token requests. Pass the\n * same one you give the connector so a proxy or an instrumentation wrapper\n * covers the OAuth traffic too.\n */\n fetch?: FetchLike\n}\n\nexport interface McpOAuthCallback {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\n/**\n * Pull the authorization-code parameters out of a redirect URL. Reads the query\n * string and, for hash-routed apps, the fragment. Returns undefined when the\n * URL carries neither a `code` nor an `error`, so it is safe to call on every\n * page load.\n */\nexport const readOAuthCallback = (input?: string | URL): McpOAuthCallback | undefined => {\n const href = input\n ? String(input)\n : typeof globalThis.location !== 'undefined'\n ? globalThis.location.href\n : undefined\n if (!href) return undefined\n let url: URL\n try {\n url = new URL(href)\n } catch {\n return undefined\n }\n const params = new URLSearchParams(url.search)\n const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash\n // Hash-routed apps get \"#/callback?code=…\"; take whatever follows the \"?\".\n const q = hash.indexOf('?')\n if (q >= 0) {\n for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {\n if (!params.has(k)) params.set(k, v)\n }\n }\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n if (!code && !error) return undefined\n return {\n code,\n state: params.get('state') ?? undefined,\n error,\n errorDescription: params.get('error_description') ?? undefined,\n }\n}\n\n/**\n * Finish the flow after the authorization server redirected back: verify the\n * `state`, exchange the code (PKCE) and persist the tokens. Throws when the AS\n * returned an error, when `state` doesn't match what we issued, or when the\n * exchange fails — never resolves on a half-finished authorization.\n */\nexport const finishMcpOAuth = async (\n provider: BrowserOAuthProvider,\n callback: McpOAuthCallback,\n opts: McpOAuthRequestOptions = {},\n): Promise<void> => {\n if (callback.error) {\n throw new Error(\n `Authorization denied by the server: ${callback.error}${\n callback.errorDescription ? ` - ${callback.errorDescription}` : ''\n }`,\n )\n }\n if (!callback.code) {\n throw new Error('Authorization callback carried no code')\n }\n if (!(await provider.verifyState(callback.state))) {\n throw new Error('Authorization state mismatch; refusing to exchange the code (possible CSRF)')\n }\n const result = await auth(provider, {\n serverUrl: provider.serverUrl,\n authorizationCode: callback.code,\n fetchFn: opts.fetch,\n })\n if (result !== 'AUTHORIZED') {\n throw new Error(`Token exchange did not complete (SDK returned \"${result}\")`)\n }\n}\n\n/**\n * Kick off (or silently renew) an authorization. Returns `'AUTHORIZED'` when a\n * usable token is in hand — including the case where a refresh token was\n * silently exchanged — or `'REDIRECT'` when the user has to visit\n * `provider.authorizationUrl`.\n */\nexport const beginMcpOAuth = async (\n provider: BrowserOAuthProvider,\n opts: McpOAuthRequestOptions = {},\n): Promise<'AUTHORIZED' | 'REDIRECT'> => {\n provider.authorizationUrl = undefined\n return auth(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch })\n}\n","import { openDB, type IDBPDatabase } from 'idb'\n\nexport interface AgentWebDBOptions {\n /** Database name (default 'agent-web'). Use a distinct name to isolate apps. */\n dbName?: string\n}\n\nexport const KEYS_STORE = 'keys'\nexport const SECRETS_STORE = 'secrets'\nexport const SESSIONS_STORE = 'sessions'\n\nlet cache = new Map<string, Promise<IDBPDatabase>>()\n\n/**\n * The single owner of the IndexedDB database. Every persistent store — the\n * non-extractable crypto key, encrypted secrets, and session transcripts —\n * lives under ONE database name + version. Centralising `openDB` here prevents\n * the classic bug where two modules open the same DB name with different store\n * sets and the second call throws a VersionError.\n */\nexport const openAgentWebDB = (opts: AgentWebDBOptions = {}): Promise<IDBPDatabase> => {\n const name = opts.dbName ?? 'agent-web'\n let p = cache.get(name)\n if (!p) {\n p = openDB(name, 1, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(KEYS_STORE)) db.createObjectStore(KEYS_STORE)\n if (!db.objectStoreNames.contains(SECRETS_STORE)) db.createObjectStore(SECRETS_STORE)\n if (!db.objectStoreNames.contains(SESSIONS_STORE)) db.createObjectStore(SESSIONS_STORE)\n },\n })\n cache.set(name, p)\n // A failed open (private mode, storage denied) must not stay cached, or\n // every later call would replay the same rejection forever.\n p.catch(() => {\n if (cache.get(name) === p) cache.delete(name)\n })\n }\n return p\n}\n\n/** Test hook: drop cached connections so a fresh (e.g. fake-indexeddb) DB is opened. */\nexport const _resetDBCache = (): void => {\n cache = new Map()\n}\n","import { KEYS_STORE, openAgentWebDB } from '../storage/db.js'\n\nconst KEY_ID = 'vault-key-v1'\n\nconst webcrypto = (): Crypto => {\n const c = (globalThis as { crypto?: Crypto }).crypto\n if (!c?.subtle || !c.getRandomValues) {\n throw new Error('WebCrypto (crypto.subtle) is unavailable in this environment')\n }\n return c\n}\n\nconst loadOrCreateKey = async (dbName?: string): Promise<CryptoKey> => {\n const db = await openAgentWebDB({ dbName })\n const existing = (await db.get(KEYS_STORE, KEY_ID)) as CryptoKey | undefined\n if (existing) return existing\n const key = await webcrypto().subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n /* extractable */ false,\n ['encrypt', 'decrypt'],\n )\n // Double-check inside one readwrite transaction: another tab may have created\n // a key while ours was generating. Losing that race and overwriting would\n // leave the other tab's secrets permanently undecryptable.\n const tx = db.transaction(KEYS_STORE, 'readwrite')\n const winner = (await tx.store.get(KEY_ID)) as CryptoKey | undefined\n if (winner) {\n await tx.done\n return winner\n }\n await tx.store.put(key, KEY_ID)\n await tx.done\n return key\n}\n\nconst keyCache = new Map<string, Promise<CryptoKey>>()\n\n/**\n * Fetch (or lazily create) the vault's AES-GCM key. The key is generated\n * NON-EXTRACTABLE and stored as a live `CryptoKey` in IndexedDB — a CryptoKey\n * is structured-cloneable, so it persists across reloads WITHOUT its raw bytes\n * ever being exposed to JavaScript. Not even in-page script can read it out; it\n * can only be used to encrypt / decrypt. That is what makes at-rest storage of\n * the secrets meaningful (see IndexedDBVault's threat model).\n *\n * Concurrent callers share one in-flight promise per DB, and creation is\n * double-checked in a single IDB transaction, so two racing writers can never\n * end up encrypting under different keys.\n */\nexport const getOrCreateVaultKey = (dbName?: string): Promise<CryptoKey> => {\n const cacheKey = dbName ?? 'agent-web'\n let p = keyCache.get(cacheKey)\n if (!p) {\n p = loadOrCreateKey(dbName)\n keyCache.set(cacheKey, p)\n p.catch(() => {\n if (keyCache.get(cacheKey) === p) keyCache.delete(cacheKey)\n })\n }\n return p\n}\n\nexport interface EncryptedBlob {\n /** 96-bit random IV, fresh per write. */\n iv: Uint8Array\n ciphertext: ArrayBuffer\n}\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\n/** Encrypt a JSON-serialisable value with a fresh 96-bit IV (AES-GCM). */\nexport const encryptJSON = async (key: CryptoKey, value: unknown): Promise<EncryptedBlob> => {\n const c = webcrypto()\n const iv = c.getRandomValues(new Uint8Array(12))\n const data = textEncoder.encode(JSON.stringify(value))\n const ciphertext = await c.subtle.encrypt({ name: 'AES-GCM', iv }, key, data)\n return { iv, ciphertext }\n}\n\n/** Decrypt a blob produced by encryptJSON. Throws if the key or blob don't match. */\nexport const decryptJSON = async <T = unknown>(key: CryptoKey, blob: EncryptedBlob): Promise<T> => {\n // Copy the IV into a fresh ArrayBuffer-backed view: after a round-trip through\n // IndexedDB its element buffer is typed as ArrayBufferLike, which no longer\n // satisfies BufferSource under TS's stricter typed-array generics.\n const iv = new Uint8Array(blob.iv)\n const plain = await webcrypto().subtle.decrypt({ name: 'AES-GCM', iv }, key, blob.ciphertext)\n return JSON.parse(textDecoder.decode(plain)) as T\n}\n","import { openAgentWebDB, SECRETS_STORE } from '../storage/db.js'\nimport { decryptJSON, encryptJSON, getOrCreateVaultKey, type EncryptedBlob } from './crypto.js'\n\nexport interface VaultOptions {\n /** Database name, forwarded to the shared IDB owner (default 'agent-web'). */\n dbName?: string\n}\n\n/**\n * Encrypted key/value secret store backed by IndexedDB. Values are encrypted at\n * rest with the non-extractable AES-GCM vault key (see crypto.ts).\n *\n * ── THREAT MODEL (read before shipping any app-owned key to a browser) ──\n * This vault PROTECTS secrets **at rest** — an IndexedDB / disk dump yields\n * only ciphertext — and against **casual DevTools inspection** — no plaintext\n * key is stored, and the AES key cannot be exported even by in-page script.\n *\n * It does **NOT** defend against **active XSS on your origin**: any script that\n * already runs on the page can call `getSecret()`, or use the live `CryptoKey`\n * to decrypt. Browser storage is not a secrets manager.\n *\n * Therefore this vault is for the **end user's OWN key on their OWN device**\n * (bring-your-own-key). A shared, app-owned key must NEVER reach the client —\n * put it behind a server proxy (`ProviderModelSpec.baseURL`) or the Vercel AI\n * Gateway instead.\n */\nexport class IndexedDBVault {\n private readonly dbName?: string\n\n constructor(opts: VaultOptions = {}) {\n this.dbName = opts.dbName\n }\n\n private key(): Promise<CryptoKey> {\n return getOrCreateVaultKey(this.dbName)\n }\n\n /** Encrypt and store a secret string under `id`. */\n async setSecret(id: string, value: string): Promise<void> {\n const blob = await encryptJSON(await this.key(), value)\n const db = await openAgentWebDB({ dbName: this.dbName })\n await db.put(SECRETS_STORE, blob, id)\n }\n\n /** Fetch and decrypt a secret, or undefined if absent. */\n async getSecret(id: string): Promise<string | undefined> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n const blob = (await db.get(SECRETS_STORE, id)) as EncryptedBlob | undefined\n if (!blob) return undefined\n return decryptJSON<string>(await this.key(), blob)\n }\n\n async deleteSecret(id: string): Promise<void> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n await db.delete(SECRETS_STORE, id)\n }\n\n async listSecretIds(): Promise<string[]> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n return (await db.getAllKeys(SECRETS_STORE)).map(String)\n }\n}\n"],"mappings":"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","names":["import_auth","import_auth"]}
1
+ {"version":3,"sources":["../src/mcp/index.ts","../src/mcp/http.ts","../src/mcp/oauth.ts","../src/storage/db.ts","../src/secrets/crypto.ts","../src/secrets/vault.ts"],"sourcesContent":["export { connectMcpHttp, diagnoseMcpCors, flattenContent } from './http.js'\n// Re-exported so a host can identify an authorization failure by identity.\n// The SDK's class does not set `name`, so `err.name === 'UnauthorizedError'`\n// is always false — string sniffing is not an option, and a host that only\n// depends on this package has no other handle on the class.\nexport { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'\nexport type {\n McpHttpServerConfig,\n McpCatalogEntry,\n McpServerResult,\n ConnectedMcp,\n ConnectMcpOptions,\n} from './http.js'\nexport {\n BrowserOAuthProvider,\n MemoryOAuthStorage,\n VaultOAuthStorage,\n beginMcpOAuth,\n finishMcpOAuth,\n readOAuthCallback,\n} from './oauth.js'\nexport type {\n BrowserOAuthProviderOptions,\n McpOAuthCallback,\n McpOAuthRequestOptions,\n McpOAuthStorage,\n} from './oauth.js'\n","import {\n auth,\n UnauthorizedError,\n type OAuthClientProvider,\n} from '@modelcontextprotocol/sdk/client/auth.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPReconnectionOptions,\n} from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\nimport { assertSecureOAuthUrl } from './oauth.js'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n *\n * Three ways to authenticate, in ascending order of capability:\n * 1. `headers` — a static token you already hold.\n * 2. `getHeaders` — resolved once per connect, for tokens you mint yourself.\n * 3. `authProvider` — full OAuth 2.1: the SDK discovers the authorization\n * server, dynamically registers the client (DCR), runs PKCE, and — the\n * part 1 and 2 cannot do — transparently REFRESHES the access token when\n * the server answers 401 mid-run. See `./oauth`.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n /**\n * OAuth client provider (see `./oauth` for a browser implementation). The\n * transport sends its access token, refreshes on 401 and retries the request.\n * Do NOT also set an `Authorization` entry in `headers`: request headers are\n * merged last and would shadow the OAuth token.\n */\n authProvider?: OAuthClientProvider\n /** Custom fetch for every request: proxying, instrumentation, retries. */\n fetch?: FetchLike\n /** Overrides for the SSE reconnect backoff. Merged over the defaults below. */\n reconnection?: Partial<StreamableHTTPReconnectionOptions>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface McpServerResult {\n name: string\n connected: boolean\n error?: string\n /**\n * The server demands OAuth and we have no usable token. When the config\n * carried a `BrowserOAuthProvider`, its `authorizationUrl` now holds the page\n * to send the user to; reconnect once they are back.\n */\n needsAuthorization?: boolean\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: McpServerResult[]\n /**\n * Re-list one server's tools and rewrite its entries in `tools`/`catalog`.\n * Both are mutated in place, so anything holding a reference (including a\n * live agent config) sees the new set. Throws if the server isn't connected.\n */\n refreshServer: (name: string) => Promise<void>\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n /**\n * Fires when a server pushes `notifications/tools/list_changed`. Call\n * `refreshServer(name)` from a point where no run is reading `tools` — this\n * connector deliberately does not refresh behind a running agent's back.\n */\n onToolsChanged?: (server: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// Browsers drop long-lived SSE streams on tab-throttling and flaky networks far\n// more often than a server does; the SDK's default (2 retries) gives up too\n// early for an agent that may idle between steps.\nconst DEFAULT_RECONNECTION: StreamableHTTPReconnectionOptions = {\n maxReconnectionDelay: 30_000,\n initialReconnectionDelay: 1_000,\n reconnectionDelayGrowFactor: 1.5,\n maxRetries: 5,\n}\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Sanitizing can map two distinct MCP names onto one key (\"a.b\" and \"a-b\" both\n * become \"a_b\"). Suffix instead of dropping: a tool the model can't see is a\n * silent capability loss, and the disambiguated name still resolves back to the\n * original on dispatch.\n */\nconst uniqueKey = (base: string, taken: ToolSet): string => {\n // hasOwn, not truthiness: a tool named so that the key lands on \"__proto__\"\n // would otherwise read as already taken through the prototype chain.\n if (!Object.hasOwn(taken, base)) return base\n for (let n = 2; n < 100; n++) {\n const suffix = `_${n}`\n const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix\n if (!Object.hasOwn(taken, candidate)) return candidate\n }\n return ''\n}\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface McpToolDescriptor {\n name: string\n description?: string\n inputSchema: unknown\n}\n\ninterface ServerConnect {\n name: string\n client?: Client\n listed?: McpToolDescriptor[]\n error?: string\n needsAuthorization?: boolean\n}\n\n/** Thrown by the refresh-only view when the SDK reaches for a redirect. */\nclass RefreshOnlyAbort extends Error {}\n\n/**\n * A view of the provider that can renew a token but can never start an\n * interactive authorization.\n *\n * `auth()` does not have a refresh-only mode: when a refresh fails for a reason\n * it deems recoverable (a 5xx from the token endpoint, a network blip) it falls\n * through to a brand-new authorization — issuing a state and a PKCE verifier,\n * overwriting the stored pair, and calling `redirectToAuthorization`. The\n * transport then does its own `auth()` on the first 401, overwriting them\n * again, so the user comes back holding a code the second pair can't verify.\n * Blocking the three redirect-path methods aborts the escalation before\n * anything is overwritten; the transport's 401 handling stays the single owner\n * of interactive authorization.\n */\nconst refreshOnly = (p: OAuthClientProvider): OAuthClientProvider => ({\n // Must stay truthy: `auth()` reads a missing redirectUrl as \"non-interactive\n // grant\" and would try to fetch a token without one.\n get redirectUrl() {\n return p.redirectUrl\n },\n get clientMetadata() {\n return p.clientMetadata\n },\n clientInformation: () => p.clientInformation(),\n saveClientInformation: p.saveClientInformation?.bind(p),\n tokens: () => p.tokens(),\n saveTokens: (t) => p.saveTokens(t),\n invalidateCredentials: p.invalidateCredentials?.bind(p),\n addClientAuthentication: p.addClientAuthentication?.bind(p),\n validateResourceURL: p.validateResourceURL?.bind(p),\n codeVerifier: () => p.codeVerifier(),\n state: () => {\n throw new RefreshOnlyAbort()\n },\n saveCodeVerifier: () => {\n throw new RefreshOnlyAbort()\n },\n redirectToAuthorization: () => {\n throw new RefreshOnlyAbort()\n },\n})\n\n/**\n * An access token that is already expired produces a guaranteed 401 on the\n * first request. When the provider tells us so AND we hold a refresh token,\n * renew before connecting: one round-trip saved, and a run that starts on a\n * stale token doesn't surface a spurious auth error to the model.\n *\n * Best-effort by design — anything that goes wrong here is left to the\n * transport's own 401 handling, which is the path that knows how to ask the\n * user for a new authorization.\n */\nconst refreshIfExpired = async (\n provider: OAuthClientProvider,\n serverUrl: string,\n fetchFn: FetchLike | undefined,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n name: string,\n): Promise<void> => {\n const check = (provider as { isAccessTokenExpired?: () => boolean | Promise<boolean> })\n .isAccessTokenExpired\n if (typeof check !== 'function') return\n if (!(await check.call(provider))) return\n const tokens = await provider.tokens()\n // Nothing to refresh with: let the 401 path ask for a new authorization.\n if (!tokens?.refresh_token) return\n try {\n await auth(refreshOnly(provider), { serverUrl, fetchFn })\n } catch (err) {\n log(\n 'info',\n err instanceof RefreshOnlyAbort\n ? `[mcp] ${name}: the stored token could not be refreshed; a new authorization is needed`\n : `[mcp] ${name}: proactive token refresh failed (${\n err instanceof Error ? err.message : String(err)\n }); continuing`,\n )\n }\n}\n\n/**\n * A fetch the browser refused for CORS reasons is indistinguishable, from\n * JavaScript, from a server that is simply down: both surface as a bare\n * `TypeError`. The reason is deliberately withheld, and the browser prints it\n * only to the devtools console — which is why this failure is usually read as a\n * client bug and debugged in the wrong place for a long time.\n */\nconst isNetworkLevelFailure = (err: unknown): boolean =>\n err instanceof TypeError || /failed to fetch|networkerror|load failed/i.test(String(err))\n\n/**\n * Work out WHY the browser blocked us, by asking a question it will answer.\n *\n * A request carrying only `content-type` needs the same preflight as the real\n * one but asks for nothing else. So if the plain request gets through while the\n * transport's did not, the origin is fine and a *header* is the problem — and\n * the header the transport adds is `MCP-Protocol-Version`, which the spec has\n * required on every post-initialize request since 2025-06-18 and which server\n * CORS lists written before that date do not include. That is the shape of the\n * bug that lets OAuth and `initialize` succeed and then kills the connection\n * moments later, looking for all the world like the consent step failed.\n *\n * Exported because a UI wants to say this too, not just log it.\n */\nexport const diagnoseMcpCors = async (\n url: string,\n fetchFn: FetchLike = globalThis.fetch,\n): Promise<string | undefined> => {\n let plain: Response\n try {\n plain = await fetchFn(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'ping' }),\n })\n } catch {\n return (\n `the browser blocked every request to ${url}, including a plain one. ` +\n 'The server is unreachable, or it does not answer the CORS preflight for this ' +\n 'origin: it must return Access-Control-Allow-Origin (and handle OPTIONS) on the ' +\n 'MCP endpoint, the OAuth endpoints and the /.well-known documents.'\n )\n }\n const hints = [\n `a plain request to ${url} succeeded (HTTP ${plain.status}) but the transport's did not, ` +\n 'so the origin is allowed and a request HEADER is being refused. Add ' +\n 'MCP-Protocol-Version (required on every request after initialize) and Last-Event-ID ' +\n \"to the server's Access-Control-Allow-Headers, alongside Content-Type, Authorization \" +\n 'and mcp-session-id.',\n ]\n // A 401 whose challenge JS cannot read means OAuth discovery cannot start\n // from a browser at all, so say so while we are here.\n if (plain.status === 401 && !plain.headers.get('www-authenticate')) {\n hints.push(\n 'The server also answers 401 without exposing WWW-Authenticate: add it to ' +\n 'Access-Control-Expose-Headers, or a browser client cannot read the challenge ' +\n 'and OAuth discovery never starts.',\n )\n }\n return hints.join(' ')\n}\n\nconst openConnection = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerConnect> => {\n let client: Client | undefined\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n if (cfg.authProvider) {\n if (headers && 'Authorization' in headers) {\n log(\n 'warn',\n `[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`,\n )\n }\n assertSecureOAuthUrl(cfg.url, name)\n await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name)\n }\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n authProvider: cfg.authProvider,\n fetch: cfg.fetch,\n reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection },\n })\n // Without these, a dropped SSE stream or a transport-level protocol error\n // is swallowed and the agent just stops getting results.\n transport.onerror = (err) => log('warn', `[mcp] ${name}: transport error - ${err.message}`)\n // info, not warn: an ordinary close() ends here too, and a routine\n // teardown logged as a warning trains people to ignore warnings.\n transport.onclose = () => log('info', `[mcp] ${name}: transport closed`)\n\n client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n // Subscribe BEFORE the first list call: a server that mutates its tool set\n // during init would otherwise lose the notification in the window between\n // connect() and listTools().\n if (opts.onToolsChanged) {\n const notify = opts.onToolsChanged\n client.setNotificationHandler(ToolListChangedNotificationSchema, () => notify(name))\n }\n\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n return { name, client, listed }\n } catch (err) {\n // The client may be live even though we ended up here (listTools() failing\n // after a successful connect). Close it, or the SSE stream leaks for the\n // lifetime of the tab.\n if (client) await client.close().catch(() => {})\n const needsAuthorization = err instanceof UnauthorizedError\n let message = err instanceof Error ? err.message : String(err)\n // Turn \"TypeError: Failed to fetch\" into the sentence the reader needs.\n // Only on the failure path, so the extra request costs nothing in normal use.\n if (!needsAuthorization && isNetworkLevelFailure(err)) {\n const hint = await diagnoseMcpCors(cfg.url, cfg.fetch).catch(() => undefined)\n if (hint) message = `${message} - ${hint}`\n }\n log(\n 'error',\n needsAuthorization\n ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl`\n : `[mcp] ${name}: failed to connect - ${message}`,\n )\n return { name, error: message, needsAuthorization }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: McpServerResult[] = []\n // server name → the tool keys it currently owns, so a refresh can remove\n // exactly its own entries.\n const mountedKeys = new Map<string, string[]>()\n\n const mountTools = (name: string, client: Client, listed: McpToolDescriptor[]): void => {\n const prefix = `${sanitizeName(name)}__`\n // Drop this server's existing entries first, mutating in place so external\n // references (an agent's live ToolSet) stay valid. We delete the keys this\n // server actually mounted rather than everything matching its prefix:\n // sanitizing can make two server names share a prefix (\"docs.v1\" and\n // \"docs/v1\"), and one server's prefix can nest inside another's (\"a\" and\n // \"a__b\") — either way a prefix sweep would silently unmount a DIFFERENT\n // server's tools.\n for (const k of mountedKeys.get(name) ?? []) {\n delete tools[k]\n }\n for (let i = catalog.length - 1; i >= 0; i--) {\n if (catalog[i].server === name) catalog.splice(i, 1)\n }\n const keys: string[] = []\n\n let mounted = 0\n for (const t of listed) {\n const prefixed = prefix + sanitizeName(t.name)\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const key = uniqueKey(prefixed, tools)\n if (!key) {\n log('warn', `[mcp] ${name}: cannot find a free name for tool \"${t.name}\"; skipping`)\n continue\n }\n if (key !== prefixed) {\n log('warn', `[mcp] ${name}: tool name \"${prefixed}\" already taken; mounted as \"${key}\"`)\n }\n const description = t.description ?? ''\n tools[key] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n })\n catalog.push({ name: key, description, server: name })\n keys.push(key)\n mounted += 1\n }\n mountedKeys.set(name, keys)\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n }\n\n const connects = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log)),\n )\n\n for (const server of connects) {\n if (!server.client || !server.listed) {\n results.push({\n name: server.name,\n connected: false,\n error: server.error,\n ...(server.needsAuthorization ? { needsAuthorization: true } : {}),\n })\n continue\n }\n clients.set(server.name, server.client)\n mountTools(server.name, server.client, server.listed)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n refreshServer: async (name: string) => {\n const client = clients.get(name)\n if (!client) throw new Error(`MCP server \"${name}\" is not connected`)\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n mountTools(name, client, listed)\n },\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n","import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'\nimport type {\n OAuthClientInformationMixed,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { IndexedDBVault, type VaultOptions } from '../secrets/vault.js'\n\n/**\n * Browser-side OAuth 2.1 for remote MCP servers: Dynamic Client Registration\n * (RFC 7591), PKCE authorization-code flow, and refresh-token rotation.\n *\n * The heavy lifting lives in the MCP SDK — `auth()` discovers the protected\n * resource (RFC 9728), registers the client if we have no client_id yet,\n * exchanges the code, and refreshes expired tokens; `StreamableHTTPClient-\n * Transport` retries any 401 through it. This module supplies the missing\n * half: WHERE the tokens, the registration, and the PKCE verifier are kept,\n * and HOW the user is sent to the authorization server.\n *\n * ── WHY NO AUTOMATIC NAVIGATION ──\n * `redirectToAuthorization` deliberately does NOT navigate by default. The SDK\n * can call it from deep inside a tool call, and throwing the user out of a\n * running agent would discard in-flight state. Instead the URL is recorded on\n * `authorizationUrl`, `connectMcpHttp` reports `needsAuthorization: true`, and\n * the app decides when to send the user there (a button, a popup, a redirect).\n * Pass `onRedirect` to opt into eager navigation.\n */\n\n/** Where an OAuth provider keeps its tokens / registration / PKCE verifier. */\nexport interface McpOAuthStorage {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * Default storage: the same encrypted IndexedDB vault that holds provider API\n * keys (AES-GCM, non-extractable key). Read the threat model in\n * `secrets/vault.ts` — this protects tokens at rest, not against active XSS.\n */\nexport class VaultOAuthStorage implements McpOAuthStorage {\n private readonly vault: IndexedDBVault\n\n constructor(opts: VaultOptions = {}) {\n this.vault = new IndexedDBVault(opts)\n }\n\n get(key: string): Promise<string | undefined> {\n return this.vault.getSecret(key)\n }\n set(key: string, value: string): Promise<void> {\n return this.vault.setSecret(key, value)\n }\n delete(key: string): Promise<void> {\n return this.vault.deleteSecret(key)\n }\n}\n\n/** Non-persistent storage for tests / SSR. Tokens die with the tab. */\nexport class MemoryOAuthStorage implements McpOAuthStorage {\n private readonly map = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.map.get(key)\n }\n async set(key: string, value: string): Promise<void> {\n this.map.set(key, value)\n }\n async delete(key: string): Promise<void> {\n this.map.delete(key)\n }\n}\n\nexport interface BrowserOAuthProviderOptions {\n /** The MCP endpoint this provider authorizes against (the resource server). */\n serverUrl: string\n /**\n * Where the authorization server sends the user back. Must be a page of your\n * app that calls `readOAuthCallback()` + `finishMcpOAuth()`. Registered with\n * the AS via DCR, so it has to match byte-for-byte on the way back.\n */\n redirectUrl: string | URL\n /** `client_name` submitted during dynamic registration. */\n clientName?: string\n /** Fallback scope when the server's metadata advertises none. */\n scope?: string\n /** Merged over the defaults; use it for `software_id`, `contacts`, … */\n clientMetadata?: Partial<OAuthClientMetadata>\n /** Defaults to the encrypted IndexedDB vault. */\n storage?: McpOAuthStorage\n /** Treat an access token as expired this many seconds early. Default 30. */\n expirySkewSeconds?: number\n /** Called when the SDK wants the user at the authorization server. */\n onRedirect?: (url: URL) => void | Promise<void>\n}\n\ninterface StoredTokens {\n tokens: OAuthTokens\n /** Absolute ms timestamp derived from `expires_in` at save time. */\n expiresAt?: number\n}\n\nconst DEFAULT_SKEW_SECONDS = 30\n\n// Namespacing by origin+path+query keeps two MCP servers on the same host\n// (/mcp/docs vs /mcp/crm, or ?tenant=a vs ?tenant=b) from sharing a\n// registration or a token — one tenant's bearer must never be sent to another.\nconst namespaceFor = (serverUrl: string): string => {\n const u = new URL(serverUrl)\n return `mcp-oauth:${u.origin}${u.pathname.replace(/\\/+$/, '')}${u.search}`\n}\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])\n\n/**\n * OAuth over plaintext hands the bearer token to anyone on the path, so only\n * https — or a loopback host, where there is no network to sniff — is allowed.\n * Parsed rather than pattern-matched: `http://localhost.attacker.example/` is a\n * registrable host that a prefix match would wave through.\n */\nexport const assertSecureOAuthUrl = (url: string, label = 'MCP server'): void => {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`${label}: \"${url}\" is not a valid URL`)\n }\n if (parsed.protocol === 'https:' || LOOPBACK_HOSTS.has(parsed.hostname)) return\n throw new Error(\n `${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to \"${url}\"`,\n )\n}\n\nconst randomToken = (): string => {\n const bytes = new Uint8Array(32)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * An `OAuthClientProvider` (MCP SDK) that persists everything it is handed and\n * never navigates behind your back. One instance per MCP server.\n */\nexport class BrowserOAuthProvider implements OAuthClientProvider {\n /** Set when the SDK asks for an authorization redirect; read it in your UI. */\n authorizationUrl?: URL\n\n readonly serverUrl: string\n\n private readonly storage: McpOAuthStorage\n private readonly ns: string\n private readonly _redirectUrl: string\n private readonly _clientMetadata: OAuthClientMetadata\n private readonly skewMs: number\n private readonly onRedirect?: (url: URL) => void | Promise<void>\n\n constructor(opts: BrowserOAuthProviderOptions) {\n assertSecureOAuthUrl(opts.serverUrl)\n this.serverUrl = opts.serverUrl\n this.storage = opts.storage ?? new VaultOAuthStorage()\n this.ns = namespaceFor(opts.serverUrl)\n this._redirectUrl = String(opts.redirectUrl)\n this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1000\n this.onRedirect = opts.onRedirect\n this._clientMetadata = {\n client_name: opts.clientName ?? 'agent-web',\n redirect_uris: [this._redirectUrl],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n // Public client: a browser cannot hold a client_secret. PKCE is what\n // actually protects the code exchange.\n token_endpoint_auth_method: 'none',\n ...(opts.scope ? { scope: opts.scope } : {}),\n ...opts.clientMetadata,\n } as OAuthClientMetadata\n }\n\n get redirectUrl(): string {\n return this._redirectUrl\n }\n\n get clientMetadata(): OAuthClientMetadata {\n return this._clientMetadata\n }\n\n private key(suffix: string): string {\n return `${this.ns}:${suffix}`\n }\n\n private async readJSON<T>(suffix: string): Promise<T | undefined> {\n // The read itself is inside the guard on purpose: the default storage\n // decrypts, and an AES-GCM blob written under a key that no longer exists\n // (a partially cleared IndexedDB, a rotated vault key) throws. Treating\n // that as \"no value\" lets the flow re-authorize instead of every call\n // rejecting with an opaque OperationError forever.\n try {\n const raw = await this.storage.get(this.key(suffix))\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n } catch {\n await this.storage.delete(this.key(suffix)).catch(() => {})\n return undefined\n }\n }\n\n async state(): Promise<string> {\n const value = randomToken()\n await this.storage.set(this.key('state'), value)\n return value\n }\n\n /**\n * CSRF check for the redirect back. Single-use: the stored value is dropped\n * whether or not it matched, so a replayed callback can't pass twice.\n */\n async verifyState(returned: string | undefined): Promise<boolean> {\n const expected = await this.storage.get(this.key('state'))\n await this.storage.delete(this.key('state'))\n return expected !== undefined && returned !== undefined && expected === returned\n }\n\n async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {\n return this.readJSON<OAuthClientInformationMixed>('client')\n }\n\n async saveClientInformation(info: OAuthClientInformationMixed): Promise<void> {\n await this.storage.set(this.key('client'), JSON.stringify(info))\n }\n\n async tokens(): Promise<OAuthTokens | undefined> {\n return (await this.readJSON<StoredTokens>('tokens'))?.tokens\n }\n\n async saveTokens(tokens: OAuthTokens): Promise<void> {\n const stored: StoredTokens = {\n tokens,\n expiresAt:\n typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined,\n }\n await this.storage.set(this.key('tokens'), JSON.stringify(stored))\n }\n\n async saveCodeVerifier(verifier: string): Promise<void> {\n await this.storage.set(this.key('verifier'), verifier)\n }\n\n async codeVerifier(): Promise<string> {\n const v = await this.storage.get(this.key('verifier'))\n if (!v) {\n throw new Error(\n 'No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow',\n )\n }\n return v\n }\n\n async redirectToAuthorization(url: URL): Promise<void> {\n this.authorizationUrl = url\n await this.onRedirect?.(url)\n }\n\n /**\n * Called by the SDK when the server rejects what we hold: `'tokens'` after an\n * `invalid_grant` (dead refresh token), `'client'` / `'all'` after an\n * `invalid_client` (the AS forgot our dynamic registration). Clearing lets\n * the SDK re-register and re-authorize without the user clearing site data.\n */\n async invalidateCredentials(\n scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery',\n ): Promise<void> {\n const targets = scope === 'all' ? ['tokens', 'client', 'verifier', 'state'] : [scope]\n await Promise.all(targets.map((t) => this.storage.delete(this.key(t))))\n }\n\n /** True when there is no access token, or it expires within the skew window. */\n async isAccessTokenExpired(): Promise<boolean> {\n const stored = await this.readJSON<StoredTokens>('tokens')\n if (!stored?.tokens?.access_token) return true\n // No expires_in means the AS didn't say; assume it is still good and let a\n // 401 drive the refresh.\n if (stored.expiresAt === undefined) return false\n return Date.now() + this.skewMs >= stored.expiresAt\n }\n\n /** True once an authorization has completed for this server. */\n async isAuthorized(): Promise<boolean> {\n return (await this.tokens()) !== undefined\n }\n\n /** Forget tokens, registration, verifier and state for this server. */\n async reset(): Promise<void> {\n this.authorizationUrl = undefined\n await this.invalidateCredentials('all')\n }\n}\n\nexport interface McpOAuthRequestOptions {\n /**\n * Custom fetch for the discovery / registration / token requests. Pass the\n * same one you give the connector so a proxy or an instrumentation wrapper\n * covers the OAuth traffic too.\n */\n fetch?: FetchLike\n}\n\nexport interface McpOAuthCallback {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\n/**\n * Pull the authorization-code parameters out of a redirect URL. Reads the query\n * string and, for hash-routed apps, the fragment. Returns undefined when the\n * URL carries neither a `code` nor an `error`, so it is safe to call on every\n * page load.\n */\nexport const readOAuthCallback = (input?: string | URL): McpOAuthCallback | undefined => {\n const href = input\n ? String(input)\n : typeof globalThis.location !== 'undefined'\n ? globalThis.location.href\n : undefined\n if (!href) return undefined\n let url: URL\n try {\n url = new URL(href)\n } catch {\n return undefined\n }\n const params = new URLSearchParams(url.search)\n const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash\n // Hash-routed apps get \"#/callback?code=…\"; take whatever follows the \"?\".\n const q = hash.indexOf('?')\n if (q >= 0) {\n for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {\n if (!params.has(k)) params.set(k, v)\n }\n }\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n if (!code && !error) return undefined\n return {\n code,\n state: params.get('state') ?? undefined,\n error,\n errorDescription: params.get('error_description') ?? undefined,\n }\n}\n\n/**\n * Finish the flow after the authorization server redirected back: verify the\n * `state`, exchange the code (PKCE) and persist the tokens. Throws when the AS\n * returned an error, when `state` doesn't match what we issued, or when the\n * exchange fails — never resolves on a half-finished authorization.\n */\nexport const finishMcpOAuth = async (\n provider: BrowserOAuthProvider,\n callback: McpOAuthCallback,\n opts: McpOAuthRequestOptions = {},\n): Promise<void> => {\n if (callback.error) {\n throw new Error(\n `Authorization denied by the server: ${callback.error}${\n callback.errorDescription ? ` - ${callback.errorDescription}` : ''\n }`,\n )\n }\n if (!callback.code) {\n throw new Error('Authorization callback carried no code')\n }\n if (!(await provider.verifyState(callback.state))) {\n throw new Error('Authorization state mismatch; refusing to exchange the code (possible CSRF)')\n }\n const result = await auth(provider, {\n serverUrl: provider.serverUrl,\n authorizationCode: callback.code,\n fetchFn: opts.fetch,\n })\n if (result !== 'AUTHORIZED') {\n throw new Error(`Token exchange did not complete (SDK returned \"${result}\")`)\n }\n}\n\n/**\n * Kick off (or silently renew) an authorization. Returns `'AUTHORIZED'` when a\n * usable token is in hand — including the case where a refresh token was\n * silently exchanged — or `'REDIRECT'` when the user has to visit\n * `provider.authorizationUrl`.\n */\nexport const beginMcpOAuth = async (\n provider: BrowserOAuthProvider,\n opts: McpOAuthRequestOptions = {},\n): Promise<'AUTHORIZED' | 'REDIRECT'> => {\n provider.authorizationUrl = undefined\n return auth(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch })\n}\n","import { openDB, type IDBPDatabase } from 'idb'\n\nexport interface AgentWebDBOptions {\n /** Database name (default 'agent-web'). Use a distinct name to isolate apps. */\n dbName?: string\n}\n\nexport const KEYS_STORE = 'keys'\nexport const SECRETS_STORE = 'secrets'\nexport const SESSIONS_STORE = 'sessions'\n\nlet cache = new Map<string, Promise<IDBPDatabase>>()\n\n/**\n * The single owner of the IndexedDB database. Every persistent store — the\n * non-extractable crypto key, encrypted secrets, and session transcripts —\n * lives under ONE database name + version. Centralising `openDB` here prevents\n * the classic bug where two modules open the same DB name with different store\n * sets and the second call throws a VersionError.\n */\nexport const openAgentWebDB = (opts: AgentWebDBOptions = {}): Promise<IDBPDatabase> => {\n const name = opts.dbName ?? 'agent-web'\n let p = cache.get(name)\n if (!p) {\n p = openDB(name, 1, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(KEYS_STORE)) db.createObjectStore(KEYS_STORE)\n if (!db.objectStoreNames.contains(SECRETS_STORE)) db.createObjectStore(SECRETS_STORE)\n if (!db.objectStoreNames.contains(SESSIONS_STORE)) db.createObjectStore(SESSIONS_STORE)\n },\n })\n cache.set(name, p)\n // A failed open (private mode, storage denied) must not stay cached, or\n // every later call would replay the same rejection forever.\n p.catch(() => {\n if (cache.get(name) === p) cache.delete(name)\n })\n }\n return p\n}\n\n/** Test hook: drop cached connections so a fresh (e.g. fake-indexeddb) DB is opened. */\nexport const _resetDBCache = (): void => {\n cache = new Map()\n}\n","import { KEYS_STORE, openAgentWebDB } from '../storage/db.js'\n\nconst KEY_ID = 'vault-key-v1'\n\nconst webcrypto = (): Crypto => {\n const c = (globalThis as { crypto?: Crypto }).crypto\n if (!c?.subtle || !c.getRandomValues) {\n throw new Error('WebCrypto (crypto.subtle) is unavailable in this environment')\n }\n return c\n}\n\nconst loadOrCreateKey = async (dbName?: string): Promise<CryptoKey> => {\n const db = await openAgentWebDB({ dbName })\n const existing = (await db.get(KEYS_STORE, KEY_ID)) as CryptoKey | undefined\n if (existing) return existing\n const key = await webcrypto().subtle.generateKey(\n { name: 'AES-GCM', length: 256 },\n /* extractable */ false,\n ['encrypt', 'decrypt'],\n )\n // Double-check inside one readwrite transaction: another tab may have created\n // a key while ours was generating. Losing that race and overwriting would\n // leave the other tab's secrets permanently undecryptable.\n const tx = db.transaction(KEYS_STORE, 'readwrite')\n const winner = (await tx.store.get(KEY_ID)) as CryptoKey | undefined\n if (winner) {\n await tx.done\n return winner\n }\n await tx.store.put(key, KEY_ID)\n await tx.done\n return key\n}\n\nconst keyCache = new Map<string, Promise<CryptoKey>>()\n\n/**\n * Fetch (or lazily create) the vault's AES-GCM key. The key is generated\n * NON-EXTRACTABLE and stored as a live `CryptoKey` in IndexedDB — a CryptoKey\n * is structured-cloneable, so it persists across reloads WITHOUT its raw bytes\n * ever being exposed to JavaScript. Not even in-page script can read it out; it\n * can only be used to encrypt / decrypt. That is what makes at-rest storage of\n * the secrets meaningful (see IndexedDBVault's threat model).\n *\n * Concurrent callers share one in-flight promise per DB, and creation is\n * double-checked in a single IDB transaction, so two racing writers can never\n * end up encrypting under different keys.\n */\nexport const getOrCreateVaultKey = (dbName?: string): Promise<CryptoKey> => {\n const cacheKey = dbName ?? 'agent-web'\n let p = keyCache.get(cacheKey)\n if (!p) {\n p = loadOrCreateKey(dbName)\n keyCache.set(cacheKey, p)\n p.catch(() => {\n if (keyCache.get(cacheKey) === p) keyCache.delete(cacheKey)\n })\n }\n return p\n}\n\nexport interface EncryptedBlob {\n /** 96-bit random IV, fresh per write. */\n iv: Uint8Array\n ciphertext: ArrayBuffer\n}\n\nconst textEncoder = new TextEncoder()\nconst textDecoder = new TextDecoder()\n\n/** Encrypt a JSON-serialisable value with a fresh 96-bit IV (AES-GCM). */\nexport const encryptJSON = async (key: CryptoKey, value: unknown): Promise<EncryptedBlob> => {\n const c = webcrypto()\n const iv = c.getRandomValues(new Uint8Array(12))\n const data = textEncoder.encode(JSON.stringify(value))\n const ciphertext = await c.subtle.encrypt({ name: 'AES-GCM', iv }, key, data)\n return { iv, ciphertext }\n}\n\n/** Decrypt a blob produced by encryptJSON. Throws if the key or blob don't match. */\nexport const decryptJSON = async <T = unknown>(key: CryptoKey, blob: EncryptedBlob): Promise<T> => {\n // Copy the IV into a fresh ArrayBuffer-backed view: after a round-trip through\n // IndexedDB its element buffer is typed as ArrayBufferLike, which no longer\n // satisfies BufferSource under TS's stricter typed-array generics.\n const iv = new Uint8Array(blob.iv)\n const plain = await webcrypto().subtle.decrypt({ name: 'AES-GCM', iv }, key, blob.ciphertext)\n return JSON.parse(textDecoder.decode(plain)) as T\n}\n","import { openAgentWebDB, SECRETS_STORE } from '../storage/db.js'\nimport { decryptJSON, encryptJSON, getOrCreateVaultKey, type EncryptedBlob } from './crypto.js'\n\nexport interface VaultOptions {\n /** Database name, forwarded to the shared IDB owner (default 'agent-web'). */\n dbName?: string\n}\n\n/**\n * Encrypted key/value secret store backed by IndexedDB. Values are encrypted at\n * rest with the non-extractable AES-GCM vault key (see crypto.ts).\n *\n * ── THREAT MODEL (read before shipping any app-owned key to a browser) ──\n * This vault PROTECTS secrets **at rest** — an IndexedDB / disk dump yields\n * only ciphertext — and against **casual DevTools inspection** — no plaintext\n * key is stored, and the AES key cannot be exported even by in-page script.\n *\n * It does **NOT** defend against **active XSS on your origin**: any script that\n * already runs on the page can call `getSecret()`, or use the live `CryptoKey`\n * to decrypt. Browser storage is not a secrets manager.\n *\n * Therefore this vault is for the **end user's OWN key on their OWN device**\n * (bring-your-own-key). A shared, app-owned key must NEVER reach the client —\n * put it behind a server proxy (`ProviderModelSpec.baseURL`) or the Vercel AI\n * Gateway instead.\n */\nexport class IndexedDBVault {\n private readonly dbName?: string\n\n constructor(opts: VaultOptions = {}) {\n this.dbName = opts.dbName\n }\n\n private key(): Promise<CryptoKey> {\n return getOrCreateVaultKey(this.dbName)\n }\n\n /** Encrypt and store a secret string under `id`. */\n async setSecret(id: string, value: string): Promise<void> {\n const blob = await encryptJSON(await this.key(), value)\n const db = await openAgentWebDB({ dbName: this.dbName })\n await db.put(SECRETS_STORE, blob, id)\n }\n\n /** Fetch and decrypt a secret, or undefined if absent. */\n async getSecret(id: string): Promise<string | undefined> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n const blob = (await db.get(SECRETS_STORE, id)) as EncryptedBlob | undefined\n if (!blob) return undefined\n return decryptJSON<string>(await this.key(), blob)\n }\n\n async deleteSecret(id: string): Promise<void> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n await db.delete(SECRETS_STORE, id)\n }\n\n async listSecretIds(): Promise<string[]> {\n const db = await openAgentWebDB({ dbName: this.dbName })\n return (await db.getAllKeys(SECRETS_STORE)).map(String)\n }\n}\n"],"mappings":"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","names":["import_auth","import_auth"]}
package/dist/mcp.d.cts CHANGED
@@ -85,6 +85,21 @@ interface ConnectMcpOptions {
85
85
  * array of text + raw parts for the host to handle.
86
86
  */
87
87
  declare const flattenContent: (content: unknown) => unknown;
88
+ /**
89
+ * Work out WHY the browser blocked us, by asking a question it will answer.
90
+ *
91
+ * A request carrying only `content-type` needs the same preflight as the real
92
+ * one but asks for nothing else. So if the plain request gets through while the
93
+ * transport's did not, the origin is fine and a *header* is the problem — and
94
+ * the header the transport adds is `MCP-Protocol-Version`, which the spec has
95
+ * required on every post-initialize request since 2025-06-18 and which server
96
+ * CORS lists written before that date do not include. That is the shape of the
97
+ * bug that lets OAuth and `initialize` succeed and then kills the connection
98
+ * moments later, looking for all the world like the consent step failed.
99
+ *
100
+ * Exported because a UI wants to say this too, not just log it.
101
+ */
102
+ declare const diagnoseMcpCors: (url: string, fetchFn?: FetchLike) => Promise<string | undefined>;
88
103
  /**
89
104
  * Connect to one or more HTTP MCP servers and return their tools + catalogue.
90
105
  * Servers connect concurrently; tools, catalogue, and results merge in the
@@ -240,4 +255,4 @@ declare const finishMcpOAuth: (provider: BrowserOAuthProvider, callback: McpOAut
240
255
  */
241
256
  declare const beginMcpOAuth: (provider: BrowserOAuthProvider, opts?: McpOAuthRequestOptions) => Promise<"AUTHORIZED" | "REDIRECT">;
242
257
 
243
- export { BrowserOAuthProvider, type BrowserOAuthProviderOptions, type ConnectMcpOptions, type ConnectedMcp, type McpCatalogEntry, type McpHttpServerConfig, type McpOAuthCallback, type McpOAuthRequestOptions, type McpOAuthStorage, type McpServerResult, MemoryOAuthStorage, VaultOAuthStorage, beginMcpOAuth, connectMcpHttp, finishMcpOAuth, flattenContent, readOAuthCallback };
258
+ export { BrowserOAuthProvider, type BrowserOAuthProviderOptions, type ConnectMcpOptions, type ConnectedMcp, type McpCatalogEntry, type McpHttpServerConfig, type McpOAuthCallback, type McpOAuthRequestOptions, type McpOAuthStorage, type McpServerResult, MemoryOAuthStorage, VaultOAuthStorage, beginMcpOAuth, connectMcpHttp, diagnoseMcpCors, finishMcpOAuth, flattenContent, readOAuthCallback };
package/dist/mcp.d.ts CHANGED
@@ -85,6 +85,21 @@ interface ConnectMcpOptions {
85
85
  * array of text + raw parts for the host to handle.
86
86
  */
87
87
  declare const flattenContent: (content: unknown) => unknown;
88
+ /**
89
+ * Work out WHY the browser blocked us, by asking a question it will answer.
90
+ *
91
+ * A request carrying only `content-type` needs the same preflight as the real
92
+ * one but asks for nothing else. So if the plain request gets through while the
93
+ * transport's did not, the origin is fine and a *header* is the problem — and
94
+ * the header the transport adds is `MCP-Protocol-Version`, which the spec has
95
+ * required on every post-initialize request since 2025-06-18 and which server
96
+ * CORS lists written before that date do not include. That is the shape of the
97
+ * bug that lets OAuth and `initialize` succeed and then kills the connection
98
+ * moments later, looking for all the world like the consent step failed.
99
+ *
100
+ * Exported because a UI wants to say this too, not just log it.
101
+ */
102
+ declare const diagnoseMcpCors: (url: string, fetchFn?: FetchLike) => Promise<string | undefined>;
88
103
  /**
89
104
  * Connect to one or more HTTP MCP servers and return their tools + catalogue.
90
105
  * Servers connect concurrently; tools, catalogue, and results merge in the
@@ -240,4 +255,4 @@ declare const finishMcpOAuth: (provider: BrowserOAuthProvider, callback: McpOAut
240
255
  */
241
256
  declare const beginMcpOAuth: (provider: BrowserOAuthProvider, opts?: McpOAuthRequestOptions) => Promise<"AUTHORIZED" | "REDIRECT">;
242
257
 
243
- export { BrowserOAuthProvider, type BrowserOAuthProviderOptions, type ConnectMcpOptions, type ConnectedMcp, type McpCatalogEntry, type McpHttpServerConfig, type McpOAuthCallback, type McpOAuthRequestOptions, type McpOAuthStorage, type McpServerResult, MemoryOAuthStorage, VaultOAuthStorage, beginMcpOAuth, connectMcpHttp, finishMcpOAuth, flattenContent, readOAuthCallback };
258
+ export { BrowserOAuthProvider, type BrowserOAuthProviderOptions, type ConnectMcpOptions, type ConnectedMcp, type McpCatalogEntry, type McpHttpServerConfig, type McpOAuthCallback, type McpOAuthRequestOptions, type McpOAuthStorage, type McpServerResult, MemoryOAuthStorage, VaultOAuthStorage, beginMcpOAuth, connectMcpHttp, diagnoseMcpCors, finishMcpOAuth, flattenContent, readOAuthCallback };
package/dist/mcp.js CHANGED
@@ -311,6 +311,28 @@ var refreshIfExpired = async (provider, serverUrl, fetchFn, log, name) => {
311
311
  );
312
312
  }
313
313
  };
314
+ var isNetworkLevelFailure = (err) => err instanceof TypeError || /failed to fetch|networkerror|load failed/i.test(String(err));
315
+ var diagnoseMcpCors = async (url, fetchFn = globalThis.fetch) => {
316
+ let plain;
317
+ try {
318
+ plain = await fetchFn(url, {
319
+ method: "POST",
320
+ headers: { "content-type": "application/json" },
321
+ body: JSON.stringify({ jsonrpc: "2.0", id: 0, method: "ping" })
322
+ });
323
+ } catch {
324
+ return `the browser blocked every request to ${url}, including a plain one. The server is unreachable, or it does not answer the CORS preflight for this origin: it must return Access-Control-Allow-Origin (and handle OPTIONS) on the MCP endpoint, the OAuth endpoints and the /.well-known documents.`;
325
+ }
326
+ const hints = [
327
+ `a plain request to ${url} succeeded (HTTP ${plain.status}) but the transport's did not, so the origin is allowed and a request HEADER is being refused. Add MCP-Protocol-Version (required on every request after initialize) and Last-Event-ID to the server's Access-Control-Allow-Headers, alongside Content-Type, Authorization and mcp-session-id.`
328
+ ];
329
+ if (plain.status === 401 && !plain.headers.get("www-authenticate")) {
330
+ hints.push(
331
+ "The server also answers 401 without exposing WWW-Authenticate: add it to Access-Control-Expose-Headers, or a browser client cannot read the challenge and OAuth discovery never starts."
332
+ );
333
+ }
334
+ return hints.join(" ");
335
+ };
314
336
  var openConnection = async (name, cfg, opts, log) => {
315
337
  let client;
316
338
  try {
@@ -351,7 +373,11 @@ var openConnection = async (name, cfg, opts, log) => {
351
373
  if (client) await client.close().catch(() => {
352
374
  });
353
375
  const needsAuthorization = err instanceof UnauthorizedError;
354
- const message = err instanceof Error ? err.message : String(err);
376
+ let message = err instanceof Error ? err.message : String(err);
377
+ if (!needsAuthorization && isNetworkLevelFailure(err)) {
378
+ const hint = await diagnoseMcpCors(cfg.url, cfg.fetch).catch(() => void 0);
379
+ if (hint) message = `${message} - ${hint}`;
380
+ }
355
381
  log(
356
382
  "error",
357
383
  needsAuthorization ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl` : `[mcp] ${name}: failed to connect - ${message}`
@@ -460,6 +486,7 @@ export {
460
486
  VaultOAuthStorage,
461
487
  beginMcpOAuth,
462
488
  connectMcpHttp,
489
+ diagnoseMcpCors,
463
490
  finishMcpOAuth,
464
491
  flattenContent,
465
492
  readOAuthCallback
package/dist/mcp.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/mcp/http.ts","../src/mcp/oauth.ts","../src/mcp/index.ts"],"sourcesContent":["import {\n auth,\n UnauthorizedError,\n type OAuthClientProvider,\n} from '@modelcontextprotocol/sdk/client/auth.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPReconnectionOptions,\n} from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\nimport { assertSecureOAuthUrl } from './oauth.js'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n *\n * Three ways to authenticate, in ascending order of capability:\n * 1. `headers` — a static token you already hold.\n * 2. `getHeaders` — resolved once per connect, for tokens you mint yourself.\n * 3. `authProvider` — full OAuth 2.1: the SDK discovers the authorization\n * server, dynamically registers the client (DCR), runs PKCE, and — the\n * part 1 and 2 cannot do — transparently REFRESHES the access token when\n * the server answers 401 mid-run. See `./oauth`.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n /**\n * OAuth client provider (see `./oauth` for a browser implementation). The\n * transport sends its access token, refreshes on 401 and retries the request.\n * Do NOT also set an `Authorization` entry in `headers`: request headers are\n * merged last and would shadow the OAuth token.\n */\n authProvider?: OAuthClientProvider\n /** Custom fetch for every request: proxying, instrumentation, retries. */\n fetch?: FetchLike\n /** Overrides for the SSE reconnect backoff. Merged over the defaults below. */\n reconnection?: Partial<StreamableHTTPReconnectionOptions>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface McpServerResult {\n name: string\n connected: boolean\n error?: string\n /**\n * The server demands OAuth and we have no usable token. When the config\n * carried a `BrowserOAuthProvider`, its `authorizationUrl` now holds the page\n * to send the user to; reconnect once they are back.\n */\n needsAuthorization?: boolean\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: McpServerResult[]\n /**\n * Re-list one server's tools and rewrite its entries in `tools`/`catalog`.\n * Both are mutated in place, so anything holding a reference (including a\n * live agent config) sees the new set. Throws if the server isn't connected.\n */\n refreshServer: (name: string) => Promise<void>\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n /**\n * Fires when a server pushes `notifications/tools/list_changed`. Call\n * `refreshServer(name)` from a point where no run is reading `tools` — this\n * connector deliberately does not refresh behind a running agent's back.\n */\n onToolsChanged?: (server: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// Browsers drop long-lived SSE streams on tab-throttling and flaky networks far\n// more often than a server does; the SDK's default (2 retries) gives up too\n// early for an agent that may idle between steps.\nconst DEFAULT_RECONNECTION: StreamableHTTPReconnectionOptions = {\n maxReconnectionDelay: 30_000,\n initialReconnectionDelay: 1_000,\n reconnectionDelayGrowFactor: 1.5,\n maxRetries: 5,\n}\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Sanitizing can map two distinct MCP names onto one key (\"a.b\" and \"a-b\" both\n * become \"a_b\"). Suffix instead of dropping: a tool the model can't see is a\n * silent capability loss, and the disambiguated name still resolves back to the\n * original on dispatch.\n */\nconst uniqueKey = (base: string, taken: ToolSet): string => {\n // hasOwn, not truthiness: a tool named so that the key lands on \"__proto__\"\n // would otherwise read as already taken through the prototype chain.\n if (!Object.hasOwn(taken, base)) return base\n for (let n = 2; n < 100; n++) {\n const suffix = `_${n}`\n const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix\n if (!Object.hasOwn(taken, candidate)) return candidate\n }\n return ''\n}\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface McpToolDescriptor {\n name: string\n description?: string\n inputSchema: unknown\n}\n\ninterface ServerConnect {\n name: string\n client?: Client\n listed?: McpToolDescriptor[]\n error?: string\n needsAuthorization?: boolean\n}\n\n/** Thrown by the refresh-only view when the SDK reaches for a redirect. */\nclass RefreshOnlyAbort extends Error {}\n\n/**\n * A view of the provider that can renew a token but can never start an\n * interactive authorization.\n *\n * `auth()` does not have a refresh-only mode: when a refresh fails for a reason\n * it deems recoverable (a 5xx from the token endpoint, a network blip) it falls\n * through to a brand-new authorization — issuing a state and a PKCE verifier,\n * overwriting the stored pair, and calling `redirectToAuthorization`. The\n * transport then does its own `auth()` on the first 401, overwriting them\n * again, so the user comes back holding a code the second pair can't verify.\n * Blocking the three redirect-path methods aborts the escalation before\n * anything is overwritten; the transport's 401 handling stays the single owner\n * of interactive authorization.\n */\nconst refreshOnly = (p: OAuthClientProvider): OAuthClientProvider => ({\n // Must stay truthy: `auth()` reads a missing redirectUrl as \"non-interactive\n // grant\" and would try to fetch a token without one.\n get redirectUrl() {\n return p.redirectUrl\n },\n get clientMetadata() {\n return p.clientMetadata\n },\n clientInformation: () => p.clientInformation(),\n saveClientInformation: p.saveClientInformation?.bind(p),\n tokens: () => p.tokens(),\n saveTokens: (t) => p.saveTokens(t),\n invalidateCredentials: p.invalidateCredentials?.bind(p),\n addClientAuthentication: p.addClientAuthentication?.bind(p),\n validateResourceURL: p.validateResourceURL?.bind(p),\n codeVerifier: () => p.codeVerifier(),\n state: () => {\n throw new RefreshOnlyAbort()\n },\n saveCodeVerifier: () => {\n throw new RefreshOnlyAbort()\n },\n redirectToAuthorization: () => {\n throw new RefreshOnlyAbort()\n },\n})\n\n/**\n * An access token that is already expired produces a guaranteed 401 on the\n * first request. When the provider tells us so AND we hold a refresh token,\n * renew before connecting: one round-trip saved, and a run that starts on a\n * stale token doesn't surface a spurious auth error to the model.\n *\n * Best-effort by design — anything that goes wrong here is left to the\n * transport's own 401 handling, which is the path that knows how to ask the\n * user for a new authorization.\n */\nconst refreshIfExpired = async (\n provider: OAuthClientProvider,\n serverUrl: string,\n fetchFn: FetchLike | undefined,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n name: string,\n): Promise<void> => {\n const check = (provider as { isAccessTokenExpired?: () => boolean | Promise<boolean> })\n .isAccessTokenExpired\n if (typeof check !== 'function') return\n if (!(await check.call(provider))) return\n const tokens = await provider.tokens()\n // Nothing to refresh with: let the 401 path ask for a new authorization.\n if (!tokens?.refresh_token) return\n try {\n await auth(refreshOnly(provider), { serverUrl, fetchFn })\n } catch (err) {\n log(\n 'info',\n err instanceof RefreshOnlyAbort\n ? `[mcp] ${name}: the stored token could not be refreshed; a new authorization is needed`\n : `[mcp] ${name}: proactive token refresh failed (${\n err instanceof Error ? err.message : String(err)\n }); continuing`,\n )\n }\n}\n\nconst openConnection = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerConnect> => {\n let client: Client | undefined\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n if (cfg.authProvider) {\n if (headers && 'Authorization' in headers) {\n log(\n 'warn',\n `[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`,\n )\n }\n assertSecureOAuthUrl(cfg.url, name)\n await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name)\n }\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n authProvider: cfg.authProvider,\n fetch: cfg.fetch,\n reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection },\n })\n // Without these, a dropped SSE stream or a transport-level protocol error\n // is swallowed and the agent just stops getting results.\n transport.onerror = (err) => log('warn', `[mcp] ${name}: transport error - ${err.message}`)\n // info, not warn: an ordinary close() ends here too, and a routine\n // teardown logged as a warning trains people to ignore warnings.\n transport.onclose = () => log('info', `[mcp] ${name}: transport closed`)\n\n client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n // Subscribe BEFORE the first list call: a server that mutates its tool set\n // during init would otherwise lose the notification in the window between\n // connect() and listTools().\n if (opts.onToolsChanged) {\n const notify = opts.onToolsChanged\n client.setNotificationHandler(ToolListChangedNotificationSchema, () => notify(name))\n }\n\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n return { name, client, listed }\n } catch (err) {\n // The client may be live even though we ended up here (listTools() failing\n // after a successful connect). Close it, or the SSE stream leaks for the\n // lifetime of the tab.\n if (client) await client.close().catch(() => {})\n const needsAuthorization = err instanceof UnauthorizedError\n const message = err instanceof Error ? err.message : String(err)\n log(\n 'error',\n needsAuthorization\n ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl`\n : `[mcp] ${name}: failed to connect - ${message}`,\n )\n return { name, error: message, needsAuthorization }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: McpServerResult[] = []\n // server name → the tool keys it currently owns, so a refresh can remove\n // exactly its own entries.\n const mountedKeys = new Map<string, string[]>()\n\n const mountTools = (name: string, client: Client, listed: McpToolDescriptor[]): void => {\n const prefix = `${sanitizeName(name)}__`\n // Drop this server's existing entries first, mutating in place so external\n // references (an agent's live ToolSet) stay valid. We delete the keys this\n // server actually mounted rather than everything matching its prefix:\n // sanitizing can make two server names share a prefix (\"docs.v1\" and\n // \"docs/v1\"), and one server's prefix can nest inside another's (\"a\" and\n // \"a__b\") — either way a prefix sweep would silently unmount a DIFFERENT\n // server's tools.\n for (const k of mountedKeys.get(name) ?? []) {\n delete tools[k]\n }\n for (let i = catalog.length - 1; i >= 0; i--) {\n if (catalog[i].server === name) catalog.splice(i, 1)\n }\n const keys: string[] = []\n\n let mounted = 0\n for (const t of listed) {\n const prefixed = prefix + sanitizeName(t.name)\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const key = uniqueKey(prefixed, tools)\n if (!key) {\n log('warn', `[mcp] ${name}: cannot find a free name for tool \"${t.name}\"; skipping`)\n continue\n }\n if (key !== prefixed) {\n log('warn', `[mcp] ${name}: tool name \"${prefixed}\" already taken; mounted as \"${key}\"`)\n }\n const description = t.description ?? ''\n tools[key] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n })\n catalog.push({ name: key, description, server: name })\n keys.push(key)\n mounted += 1\n }\n mountedKeys.set(name, keys)\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n }\n\n const connects = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log)),\n )\n\n for (const server of connects) {\n if (!server.client || !server.listed) {\n results.push({\n name: server.name,\n connected: false,\n error: server.error,\n ...(server.needsAuthorization ? { needsAuthorization: true } : {}),\n })\n continue\n }\n clients.set(server.name, server.client)\n mountTools(server.name, server.client, server.listed)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n refreshServer: async (name: string) => {\n const client = clients.get(name)\n if (!client) throw new Error(`MCP server \"${name}\" is not connected`)\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n mountTools(name, client, listed)\n },\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n","import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'\nimport type {\n OAuthClientInformationMixed,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { IndexedDBVault, type VaultOptions } from '../secrets/vault.js'\n\n/**\n * Browser-side OAuth 2.1 for remote MCP servers: Dynamic Client Registration\n * (RFC 7591), PKCE authorization-code flow, and refresh-token rotation.\n *\n * The heavy lifting lives in the MCP SDK — `auth()` discovers the protected\n * resource (RFC 9728), registers the client if we have no client_id yet,\n * exchanges the code, and refreshes expired tokens; `StreamableHTTPClient-\n * Transport` retries any 401 through it. This module supplies the missing\n * half: WHERE the tokens, the registration, and the PKCE verifier are kept,\n * and HOW the user is sent to the authorization server.\n *\n * ── WHY NO AUTOMATIC NAVIGATION ──\n * `redirectToAuthorization` deliberately does NOT navigate by default. The SDK\n * can call it from deep inside a tool call, and throwing the user out of a\n * running agent would discard in-flight state. Instead the URL is recorded on\n * `authorizationUrl`, `connectMcpHttp` reports `needsAuthorization: true`, and\n * the app decides when to send the user there (a button, a popup, a redirect).\n * Pass `onRedirect` to opt into eager navigation.\n */\n\n/** Where an OAuth provider keeps its tokens / registration / PKCE verifier. */\nexport interface McpOAuthStorage {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * Default storage: the same encrypted IndexedDB vault that holds provider API\n * keys (AES-GCM, non-extractable key). Read the threat model in\n * `secrets/vault.ts` — this protects tokens at rest, not against active XSS.\n */\nexport class VaultOAuthStorage implements McpOAuthStorage {\n private readonly vault: IndexedDBVault\n\n constructor(opts: VaultOptions = {}) {\n this.vault = new IndexedDBVault(opts)\n }\n\n get(key: string): Promise<string | undefined> {\n return this.vault.getSecret(key)\n }\n set(key: string, value: string): Promise<void> {\n return this.vault.setSecret(key, value)\n }\n delete(key: string): Promise<void> {\n return this.vault.deleteSecret(key)\n }\n}\n\n/** Non-persistent storage for tests / SSR. Tokens die with the tab. */\nexport class MemoryOAuthStorage implements McpOAuthStorage {\n private readonly map = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.map.get(key)\n }\n async set(key: string, value: string): Promise<void> {\n this.map.set(key, value)\n }\n async delete(key: string): Promise<void> {\n this.map.delete(key)\n }\n}\n\nexport interface BrowserOAuthProviderOptions {\n /** The MCP endpoint this provider authorizes against (the resource server). */\n serverUrl: string\n /**\n * Where the authorization server sends the user back. Must be a page of your\n * app that calls `readOAuthCallback()` + `finishMcpOAuth()`. Registered with\n * the AS via DCR, so it has to match byte-for-byte on the way back.\n */\n redirectUrl: string | URL\n /** `client_name` submitted during dynamic registration. */\n clientName?: string\n /** Fallback scope when the server's metadata advertises none. */\n scope?: string\n /** Merged over the defaults; use it for `software_id`, `contacts`, … */\n clientMetadata?: Partial<OAuthClientMetadata>\n /** Defaults to the encrypted IndexedDB vault. */\n storage?: McpOAuthStorage\n /** Treat an access token as expired this many seconds early. Default 30. */\n expirySkewSeconds?: number\n /** Called when the SDK wants the user at the authorization server. */\n onRedirect?: (url: URL) => void | Promise<void>\n}\n\ninterface StoredTokens {\n tokens: OAuthTokens\n /** Absolute ms timestamp derived from `expires_in` at save time. */\n expiresAt?: number\n}\n\nconst DEFAULT_SKEW_SECONDS = 30\n\n// Namespacing by origin+path+query keeps two MCP servers on the same host\n// (/mcp/docs vs /mcp/crm, or ?tenant=a vs ?tenant=b) from sharing a\n// registration or a token — one tenant's bearer must never be sent to another.\nconst namespaceFor = (serverUrl: string): string => {\n const u = new URL(serverUrl)\n return `mcp-oauth:${u.origin}${u.pathname.replace(/\\/+$/, '')}${u.search}`\n}\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])\n\n/**\n * OAuth over plaintext hands the bearer token to anyone on the path, so only\n * https — or a loopback host, where there is no network to sniff — is allowed.\n * Parsed rather than pattern-matched: `http://localhost.attacker.example/` is a\n * registrable host that a prefix match would wave through.\n */\nexport const assertSecureOAuthUrl = (url: string, label = 'MCP server'): void => {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`${label}: \"${url}\" is not a valid URL`)\n }\n if (parsed.protocol === 'https:' || LOOPBACK_HOSTS.has(parsed.hostname)) return\n throw new Error(\n `${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to \"${url}\"`,\n )\n}\n\nconst randomToken = (): string => {\n const bytes = new Uint8Array(32)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * An `OAuthClientProvider` (MCP SDK) that persists everything it is handed and\n * never navigates behind your back. One instance per MCP server.\n */\nexport class BrowserOAuthProvider implements OAuthClientProvider {\n /** Set when the SDK asks for an authorization redirect; read it in your UI. */\n authorizationUrl?: URL\n\n readonly serverUrl: string\n\n private readonly storage: McpOAuthStorage\n private readonly ns: string\n private readonly _redirectUrl: string\n private readonly _clientMetadata: OAuthClientMetadata\n private readonly skewMs: number\n private readonly onRedirect?: (url: URL) => void | Promise<void>\n\n constructor(opts: BrowserOAuthProviderOptions) {\n assertSecureOAuthUrl(opts.serverUrl)\n this.serverUrl = opts.serverUrl\n this.storage = opts.storage ?? new VaultOAuthStorage()\n this.ns = namespaceFor(opts.serverUrl)\n this._redirectUrl = String(opts.redirectUrl)\n this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1000\n this.onRedirect = opts.onRedirect\n this._clientMetadata = {\n client_name: opts.clientName ?? 'agent-web',\n redirect_uris: [this._redirectUrl],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n // Public client: a browser cannot hold a client_secret. PKCE is what\n // actually protects the code exchange.\n token_endpoint_auth_method: 'none',\n ...(opts.scope ? { scope: opts.scope } : {}),\n ...opts.clientMetadata,\n } as OAuthClientMetadata\n }\n\n get redirectUrl(): string {\n return this._redirectUrl\n }\n\n get clientMetadata(): OAuthClientMetadata {\n return this._clientMetadata\n }\n\n private key(suffix: string): string {\n return `${this.ns}:${suffix}`\n }\n\n private async readJSON<T>(suffix: string): Promise<T | undefined> {\n // The read itself is inside the guard on purpose: the default storage\n // decrypts, and an AES-GCM blob written under a key that no longer exists\n // (a partially cleared IndexedDB, a rotated vault key) throws. Treating\n // that as \"no value\" lets the flow re-authorize instead of every call\n // rejecting with an opaque OperationError forever.\n try {\n const raw = await this.storage.get(this.key(suffix))\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n } catch {\n await this.storage.delete(this.key(suffix)).catch(() => {})\n return undefined\n }\n }\n\n async state(): Promise<string> {\n const value = randomToken()\n await this.storage.set(this.key('state'), value)\n return value\n }\n\n /**\n * CSRF check for the redirect back. Single-use: the stored value is dropped\n * whether or not it matched, so a replayed callback can't pass twice.\n */\n async verifyState(returned: string | undefined): Promise<boolean> {\n const expected = await this.storage.get(this.key('state'))\n await this.storage.delete(this.key('state'))\n return expected !== undefined && returned !== undefined && expected === returned\n }\n\n async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {\n return this.readJSON<OAuthClientInformationMixed>('client')\n }\n\n async saveClientInformation(info: OAuthClientInformationMixed): Promise<void> {\n await this.storage.set(this.key('client'), JSON.stringify(info))\n }\n\n async tokens(): Promise<OAuthTokens | undefined> {\n return (await this.readJSON<StoredTokens>('tokens'))?.tokens\n }\n\n async saveTokens(tokens: OAuthTokens): Promise<void> {\n const stored: StoredTokens = {\n tokens,\n expiresAt:\n typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined,\n }\n await this.storage.set(this.key('tokens'), JSON.stringify(stored))\n }\n\n async saveCodeVerifier(verifier: string): Promise<void> {\n await this.storage.set(this.key('verifier'), verifier)\n }\n\n async codeVerifier(): Promise<string> {\n const v = await this.storage.get(this.key('verifier'))\n if (!v) {\n throw new Error(\n 'No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow',\n )\n }\n return v\n }\n\n async redirectToAuthorization(url: URL): Promise<void> {\n this.authorizationUrl = url\n await this.onRedirect?.(url)\n }\n\n /**\n * Called by the SDK when the server rejects what we hold: `'tokens'` after an\n * `invalid_grant` (dead refresh token), `'client'` / `'all'` after an\n * `invalid_client` (the AS forgot our dynamic registration). Clearing lets\n * the SDK re-register and re-authorize without the user clearing site data.\n */\n async invalidateCredentials(\n scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery',\n ): Promise<void> {\n const targets = scope === 'all' ? ['tokens', 'client', 'verifier', 'state'] : [scope]\n await Promise.all(targets.map((t) => this.storage.delete(this.key(t))))\n }\n\n /** True when there is no access token, or it expires within the skew window. */\n async isAccessTokenExpired(): Promise<boolean> {\n const stored = await this.readJSON<StoredTokens>('tokens')\n if (!stored?.tokens?.access_token) return true\n // No expires_in means the AS didn't say; assume it is still good and let a\n // 401 drive the refresh.\n if (stored.expiresAt === undefined) return false\n return Date.now() + this.skewMs >= stored.expiresAt\n }\n\n /** True once an authorization has completed for this server. */\n async isAuthorized(): Promise<boolean> {\n return (await this.tokens()) !== undefined\n }\n\n /** Forget tokens, registration, verifier and state for this server. */\n async reset(): Promise<void> {\n this.authorizationUrl = undefined\n await this.invalidateCredentials('all')\n }\n}\n\nexport interface McpOAuthRequestOptions {\n /**\n * Custom fetch for the discovery / registration / token requests. Pass the\n * same one you give the connector so a proxy or an instrumentation wrapper\n * covers the OAuth traffic too.\n */\n fetch?: FetchLike\n}\n\nexport interface McpOAuthCallback {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\n/**\n * Pull the authorization-code parameters out of a redirect URL. Reads the query\n * string and, for hash-routed apps, the fragment. Returns undefined when the\n * URL carries neither a `code` nor an `error`, so it is safe to call on every\n * page load.\n */\nexport const readOAuthCallback = (input?: string | URL): McpOAuthCallback | undefined => {\n const href = input\n ? String(input)\n : typeof globalThis.location !== 'undefined'\n ? globalThis.location.href\n : undefined\n if (!href) return undefined\n let url: URL\n try {\n url = new URL(href)\n } catch {\n return undefined\n }\n const params = new URLSearchParams(url.search)\n const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash\n // Hash-routed apps get \"#/callback?code=…\"; take whatever follows the \"?\".\n const q = hash.indexOf('?')\n if (q >= 0) {\n for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {\n if (!params.has(k)) params.set(k, v)\n }\n }\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n if (!code && !error) return undefined\n return {\n code,\n state: params.get('state') ?? undefined,\n error,\n errorDescription: params.get('error_description') ?? undefined,\n }\n}\n\n/**\n * Finish the flow after the authorization server redirected back: verify the\n * `state`, exchange the code (PKCE) and persist the tokens. Throws when the AS\n * returned an error, when `state` doesn't match what we issued, or when the\n * exchange fails — never resolves on a half-finished authorization.\n */\nexport const finishMcpOAuth = async (\n provider: BrowserOAuthProvider,\n callback: McpOAuthCallback,\n opts: McpOAuthRequestOptions = {},\n): Promise<void> => {\n if (callback.error) {\n throw new Error(\n `Authorization denied by the server: ${callback.error}${\n callback.errorDescription ? ` - ${callback.errorDescription}` : ''\n }`,\n )\n }\n if (!callback.code) {\n throw new Error('Authorization callback carried no code')\n }\n if (!(await provider.verifyState(callback.state))) {\n throw new Error('Authorization state mismatch; refusing to exchange the code (possible CSRF)')\n }\n const result = await auth(provider, {\n serverUrl: provider.serverUrl,\n authorizationCode: callback.code,\n fetchFn: opts.fetch,\n })\n if (result !== 'AUTHORIZED') {\n throw new Error(`Token exchange did not complete (SDK returned \"${result}\")`)\n }\n}\n\n/**\n * Kick off (or silently renew) an authorization. Returns `'AUTHORIZED'` when a\n * usable token is in hand — including the case where a refresh token was\n * silently exchanged — or `'REDIRECT'` when the user has to visit\n * `provider.authorizationUrl`.\n */\nexport const beginMcpOAuth = async (\n provider: BrowserOAuthProvider,\n opts: McpOAuthRequestOptions = {},\n): Promise<'AUTHORIZED' | 'REDIRECT'> => {\n provider.authorizationUrl = undefined\n return auth(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch })\n}\n","export { connectMcpHttp, flattenContent } from './http.js'\n// Re-exported so a host can identify an authorization failure by identity.\n// The SDK's class does not set `name`, so `err.name === 'UnauthorizedError'`\n// is always false — string sniffing is not an option, and a host that only\n// depends on this package has no other handle on the class.\nexport { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'\nexport type {\n McpHttpServerConfig,\n McpCatalogEntry,\n McpServerResult,\n ConnectedMcp,\n ConnectMcpOptions,\n} from './http.js'\nexport {\n BrowserOAuthProvider,\n MemoryOAuthStorage,\n VaultOAuthStorage,\n beginMcpOAuth,\n finishMcpOAuth,\n readOAuthCallback,\n} from './oauth.js'\nexport type {\n BrowserOAuthProviderOptions,\n McpOAuthCallback,\n McpOAuthRequestOptions,\n McpOAuthStorage,\n} from './oauth.js'\n"],"mappings":"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","names":["auth","auth","UnauthorizedError"]}
1
+ {"version":3,"sources":["../src/mcp/http.ts","../src/mcp/oauth.ts","../src/mcp/index.ts"],"sourcesContent":["import {\n auth,\n UnauthorizedError,\n type OAuthClientProvider,\n} from '@modelcontextprotocol/sdk/client/auth.js'\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js'\nimport {\n StreamableHTTPClientTransport,\n type StreamableHTTPReconnectionOptions,\n} from '@modelcontextprotocol/sdk/client/streamableHttp.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { ToolListChangedNotificationSchema } from '@modelcontextprotocol/sdk/types.js'\nimport { dynamicTool, jsonSchema, type ToolSet } from 'ai'\nimport { assertSecureOAuthUrl } from './oauth.js'\n\n/**\n * A remote MCP server reached over StreamableHTTP. Browsers can ONLY speak the\n * HTTP transport — stdio (child processes) is Node-only and intentionally\n * unsupported here. This connector lives behind the `@dudko.dev/agent-web/mcp`\n * subpath so `@modelcontextprotocol/sdk` never enters the core bundle.\n *\n * Three ways to authenticate, in ascending order of capability:\n * 1. `headers` — a static token you already hold.\n * 2. `getHeaders` — resolved once per connect, for tokens you mint yourself.\n * 3. `authProvider` — full OAuth 2.1: the SDK discovers the authorization\n * server, dynamically registers the client (DCR), runs PKCE, and — the\n * part 1 and 2 cannot do — transparently REFRESHES the access token when\n * the server answers 401 mid-run. See `./oauth`.\n */\nexport interface McpHttpServerConfig {\n url: string\n headers?: Record<string, string>\n /** Called at connect time to provide fresh headers (e.g. a rotating Bearer). */\n getHeaders?: () => Promise<Record<string, string>> | Record<string, string>\n /**\n * OAuth client provider (see `./oauth` for a browser implementation). The\n * transport sends its access token, refreshes on 401 and retries the request.\n * Do NOT also set an `Authorization` entry in `headers`: request headers are\n * merged last and would shadow the OAuth token.\n */\n authProvider?: OAuthClientProvider\n /** Custom fetch for every request: proxying, instrumentation, retries. */\n fetch?: FetchLike\n /** Overrides for the SSE reconnect backoff. Merged over the defaults below. */\n reconnection?: Partial<StreamableHTTPReconnectionOptions>\n}\n\nexport interface McpCatalogEntry {\n name: string\n description: string\n server: string\n}\n\nexport interface McpServerResult {\n name: string\n connected: boolean\n error?: string\n /**\n * The server demands OAuth and we have no usable token. When the config\n * carried a `BrowserOAuthProvider`, its `authorizationUrl` now holds the page\n * to send the user to; reconnect once they are back.\n */\n needsAuthorization?: boolean\n}\n\nexport interface ConnectedMcp {\n /** Discovered tools, keyed by \"server__tool\", ready to merge into config.tools. */\n tools: ToolSet\n catalog: McpCatalogEntry[]\n /** Per-server connect outcome. */\n results: McpServerResult[]\n /**\n * Re-list one server's tools and rewrite its entries in `tools`/`catalog`.\n * Both are mutated in place, so anything holding a reference (including a\n * live agent config) sees the new set. Throws if the server isn't connected.\n */\n refreshServer: (name: string) => Promise<void>\n close: () => Promise<void>\n}\n\nexport interface ConnectMcpOptions {\n clientName?: string\n clientVersion?: string\n onLog?: (level: 'info' | 'warn' | 'error', message: string) => void\n /**\n * Fires when a server pushes `notifications/tools/list_changed`. Call\n * `refreshServer(name)` from a point where no run is reading `tools` — this\n * connector deliberately does not refresh behind a running agent's back.\n */\n onToolsChanged?: (server: string) => void\n}\n\n// Providers cap tool names at 64 chars (^[a-zA-Z0-9_-]{1,64}$). We enforce the\n// same limit on the prefixed \"server__tool\" so one server can't poison a run.\nconst MAX_TOOL_NAME_LEN = 64\n\n// Browsers drop long-lived SSE streams on tab-throttling and flaky networks far\n// more often than a server does; the SDK's default (2 retries) gives up too\n// early for an agent that may idle between steps.\nconst DEFAULT_RECONNECTION: StreamableHTTPReconnectionOptions = {\n maxReconnectionDelay: 30_000,\n initialReconnectionDelay: 1_000,\n reconnectionDelayGrowFactor: 1.5,\n maxRetries: 5,\n}\n\n// MCP names may contain characters providers reject (dots, slashes, spaces);\n// map them into the allowed alphabet. callTool still uses the ORIGINAL name.\nconst sanitizeName = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, '_')\n\n/**\n * Sanitizing can map two distinct MCP names onto one key (\"a.b\" and \"a-b\" both\n * become \"a_b\"). Suffix instead of dropping: a tool the model can't see is a\n * silent capability loss, and the disambiguated name still resolves back to the\n * original on dispatch.\n */\nconst uniqueKey = (base: string, taken: ToolSet): string => {\n // hasOwn, not truthiness: a tool named so that the key lands on \"__proto__\"\n // would otherwise read as already taken through the prototype chain.\n if (!Object.hasOwn(taken, base)) return base\n for (let n = 2; n < 100; n++) {\n const suffix = `_${n}`\n const candidate = base.slice(0, MAX_TOOL_NAME_LEN - suffix.length) + suffix\n if (!Object.hasOwn(taken, candidate)) return candidate\n }\n return ''\n}\n\n/**\n * Flatten an MCP tool result's content into something a model can read. Unlike\n * the Node sibling this NEVER spills to a filesystem (there isn't one in the\n * browser): all-text content is joined; mixed content passes through as an\n * array of text + raw parts for the host to handle.\n */\nexport const flattenContent = (content: unknown): unknown => {\n if (!Array.isArray(content)) return content\n const parts = content as Array<Record<string, unknown>>\n const allText =\n parts.length > 0 && parts.every((p) => p?.type === 'text' && typeof p.text === 'string')\n if (allText) return parts.map((p) => p.text as string).join('\\n')\n return parts.map((p) => (p?.type === 'text' && typeof p.text === 'string' ? p.text : p))\n}\n\ninterface McpToolDescriptor {\n name: string\n description?: string\n inputSchema: unknown\n}\n\ninterface ServerConnect {\n name: string\n client?: Client\n listed?: McpToolDescriptor[]\n error?: string\n needsAuthorization?: boolean\n}\n\n/** Thrown by the refresh-only view when the SDK reaches for a redirect. */\nclass RefreshOnlyAbort extends Error {}\n\n/**\n * A view of the provider that can renew a token but can never start an\n * interactive authorization.\n *\n * `auth()` does not have a refresh-only mode: when a refresh fails for a reason\n * it deems recoverable (a 5xx from the token endpoint, a network blip) it falls\n * through to a brand-new authorization — issuing a state and a PKCE verifier,\n * overwriting the stored pair, and calling `redirectToAuthorization`. The\n * transport then does its own `auth()` on the first 401, overwriting them\n * again, so the user comes back holding a code the second pair can't verify.\n * Blocking the three redirect-path methods aborts the escalation before\n * anything is overwritten; the transport's 401 handling stays the single owner\n * of interactive authorization.\n */\nconst refreshOnly = (p: OAuthClientProvider): OAuthClientProvider => ({\n // Must stay truthy: `auth()` reads a missing redirectUrl as \"non-interactive\n // grant\" and would try to fetch a token without one.\n get redirectUrl() {\n return p.redirectUrl\n },\n get clientMetadata() {\n return p.clientMetadata\n },\n clientInformation: () => p.clientInformation(),\n saveClientInformation: p.saveClientInformation?.bind(p),\n tokens: () => p.tokens(),\n saveTokens: (t) => p.saveTokens(t),\n invalidateCredentials: p.invalidateCredentials?.bind(p),\n addClientAuthentication: p.addClientAuthentication?.bind(p),\n validateResourceURL: p.validateResourceURL?.bind(p),\n codeVerifier: () => p.codeVerifier(),\n state: () => {\n throw new RefreshOnlyAbort()\n },\n saveCodeVerifier: () => {\n throw new RefreshOnlyAbort()\n },\n redirectToAuthorization: () => {\n throw new RefreshOnlyAbort()\n },\n})\n\n/**\n * An access token that is already expired produces a guaranteed 401 on the\n * first request. When the provider tells us so AND we hold a refresh token,\n * renew before connecting: one round-trip saved, and a run that starts on a\n * stale token doesn't surface a spurious auth error to the model.\n *\n * Best-effort by design — anything that goes wrong here is left to the\n * transport's own 401 handling, which is the path that knows how to ask the\n * user for a new authorization.\n */\nconst refreshIfExpired = async (\n provider: OAuthClientProvider,\n serverUrl: string,\n fetchFn: FetchLike | undefined,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n name: string,\n): Promise<void> => {\n const check = (provider as { isAccessTokenExpired?: () => boolean | Promise<boolean> })\n .isAccessTokenExpired\n if (typeof check !== 'function') return\n if (!(await check.call(provider))) return\n const tokens = await provider.tokens()\n // Nothing to refresh with: let the 401 path ask for a new authorization.\n if (!tokens?.refresh_token) return\n try {\n await auth(refreshOnly(provider), { serverUrl, fetchFn })\n } catch (err) {\n log(\n 'info',\n err instanceof RefreshOnlyAbort\n ? `[mcp] ${name}: the stored token could not be refreshed; a new authorization is needed`\n : `[mcp] ${name}: proactive token refresh failed (${\n err instanceof Error ? err.message : String(err)\n }); continuing`,\n )\n }\n}\n\n/**\n * A fetch the browser refused for CORS reasons is indistinguishable, from\n * JavaScript, from a server that is simply down: both surface as a bare\n * `TypeError`. The reason is deliberately withheld, and the browser prints it\n * only to the devtools console — which is why this failure is usually read as a\n * client bug and debugged in the wrong place for a long time.\n */\nconst isNetworkLevelFailure = (err: unknown): boolean =>\n err instanceof TypeError || /failed to fetch|networkerror|load failed/i.test(String(err))\n\n/**\n * Work out WHY the browser blocked us, by asking a question it will answer.\n *\n * A request carrying only `content-type` needs the same preflight as the real\n * one but asks for nothing else. So if the plain request gets through while the\n * transport's did not, the origin is fine and a *header* is the problem — and\n * the header the transport adds is `MCP-Protocol-Version`, which the spec has\n * required on every post-initialize request since 2025-06-18 and which server\n * CORS lists written before that date do not include. That is the shape of the\n * bug that lets OAuth and `initialize` succeed and then kills the connection\n * moments later, looking for all the world like the consent step failed.\n *\n * Exported because a UI wants to say this too, not just log it.\n */\nexport const diagnoseMcpCors = async (\n url: string,\n fetchFn: FetchLike = globalThis.fetch,\n): Promise<string | undefined> => {\n let plain: Response\n try {\n plain = await fetchFn(url, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ jsonrpc: '2.0', id: 0, method: 'ping' }),\n })\n } catch {\n return (\n `the browser blocked every request to ${url}, including a plain one. ` +\n 'The server is unreachable, or it does not answer the CORS preflight for this ' +\n 'origin: it must return Access-Control-Allow-Origin (and handle OPTIONS) on the ' +\n 'MCP endpoint, the OAuth endpoints and the /.well-known documents.'\n )\n }\n const hints = [\n `a plain request to ${url} succeeded (HTTP ${plain.status}) but the transport's did not, ` +\n 'so the origin is allowed and a request HEADER is being refused. Add ' +\n 'MCP-Protocol-Version (required on every request after initialize) and Last-Event-ID ' +\n \"to the server's Access-Control-Allow-Headers, alongside Content-Type, Authorization \" +\n 'and mcp-session-id.',\n ]\n // A 401 whose challenge JS cannot read means OAuth discovery cannot start\n // from a browser at all, so say so while we are here.\n if (plain.status === 401 && !plain.headers.get('www-authenticate')) {\n hints.push(\n 'The server also answers 401 without exposing WWW-Authenticate: add it to ' +\n 'Access-Control-Expose-Headers, or a browser client cannot read the challenge ' +\n 'and OAuth discovery never starts.',\n )\n }\n return hints.join(' ')\n}\n\nconst openConnection = async (\n name: string,\n cfg: McpHttpServerConfig,\n opts: ConnectMcpOptions,\n log: NonNullable<ConnectMcpOptions['onLog']>,\n): Promise<ServerConnect> => {\n let client: Client | undefined\n try {\n if (cfg.headers && cfg.getHeaders) {\n throw new Error(`MCP server \"${name}\": specify either headers or getHeaders, not both`)\n }\n const headers = cfg.getHeaders ? await cfg.getHeaders() : cfg.headers\n if (cfg.authProvider) {\n if (headers && 'Authorization' in headers) {\n log(\n 'warn',\n `[mcp] ${name}: an explicit Authorization header shadows the OAuth token from authProvider`,\n )\n }\n assertSecureOAuthUrl(cfg.url, name)\n await refreshIfExpired(cfg.authProvider, cfg.url, cfg.fetch, log, name)\n }\n const transport = new StreamableHTTPClientTransport(new URL(cfg.url), {\n requestInit: headers ? { headers } : undefined,\n authProvider: cfg.authProvider,\n fetch: cfg.fetch,\n reconnectionOptions: { ...DEFAULT_RECONNECTION, ...cfg.reconnection },\n })\n // Without these, a dropped SSE stream or a transport-level protocol error\n // is swallowed and the agent just stops getting results.\n transport.onerror = (err) => log('warn', `[mcp] ${name}: transport error - ${err.message}`)\n // info, not warn: an ordinary close() ends here too, and a routine\n // teardown logged as a warning trains people to ignore warnings.\n transport.onclose = () => log('info', `[mcp] ${name}: transport closed`)\n\n client = new Client({\n name: opts.clientName ?? 'agent-web',\n version: opts.clientVersion ?? '0.0.0',\n })\n await client.connect(transport)\n\n // Subscribe BEFORE the first list call: a server that mutates its tool set\n // during init would otherwise lose the notification in the window between\n // connect() and listTools().\n if (opts.onToolsChanged) {\n const notify = opts.onToolsChanged\n client.setNotificationHandler(ToolListChangedNotificationSchema, () => notify(name))\n }\n\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n return { name, client, listed }\n } catch (err) {\n // The client may be live even though we ended up here (listTools() failing\n // after a successful connect). Close it, or the SSE stream leaks for the\n // lifetime of the tab.\n if (client) await client.close().catch(() => {})\n const needsAuthorization = err instanceof UnauthorizedError\n let message = err instanceof Error ? err.message : String(err)\n // Turn \"TypeError: Failed to fetch\" into the sentence the reader needs.\n // Only on the failure path, so the extra request costs nothing in normal use.\n if (!needsAuthorization && isNetworkLevelFailure(err)) {\n const hint = await diagnoseMcpCors(cfg.url, cfg.fetch).catch(() => undefined)\n if (hint) message = `${message} - ${hint}`\n }\n log(\n 'error',\n needsAuthorization\n ? `[mcp] ${name}: authorization required - send the user to the provider's authorizationUrl`\n : `[mcp] ${name}: failed to connect - ${message}`,\n )\n return { name, error: message, needsAuthorization }\n }\n}\n\n/**\n * Connect to one or more HTTP MCP servers and return their tools + catalogue.\n * Servers connect concurrently; tools, catalogue, and results merge in the\n * caller's declaration order, so the outcome is deterministic.\n */\nexport const connectMcpHttp = async (\n servers: Record<string, McpHttpServerConfig>,\n opts: ConnectMcpOptions = {},\n): Promise<ConnectedMcp> => {\n const log = opts.onLog ?? (() => {})\n const clients = new Map<string, Client>()\n const tools: ToolSet = {}\n const catalog: McpCatalogEntry[] = []\n const results: McpServerResult[] = []\n // server name → the tool keys it currently owns, so a refresh can remove\n // exactly its own entries.\n const mountedKeys = new Map<string, string[]>()\n\n const mountTools = (name: string, client: Client, listed: McpToolDescriptor[]): void => {\n const prefix = `${sanitizeName(name)}__`\n // Drop this server's existing entries first, mutating in place so external\n // references (an agent's live ToolSet) stay valid. We delete the keys this\n // server actually mounted rather than everything matching its prefix:\n // sanitizing can make two server names share a prefix (\"docs.v1\" and\n // \"docs/v1\"), and one server's prefix can nest inside another's (\"a\" and\n // \"a__b\") — either way a prefix sweep would silently unmount a DIFFERENT\n // server's tools.\n for (const k of mountedKeys.get(name) ?? []) {\n delete tools[k]\n }\n for (let i = catalog.length - 1; i >= 0; i--) {\n if (catalog[i].server === name) catalog.splice(i, 1)\n }\n const keys: string[] = []\n\n let mounted = 0\n for (const t of listed) {\n const prefixed = prefix + sanitizeName(t.name)\n if (prefixed.length > MAX_TOOL_NAME_LEN) {\n log(\n 'warn',\n `[mcp] ${name}: tool \"${t.name}\" exceeds the ${MAX_TOOL_NAME_LEN}-char name limit; skipping`,\n )\n continue\n }\n const key = uniqueKey(prefixed, tools)\n if (!key) {\n log('warn', `[mcp] ${name}: cannot find a free name for tool \"${t.name}\"; skipping`)\n continue\n }\n if (key !== prefixed) {\n log('warn', `[mcp] ${name}: tool name \"${prefixed}\" already taken; mounted as \"${key}\"`)\n }\n const description = t.description ?? ''\n tools[key] = dynamicTool({\n description,\n inputSchema: jsonSchema(t.inputSchema as Parameters<typeof jsonSchema>[0]),\n execute: async (args, options) => {\n const res = await client.callTool(\n { name: t.name, arguments: (args ?? {}) as Record<string, unknown> },\n undefined,\n options?.abortSignal ? { signal: options.abortSignal } : undefined,\n )\n const flat = flattenContent(res.content)\n // An MCP failure is a NORMAL response with isError set — surface it\n // as a thrown error so both tool paths record a failed call instead\n // of feeding the error text to the model as a success.\n if ((res as { isError?: boolean }).isError) {\n throw new Error(typeof flat === 'string' ? flat : JSON.stringify(flat))\n }\n return flat\n },\n })\n catalog.push({ name: key, description, server: name })\n keys.push(key)\n mounted += 1\n }\n mountedKeys.set(name, keys)\n log('info', `[mcp] ${name}: ${mounted} tools mounted`)\n }\n\n const connects = await Promise.all(\n Object.entries(servers).map(([name, cfg]) => openConnection(name, cfg, opts, log)),\n )\n\n for (const server of connects) {\n if (!server.client || !server.listed) {\n results.push({\n name: server.name,\n connected: false,\n error: server.error,\n ...(server.needsAuthorization ? { needsAuthorization: true } : {}),\n })\n continue\n }\n clients.set(server.name, server.client)\n mountTools(server.name, server.client, server.listed)\n results.push({ name: server.name, connected: true })\n }\n\n return {\n tools,\n catalog,\n results,\n refreshServer: async (name: string) => {\n const client = clients.get(name)\n if (!client) throw new Error(`MCP server \"${name}\" is not connected`)\n const listed = (await client.listTools()).tools as McpToolDescriptor[]\n mountTools(name, client, listed)\n },\n close: async () => {\n await Promise.allSettled([...clients.values()].map((c) => c.close()))\n },\n }\n}\n","import { auth, type OAuthClientProvider } from '@modelcontextprotocol/sdk/client/auth.js'\nimport type {\n OAuthClientInformationMixed,\n OAuthClientMetadata,\n OAuthTokens,\n} from '@modelcontextprotocol/sdk/shared/auth.js'\nimport type { FetchLike } from '@modelcontextprotocol/sdk/shared/transport.js'\nimport { IndexedDBVault, type VaultOptions } from '../secrets/vault.js'\n\n/**\n * Browser-side OAuth 2.1 for remote MCP servers: Dynamic Client Registration\n * (RFC 7591), PKCE authorization-code flow, and refresh-token rotation.\n *\n * The heavy lifting lives in the MCP SDK — `auth()` discovers the protected\n * resource (RFC 9728), registers the client if we have no client_id yet,\n * exchanges the code, and refreshes expired tokens; `StreamableHTTPClient-\n * Transport` retries any 401 through it. This module supplies the missing\n * half: WHERE the tokens, the registration, and the PKCE verifier are kept,\n * and HOW the user is sent to the authorization server.\n *\n * ── WHY NO AUTOMATIC NAVIGATION ──\n * `redirectToAuthorization` deliberately does NOT navigate by default. The SDK\n * can call it from deep inside a tool call, and throwing the user out of a\n * running agent would discard in-flight state. Instead the URL is recorded on\n * `authorizationUrl`, `connectMcpHttp` reports `needsAuthorization: true`, and\n * the app decides when to send the user there (a button, a popup, a redirect).\n * Pass `onRedirect` to opt into eager navigation.\n */\n\n/** Where an OAuth provider keeps its tokens / registration / PKCE verifier. */\nexport interface McpOAuthStorage {\n get(key: string): Promise<string | undefined>\n set(key: string, value: string): Promise<void>\n delete(key: string): Promise<void>\n}\n\n/**\n * Default storage: the same encrypted IndexedDB vault that holds provider API\n * keys (AES-GCM, non-extractable key). Read the threat model in\n * `secrets/vault.ts` — this protects tokens at rest, not against active XSS.\n */\nexport class VaultOAuthStorage implements McpOAuthStorage {\n private readonly vault: IndexedDBVault\n\n constructor(opts: VaultOptions = {}) {\n this.vault = new IndexedDBVault(opts)\n }\n\n get(key: string): Promise<string | undefined> {\n return this.vault.getSecret(key)\n }\n set(key: string, value: string): Promise<void> {\n return this.vault.setSecret(key, value)\n }\n delete(key: string): Promise<void> {\n return this.vault.deleteSecret(key)\n }\n}\n\n/** Non-persistent storage for tests / SSR. Tokens die with the tab. */\nexport class MemoryOAuthStorage implements McpOAuthStorage {\n private readonly map = new Map<string, string>()\n\n async get(key: string): Promise<string | undefined> {\n return this.map.get(key)\n }\n async set(key: string, value: string): Promise<void> {\n this.map.set(key, value)\n }\n async delete(key: string): Promise<void> {\n this.map.delete(key)\n }\n}\n\nexport interface BrowserOAuthProviderOptions {\n /** The MCP endpoint this provider authorizes against (the resource server). */\n serverUrl: string\n /**\n * Where the authorization server sends the user back. Must be a page of your\n * app that calls `readOAuthCallback()` + `finishMcpOAuth()`. Registered with\n * the AS via DCR, so it has to match byte-for-byte on the way back.\n */\n redirectUrl: string | URL\n /** `client_name` submitted during dynamic registration. */\n clientName?: string\n /** Fallback scope when the server's metadata advertises none. */\n scope?: string\n /** Merged over the defaults; use it for `software_id`, `contacts`, … */\n clientMetadata?: Partial<OAuthClientMetadata>\n /** Defaults to the encrypted IndexedDB vault. */\n storage?: McpOAuthStorage\n /** Treat an access token as expired this many seconds early. Default 30. */\n expirySkewSeconds?: number\n /** Called when the SDK wants the user at the authorization server. */\n onRedirect?: (url: URL) => void | Promise<void>\n}\n\ninterface StoredTokens {\n tokens: OAuthTokens\n /** Absolute ms timestamp derived from `expires_in` at save time. */\n expiresAt?: number\n}\n\nconst DEFAULT_SKEW_SECONDS = 30\n\n// Namespacing by origin+path+query keeps two MCP servers on the same host\n// (/mcp/docs vs /mcp/crm, or ?tenant=a vs ?tenant=b) from sharing a\n// registration or a token — one tenant's bearer must never be sent to another.\nconst namespaceFor = (serverUrl: string): string => {\n const u = new URL(serverUrl)\n return `mcp-oauth:${u.origin}${u.pathname.replace(/\\/+$/, '')}${u.search}`\n}\n\nconst LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1'])\n\n/**\n * OAuth over plaintext hands the bearer token to anyone on the path, so only\n * https — or a loopback host, where there is no network to sniff — is allowed.\n * Parsed rather than pattern-matched: `http://localhost.attacker.example/` is a\n * registrable host that a prefix match would wave through.\n */\nexport const assertSecureOAuthUrl = (url: string, label = 'MCP server'): void => {\n let parsed: URL\n try {\n parsed = new URL(url)\n } catch {\n throw new Error(`${label}: \"${url}\" is not a valid URL`)\n }\n if (parsed.protocol === 'https:' || LOOPBACK_HOSTS.has(parsed.hostname)) return\n throw new Error(\n `${label}: MCP OAuth requires https (or a loopback host) - refusing to send tokens to \"${url}\"`,\n )\n}\n\nconst randomToken = (): string => {\n const bytes = new Uint8Array(32)\n globalThis.crypto.getRandomValues(bytes)\n return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\n/**\n * An `OAuthClientProvider` (MCP SDK) that persists everything it is handed and\n * never navigates behind your back. One instance per MCP server.\n */\nexport class BrowserOAuthProvider implements OAuthClientProvider {\n /** Set when the SDK asks for an authorization redirect; read it in your UI. */\n authorizationUrl?: URL\n\n readonly serverUrl: string\n\n private readonly storage: McpOAuthStorage\n private readonly ns: string\n private readonly _redirectUrl: string\n private readonly _clientMetadata: OAuthClientMetadata\n private readonly skewMs: number\n private readonly onRedirect?: (url: URL) => void | Promise<void>\n\n constructor(opts: BrowserOAuthProviderOptions) {\n assertSecureOAuthUrl(opts.serverUrl)\n this.serverUrl = opts.serverUrl\n this.storage = opts.storage ?? new VaultOAuthStorage()\n this.ns = namespaceFor(opts.serverUrl)\n this._redirectUrl = String(opts.redirectUrl)\n this.skewMs = (opts.expirySkewSeconds ?? DEFAULT_SKEW_SECONDS) * 1000\n this.onRedirect = opts.onRedirect\n this._clientMetadata = {\n client_name: opts.clientName ?? 'agent-web',\n redirect_uris: [this._redirectUrl],\n grant_types: ['authorization_code', 'refresh_token'],\n response_types: ['code'],\n // Public client: a browser cannot hold a client_secret. PKCE is what\n // actually protects the code exchange.\n token_endpoint_auth_method: 'none',\n ...(opts.scope ? { scope: opts.scope } : {}),\n ...opts.clientMetadata,\n } as OAuthClientMetadata\n }\n\n get redirectUrl(): string {\n return this._redirectUrl\n }\n\n get clientMetadata(): OAuthClientMetadata {\n return this._clientMetadata\n }\n\n private key(suffix: string): string {\n return `${this.ns}:${suffix}`\n }\n\n private async readJSON<T>(suffix: string): Promise<T | undefined> {\n // The read itself is inside the guard on purpose: the default storage\n // decrypts, and an AES-GCM blob written under a key that no longer exists\n // (a partially cleared IndexedDB, a rotated vault key) throws. Treating\n // that as \"no value\" lets the flow re-authorize instead of every call\n // rejecting with an opaque OperationError forever.\n try {\n const raw = await this.storage.get(this.key(suffix))\n if (raw === undefined) return undefined\n return JSON.parse(raw) as T\n } catch {\n await this.storage.delete(this.key(suffix)).catch(() => {})\n return undefined\n }\n }\n\n async state(): Promise<string> {\n const value = randomToken()\n await this.storage.set(this.key('state'), value)\n return value\n }\n\n /**\n * CSRF check for the redirect back. Single-use: the stored value is dropped\n * whether or not it matched, so a replayed callback can't pass twice.\n */\n async verifyState(returned: string | undefined): Promise<boolean> {\n const expected = await this.storage.get(this.key('state'))\n await this.storage.delete(this.key('state'))\n return expected !== undefined && returned !== undefined && expected === returned\n }\n\n async clientInformation(): Promise<OAuthClientInformationMixed | undefined> {\n return this.readJSON<OAuthClientInformationMixed>('client')\n }\n\n async saveClientInformation(info: OAuthClientInformationMixed): Promise<void> {\n await this.storage.set(this.key('client'), JSON.stringify(info))\n }\n\n async tokens(): Promise<OAuthTokens | undefined> {\n return (await this.readJSON<StoredTokens>('tokens'))?.tokens\n }\n\n async saveTokens(tokens: OAuthTokens): Promise<void> {\n const stored: StoredTokens = {\n tokens,\n expiresAt:\n typeof tokens.expires_in === 'number' ? Date.now() + tokens.expires_in * 1000 : undefined,\n }\n await this.storage.set(this.key('tokens'), JSON.stringify(stored))\n }\n\n async saveCodeVerifier(verifier: string): Promise<void> {\n await this.storage.set(this.key('verifier'), verifier)\n }\n\n async codeVerifier(): Promise<string> {\n const v = await this.storage.get(this.key('verifier'))\n if (!v) {\n throw new Error(\n 'No PKCE code verifier stored: the authorization was started in a different browser profile, or storage was cleared mid-flow',\n )\n }\n return v\n }\n\n async redirectToAuthorization(url: URL): Promise<void> {\n this.authorizationUrl = url\n await this.onRedirect?.(url)\n }\n\n /**\n * Called by the SDK when the server rejects what we hold: `'tokens'` after an\n * `invalid_grant` (dead refresh token), `'client'` / `'all'` after an\n * `invalid_client` (the AS forgot our dynamic registration). Clearing lets\n * the SDK re-register and re-authorize without the user clearing site data.\n */\n async invalidateCredentials(\n scope: 'all' | 'client' | 'tokens' | 'verifier' | 'discovery',\n ): Promise<void> {\n const targets = scope === 'all' ? ['tokens', 'client', 'verifier', 'state'] : [scope]\n await Promise.all(targets.map((t) => this.storage.delete(this.key(t))))\n }\n\n /** True when there is no access token, or it expires within the skew window. */\n async isAccessTokenExpired(): Promise<boolean> {\n const stored = await this.readJSON<StoredTokens>('tokens')\n if (!stored?.tokens?.access_token) return true\n // No expires_in means the AS didn't say; assume it is still good and let a\n // 401 drive the refresh.\n if (stored.expiresAt === undefined) return false\n return Date.now() + this.skewMs >= stored.expiresAt\n }\n\n /** True once an authorization has completed for this server. */\n async isAuthorized(): Promise<boolean> {\n return (await this.tokens()) !== undefined\n }\n\n /** Forget tokens, registration, verifier and state for this server. */\n async reset(): Promise<void> {\n this.authorizationUrl = undefined\n await this.invalidateCredentials('all')\n }\n}\n\nexport interface McpOAuthRequestOptions {\n /**\n * Custom fetch for the discovery / registration / token requests. Pass the\n * same one you give the connector so a proxy or an instrumentation wrapper\n * covers the OAuth traffic too.\n */\n fetch?: FetchLike\n}\n\nexport interface McpOAuthCallback {\n code?: string\n state?: string\n error?: string\n errorDescription?: string\n}\n\n/**\n * Pull the authorization-code parameters out of a redirect URL. Reads the query\n * string and, for hash-routed apps, the fragment. Returns undefined when the\n * URL carries neither a `code` nor an `error`, so it is safe to call on every\n * page load.\n */\nexport const readOAuthCallback = (input?: string | URL): McpOAuthCallback | undefined => {\n const href = input\n ? String(input)\n : typeof globalThis.location !== 'undefined'\n ? globalThis.location.href\n : undefined\n if (!href) return undefined\n let url: URL\n try {\n url = new URL(href)\n } catch {\n return undefined\n }\n const params = new URLSearchParams(url.search)\n const hash = url.hash.startsWith('#') ? url.hash.slice(1) : url.hash\n // Hash-routed apps get \"#/callback?code=…\"; take whatever follows the \"?\".\n const q = hash.indexOf('?')\n if (q >= 0) {\n for (const [k, v] of new URLSearchParams(hash.slice(q + 1))) {\n if (!params.has(k)) params.set(k, v)\n }\n }\n const code = params.get('code') ?? undefined\n const error = params.get('error') ?? undefined\n if (!code && !error) return undefined\n return {\n code,\n state: params.get('state') ?? undefined,\n error,\n errorDescription: params.get('error_description') ?? undefined,\n }\n}\n\n/**\n * Finish the flow after the authorization server redirected back: verify the\n * `state`, exchange the code (PKCE) and persist the tokens. Throws when the AS\n * returned an error, when `state` doesn't match what we issued, or when the\n * exchange fails — never resolves on a half-finished authorization.\n */\nexport const finishMcpOAuth = async (\n provider: BrowserOAuthProvider,\n callback: McpOAuthCallback,\n opts: McpOAuthRequestOptions = {},\n): Promise<void> => {\n if (callback.error) {\n throw new Error(\n `Authorization denied by the server: ${callback.error}${\n callback.errorDescription ? ` - ${callback.errorDescription}` : ''\n }`,\n )\n }\n if (!callback.code) {\n throw new Error('Authorization callback carried no code')\n }\n if (!(await provider.verifyState(callback.state))) {\n throw new Error('Authorization state mismatch; refusing to exchange the code (possible CSRF)')\n }\n const result = await auth(provider, {\n serverUrl: provider.serverUrl,\n authorizationCode: callback.code,\n fetchFn: opts.fetch,\n })\n if (result !== 'AUTHORIZED') {\n throw new Error(`Token exchange did not complete (SDK returned \"${result}\")`)\n }\n}\n\n/**\n * Kick off (or silently renew) an authorization. Returns `'AUTHORIZED'` when a\n * usable token is in hand — including the case where a refresh token was\n * silently exchanged — or `'REDIRECT'` when the user has to visit\n * `provider.authorizationUrl`.\n */\nexport const beginMcpOAuth = async (\n provider: BrowserOAuthProvider,\n opts: McpOAuthRequestOptions = {},\n): Promise<'AUTHORIZED' | 'REDIRECT'> => {\n provider.authorizationUrl = undefined\n return auth(provider, { serverUrl: provider.serverUrl, fetchFn: opts.fetch })\n}\n","export { connectMcpHttp, diagnoseMcpCors, flattenContent } from './http.js'\n// Re-exported so a host can identify an authorization failure by identity.\n// The SDK's class does not set `name`, so `err.name === 'UnauthorizedError'`\n// is always false — string sniffing is not an option, and a host that only\n// depends on this package has no other handle on the class.\nexport { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js'\nexport type {\n McpHttpServerConfig,\n McpCatalogEntry,\n McpServerResult,\n ConnectedMcp,\n ConnectMcpOptions,\n} from './http.js'\nexport {\n BrowserOAuthProvider,\n MemoryOAuthStorage,\n VaultOAuthStorage,\n beginMcpOAuth,\n finishMcpOAuth,\n readOAuthCallback,\n} from './oauth.js'\nexport type {\n BrowserOAuthProviderOptions,\n McpOAuthCallback,\n McpOAuthRequestOptions,\n McpOAuthStorage,\n} from './oauth.js'\n"],"mappings":"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","names":["auth","auth","UnauthorizedError"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dudko.dev/agent-web",
3
- "version": "0.0.12",
4
- "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.",
3
+ "version": "0.0.13",
4
+ "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 → execute → replan → synthesize loop. UI-agnostic.",
5
5
  "type": "module",
6
6
  "sideEffects": false,
7
7
  "main": "./dist/index.cjs",