# API key (/docs/authentication/api-key) Use `apiKey()` for internal or development gates only. It validates a header and does not set `ctx.user`. This is not MCP OAuth. ```ts import { defineConfig } from "bitmcp"; import { apiKey } from "bitmcp/auth/api-key"; export default defineConfig({ auth: apiKey({ env: "API_KEY" }), }); ``` Clients send the key in the `x-api-key` header by default. ## Options [#options] ```ts apiKey(options?: { env?: string; headerName?: string; validate?: (key: string) => boolean | Promise; }) ``` | Option | Default | Description | | ------------ | --------------------- | -------------------------- | | `env` | `"API_KEY"` | Environment variable name | | `headerName` | `"x-api-key"` | Request header to read | | `validate` | compares to env value | Custom validation function | For production user identity, use an OAuth provider from [Supabase](/docs/authentication/supabase), [Auth0](/docs/authentication/auth0), [Better Auth](/docs/authentication/better-auth), or [Custom](/docs/authentication/custom). # Auth0 (/docs/authentication/auth0) Use this when Auth0 is your authorization server. MCP clients register directly with Auth0 and send Auth0 access tokens to your MCP server. ### Configure Auth0 [#configure-auth0] In the Auth0 dashboard: 1. Create an **API** with an identifier you will use as the token audience (for example `https://mcp.example.com/mcp`) 2. Enable **Dynamic Client Registration** for that API if required by your tenant 3. Note your tenant domain (for example `your-tenant.us.auth0.com`) The API identifier should match your public MCP URL or the `resource` you configure in bitmcp. ### Set environment variables [#set-environment-variables] ```bash AUTH0_DOMAIN=your-tenant.us.auth0.com AUTH0_AUDIENCE=https://mcp.example.com/mcp MCP_URL=https://mcp.example.com/mcp ``` `AUTH0_AUDIENCE` is used as the OAuth resource when you do not pass `resource` in the plugin options. ### Configure bitmcp [#configure-bitmcp] ```ts import { defineConfig } from "bitmcp"; import { auth0 } from "bitmcp/oauth/auth0"; export default defineConfig({ http: { path: "/mcp", allowedHosts: ["mcp.example.com"], }, auth: auth0(), }); ``` Or inline: ```ts auth: auth0({ domain: "https://your-tenant.us.auth0.com", resource: "https://mcp.example.com/mcp", }), ``` ### Use ctx.user in tools [#use-ctxuser-in-tools] ```ts async execute(_input, ctx) { return { id: ctx.user!.id, email: ctx.user!.email, roles: ctx.user!.roles, }; } ``` Auth0 roles from the token are available on `ctx.user.roles` when Auth0 includes them in the access token. ### Verify [#verify] 1. Start the server with `pnpm dev` or `pnpm start` 2. Connect from an OAuth-capable MCP client 3. Complete Auth0 login and client registration 4. Call a tool and confirm `ctx.user` is populated 5. Call `/mcp` without a token and confirm `401` ## Options [#options] ```ts auth0(options?: { domain?: URL | string; resource?: URL | string; requiredScopes?: string[]; scopesSupported?: string[]; resourceName?: string; }) ``` | Variable | Purpose | | ---------------- | ----------------------------------------- | | `AUTH0_DOMAIN` | Auth0 tenant domain | | `AUTH0_AUDIENCE` | Token audience and default OAuth resource | | `MCP_URL` | Canonical public MCP URL in production | `ctx.user` fields: `id`, `email`, `name`, `nickname`, `picture`, `emailVerified`, `updatedAt`, `roles`. # Better Auth (/docs/authentication/better-auth) Use this when Better Auth's OAuth 2.1 plugin is your authorization server. Better Auth owns registration, authorization, consent, and token issuance. bitmcp verifies JWT access tokens against Better Auth's JWKS endpoint. ### Configure Better Auth [#configure-better-auth] In your Better Auth app: 1. Enable the **OAuth 2.1 Provider** plugin 2. Expose the issuer URL including the auth base path (for example `https://app.example.com/api/auth`) 3. Confirm JWKS is served at `{authURL}/jwks` ### Set environment variables [#set-environment-variables] ```bash BETTER_AUTH_URL=https://app.example.com/api/auth MCP_URL=https://mcp.example.com/mcp ``` ### Configure bitmcp [#configure-bitmcp] ```ts import { defineConfig } from "bitmcp"; import { betterAuth } from "bitmcp/oauth/better-auth"; export default defineConfig({ http: { path: "/mcp", allowedHosts: ["mcp.example.com"], }, auth: betterAuth(), }); ``` Or inline: ```ts auth: betterAuth({ authURL: "https://app.example.com/api/auth", }), ``` ### Use ctx.user in tools [#use-ctxuser-in-tools] ```ts async execute(_input, ctx) { return { id: ctx.user!.id, email: ctx.user!.email, sessionId: ctx.user!.sessionId, }; } ``` ### Verify [#verify] 1. Start your Better Auth app and the bitmcp server 2. Connect from an OAuth-capable MCP client 3. Complete login through Better Auth 4. Confirm authenticated tools receive `ctx.user` 5. Confirm unauthenticated requests return `401` ## Options [#options] ```ts betterAuth(options?: { authURL?: URL | string; resource?: URL | string; requiredScopes?: string[]; scopesSupported?: string[]; resourceName?: string; }) ``` | Variable | Purpose | | ----------------- | -------------------------------------- | | `BETTER_AUTH_URL` | Better Auth issuer base URL | | `MCP_URL` | Canonical public MCP URL in production | `ctx.user` fields: `id`, `email`, `name`, `picture`, `emailVerified`, `sessionId`, `isAnonymous`, `roles`. # Custom (/docs/authentication/custom) Use a custom provider when your identity provider is not covered by a built-in plugin, you need token introspection instead of JWT verification, or claim mapping differs from the defaults. ## JWT identity providers [#jwt-identity-providers] Use `jwtOAuthProvider()` with a short spec: ```ts import { defineConfig } from "bitmcp"; import { jwtOAuthProvider } from "bitmcp/oauth"; export default defineConfig({ auth: jwtOAuthProvider({ name: "my-provider", resolveIssuer: () => "https://auth.example.com", jwksUrl: (issuer) => new URL(`${issuer}/.well-known/jwks.json`), oauthMetadata: (issuer) => ({ issuer, authorization_endpoint: `${issuer}/oauth/authorize`, token_endpoint: `${issuer}/oauth/token`, registration_endpoint: `${issuer}/oauth/register`, response_types_supported: ["code"], grant_types_supported: ["authorization_code", "refresh_token"], code_challenge_methods_supported: ["S256"], }), mapUser: (payload) => ({ id: String(payload.sub), email: typeof payload.email === "string" ? payload.email : undefined, }), audience: "https://mcp.example.com/mcp", }), }); ``` ## Full control with custom() [#full-control-with-custom] Use `custom()` when verification logic is not a standard JWT flow: ```ts import { custom, createJwtVerifier } from "bitmcp/oauth"; auth: custom({ createTokenVerifier: (resource) => createJwtVerifier({ issuer: "https://auth.example.com", jwksUrl: new URL("https://auth.example.com/.well-known/jwks.json"), resource, audience: resource.href, }), oauthMetadata: { issuer: "https://auth.example.com", authorization_endpoint: "https://auth.example.com/oauth/authorize", token_endpoint: "https://auth.example.com/oauth/token", response_types_supported: ["code"], }, mapUser: (authInfo) => ({ id: String(authInfo.extra?.payload?.sub ?? authInfo.clientId), }), }); ``` ## Resource binding [#resource-binding] Tokens must target your MCP server. In production set `MCP_URL` to the same URL you use as the OAuth resource (for example `https://mcp.example.com/mcp`). You can also pass `resource` on the provider options. OAuth requires HTTP. `app.stdio()` throws when `auth` is set. ## Options [#options] ```ts custom(options: { createTokenVerifier: (resource: URL) => OAuthTokenVerifier; oauthMetadata: OAuthMetadata; mapUser: (authInfo: AuthInfo) => BitmcpUser; resource?: URL | string; requiredScopes?: string[]; scopesSupported?: string[]; resourceName?: string; }) ``` | Variable | Purpose | | ------------------ | ------------------------------------------ | | `MCP_URL` | Canonical public MCP URL in production | | `BITMCP_STATE_KEY` | MRTR state when using confirm or `ctx.ask` | Options passed to a factory override environment variables. # Supabase (/docs/authentication/supabase) Use this when Supabase Auth is your authorization server. Supabase handles login, consent, client registration, and token issuance. bitmcp verifies access tokens and maps claims to `ctx.user`. ### Enable Supabase OAuth [#enable-supabase-oauth] In the Supabase dashboard: 1. Open **Authentication → Sign In / Providers → OAuth Server** 2. Enable the **OAuth 2.1 server** 3. Enable **Allow Dynamic OAuth Apps** so MCP clients can register 4. Set the consent URL to a route your app implements (for example `http://localhost:3000/auth/consent`) 5. Enable at least one sign-in method Copy your **Project ID** (or full project URL for local/self-hosted Supabase). ### Set environment variables [#set-environment-variables] ```bash SUPABASE_PROJECT_ID=your-project-id MCP_URL=https://mcp.example.com/mcp BITMCP_STATE_KEY=... ``` Set `MCP_URL` to your public MCP endpoint in production. Set `BITMCP_STATE_KEY` if you use confirm or `ctx.ask`. ### Configure bitmcp [#configure-bitmcp] ```ts import { defineConfig } from "bitmcp"; import { supabase } from "bitmcp/oauth/supabase"; export default defineConfig({ name: "notes", http: { path: "/mcp", allowedHosts: ["mcp.example.com"], }, auth: supabase(), }); ``` Or pass options explicitly: ```ts auth: supabase({ projectId: process.env.SUPABASE_PROJECT_ID!, supabaseUrl: "http://127.0.0.1:54321", }), ``` ### Use ctx.user in tools [#use-ctxuser-in-tools] ```ts export default defineTool({ description: "List notes for the signed-in user", async execute(_input, ctx) { const userId = ctx.user!.id; return { userId }; }, }); ``` `ctx.user` includes `id`, `email`, `name`, and other Supabase claims when present in the token. ### Verify [#verify] Run the server and connect with an OAuth-capable MCP client. Confirm the client discovers OAuth metadata, login completes through Supabase, authenticated tool calls include `ctx.user`, and requests without a Bearer token return `401`. ## Row Level Security [#row-level-security] To query Supabase as the authenticated user, create a Supabase client in your tool using the access token from the MCP session. bitmcp does not put the raw token on `ctx.user`; read it from your auth layer or pass data scoped by `ctx.user.id` in application code. For server-owned operations, use the Supabase service role outside the user context. ## Options [#options] ```ts supabase(options?: { projectId?: string; supabaseUrl?: URL | string; jwtSecret?: string; audience?: string; resource?: URL | string; requiredScopes?: string[]; scopesSupported?: string[]; resourceName?: string; }) ``` | Variable | When | | --------------------- | ---------------------------------------------------- | | `SUPABASE_PROJECT_ID` | Default project id | | `SUPABASE_URL` | Local or self-hosted Supabase instead of `projectId` | | `SUPABASE_JWT_SECRET` | Legacy HS256 tokens (32+ bytes) | | `MCP_URL` | Canonical public MCP URL in production | `ctx.user` fields: `id`, `email`, `name`, `fullName`, `username`, `avatarUrl`, `role`, `aal`, `amr`, `sessionId`. Options passed to the factory override environment variables. # Actions (/docs/core-concepts/actions) Actions are backing tools the View can call. They are hidden from the model. ```ts import { z } from "zod"; import { defineAction } from "bitmcp"; export const refresh = defineAction({ description: "Reload forecast points", input: z.object({ city: z.string() }), async execute({ city }) { return { city, summary: `Updated forecast for ${city}`, points: [] }; }, }); ``` `defineAction` is `defineTool` with `visibility: ["app"]` and `kind: "action"`. It accepts the same options as `defineTool`. ## Naming [#naming] Place named exports in `actions.ts` beside the tool: ``` src/tools/forecast/ tool.ts view.tsx actions.ts export const refresh = defineAction(...) ``` The registered name is `forecast.refresh`. ## From the View [#from-the-view] ```tsx callTool("forecast.refresh", { city: result.city }); ``` Each call is a new stateless `tools/call`. Pass handles or ids explicitly when resuming work. ## Why actions exist [#why-actions-exist] Iframe clicks should not pollute `tools/list` with UI-only helpers. The model sees the main tool. The View sees refresh, filter, or pagination helpers. This uses the official Apps visibility field, not a bitmcp invention. # Confirmations (/docs/core-concepts/confirmations) Destructive or sensitive tools declare confirmation outside `execute`. ```ts export default defineTool({ description: "Delete rows matching a query", input: z.object({ query: z.string() }), async confirm({ query }) { const plan = planDelete(query); return { message: `Delete ${plan.rows} rows matching ${query}?`, preview: plan, }; }, async execute({ query }) { const plan = planDelete(query); return result(plan, `Deleted ${plan.rows} rows.`); }, }); ``` ## MRTR [#mrtr] `confirm` maps to `resultType: "input_required"` (MRTR). * Text-only hosts see `message` * UI hosts may render `preview` from the MRTR `structuredContent` * `execute` runs once, after accept * The mutation does not run on the first call and does not replay ## Example [#example] The `examples/confirm` app deletes rows only after MRTR acceptance. Try the tool in a text-only client to verify MRTR without a View: ```bash cd examples/confirm pnpm dev ``` ## ctx.ask [#ctxask] For mid-flight fields, use a continuation inside `execute`: ```ts async execute(input, ctx) { const { note } = await ctx.ask(z.object({ note: z.string().describe("Why are you deleting these rows?"), })); return applyDelete(input.query, note); } ``` `ctx.ask` re-enters `execute` on a new request with `inputResponses`. Do not mutate before `ask`. Prefer `confirm` for yes/no gates. Raw `ctx.inputRequired` remains an escape hatch. ## Never View-only [#never-view-only] A View must not be the only confirm path for a side effect. MRTR works in terminal hosts. Views can show previews, not replace the gate. # Handles (/docs/core-concepts/handles) bitmcp is stateless. There is no `Mcp-Session-Id` and no sticky server memory. ```ts import { memoryHandleStore, type HandleStore } from "bitmcp"; type Job = { status: string; progress: number; source: string }; export const jobs: HandleStore = memoryHandleStore(); async execute({ jobId, source }, ctx) { const id = jobId ?? ctx.handle(); const existing = await jobs.get(id); if (!existing) { await jobs.set(id, { status: "running", progress: 0, source }); } const job = (await jobs.get(id)) ?? { status: "running", progress: 0, source }; return { jobId: id, ...job }; } ``` ## ctx.handle() [#ctxhandle] Mints an opaque id (UUID). Store whatever you need under that id with a `HandleStore`: * `memoryHandleStore()` in development * KV, Redis, or D1 in production, same `{ get, set }` shape The model and the View pass `jobId` back as a normal argument on the next call. ## Stateless requests [#stateless-requests] Every HTTP request builds a fresh MCP server. Tool code must assume the next call may hit another replica. Handles make that safe. Sessions do not. ## Long work [#long-work] Return a handle immediately. Let the View poll status via a backing tool. Each poll is a new stateless `tools/call`. `callTool` updates `useApp().result` from `structuredContent`. The `examples/task` app starts a long ingest job and returns a handle right away: ```bash cd examples/task pnpm dev ``` Production stores must implement the same `HandleStore` `{ get, set }` shape against KV, Redis, or D1. The in-memory adapter is for a single process. ## What this proves [#what-this-proves] * No open SSE session * Handles survive restarts when backed by storage you provide * Stateless replicas can serve status checks with the same `jobId` arg # Tools (/docs/core-concepts/tools) A tool is a server function exposed to the model through MCP. ```ts import { z } from "zod"; import { defineTool } from "bitmcp"; export default defineTool({ description: "Get a forecast and show an interactive chart", input: z.object({ city: z.string() }), output: z.object({ city: z.string(), summary: z.string(), points: z.array(z.object({ t: z.string(), temp: z.number() })), }), async execute({ city }) { return { city, summary: `Mild in ${city}`, points: [] }; }, }); ``` ## Return data [#return-data] Returning an object produces `structuredContent` for the View and text for every host (generated summary or JSON). Custom text: ```ts import { result } from "bitmcp"; return result(data, `Forecast for ${city}: ${data.summary}`); ``` ## Schemas [#schemas] * `input` validates tool arguments (Zod or Standard Schema) * `output` validates the return value and types the View result ## CSP allowlists [#csp-allowlists] Default CSP is deny-all for iframe network access. * `connect` — `connect-src` allowlist for fetch from the View * `resources` — asset src allowlist for CDN scripts or styles ## Visibility [#visibility] Defaults to `["model", "app"]`. Model-visible tools appear in `tools/list`. Use [Actions](/docs/core-concepts/actions) for app-only tools. ## Rules [#rules] * `execute` runs on the server only * Text is always produced * Presence of `view.tsx` attaches UI. Do not set `resourceUri` manually * `execute` must not return `undefined` ## defineTool options [#definetool-options] ```ts defineTool({ name?: string; description: string; input?: Schema; output?: Schema; connect?: string[]; resources?: string[]; visibility?: ("model" | "app")[]; annotations?: { readOnlyHint?: boolean; destructiveHint?: boolean; idempotentHint?: boolean; openWorldHint?: boolean; }; confirm?: (input, ctx) => ConfirmResult | string; html?: string; execute: (input, ctx) => Promise> | TOutput | Result; }); ``` Happy-path fields: `description`, `input`, `execute`. `confirm` sets `destructiveHint: true` unless you override `annotations`. ## ToolContext [#toolcontext] | Field | Description | | ----------------------- | ---------------------------------------- | | `ask(schema, message?)` | MRTR continuation inside `execute` | | `inputRequired(spec)` | Raw MRTR escape hatch | | `inputResponses` | Answers from a prior `ask` | | `handle()` | Mint opaque handle id | | `user` | Authenticated user when OAuth is enabled | | `request` | Incoming HTTP request or stdio envelope | When OAuth is enabled and the request is authenticated: ```ts ctx.user?: { id: string; email?: string; name?: string; [key: string]: unknown; } ``` ## Result helpers [#result-helpers] ```ts result(data, text): Result isResult(value): boolean ``` `ConfirmResult` shape: ```ts type ConfirmResult = { message: string; preview?: unknown; }; ``` `preview` is included on the MRTR `structuredContent` so a View can render it. Text clients still see `message`. # Views (/docs/core-concepts/views) A View is a React component compiled into a predeclared HTML resource. ```tsx import { useApp } from "bitmcp/react"; export default function Forecast() { const { result, callTool } = useApp(); if (!result) return null; return (

{result.city}

); } ``` ## ui:// resources [#ui-resources] The compiler bundles `view.tsx` with esbuild into one HTML document with MIME type `text/html;profile=mcp-app`. * URI shape: `ui://tools//` * Content-addressed: the hash is the compiled HTML (scripts and CSS inlined), not the TSX source * Cacheable with `ttlMs` and `cacheScope: public` ## HTML shell [#html-shell] The compiler injects: * Official Apps `useApp({ appInfo })` * Host theme via `useHostStyles` * Tailwind v4 CSS for classes used in that View * Typed `result` from the sibling tool Authors never pass `appInfo` or call `useHostStyles` on the happy path. The compiler injects `AppShell` in the HTML shell. Authors do not render it in `view.tsx`. ## Data flow [#data-flow] 1. Host calls the tool 2. `structuredContent` arrives via `ontoolresult` 3. Host loads the hashed HTML in a sandbox iframe 4. View reads `result` from `useApp()` ## useApp() [#useapp] Import from `bitmcp/react`: ```tsx const { result, callTool, app, isConnected, error } = useApp(); ``` File-based tools generate `.bitmcp/views.d.ts` so `result` is inferred from the sibling tool. Pass a generic only when you need to override that. | Field | Description | | ----------------------- | ------------------------------------------------------------------------ | | `result` | `structuredContent` from the host via `ontoolresult`, or from `callTool` | | `callTool(name, args?)` | Calls a backing tool through the host and updates `result` | | `app` | Official Apps client instance | | `isConnected` | Connection state | | `error` | Connection error if any | `callTool` keeps a stable function identity so poll intervals do not reset every render. ## callTool [#calltool] Views never call your server directly. `callTool` goes through the host, which issues a new stateless `tools/call`. Network from the iframe is CSP-gated. Tool calls are not. ## Styling [#styling] Every View is compiled with Tailwind v4. Use `className` utilities. You do not add a Vite plugin or a `tailwind.config` file. Each View is an isolated iframe. The compiler scans that View and emits only the utilities it uses. CSS is inlined in a `