> ## Documentation Index
> Fetch the complete documentation index at: https://toolgrain.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# HTTP authorization

> Detailed HTTP authorization, public resource URLs, routing, and custom providers.

Configure authorization on `createServer({ auth })`. Tools remain standalone and
read the verified identity using their integration's synchronous `useAuth(ctx)`.
The API is the same for MCP `2026-07-28` and `2025-06-18`.

## JWT

```ts theme={null}
// tools/who-am-i.ts
import { useAuth } from "@toolgrain/jwt";
import { tool } from "@toolgrain/mcp";
import { z } from "zod";

export const whoAmI = tool({
  input: z.object({}),
  execute(_input, ctx) {
    const { claims } = useAuth(ctx);
    return {
      subject: claims.sub ?? null,
      issuer: claims.iss,
      expiresAt: claims.exp,
    };
  },
});
```

```ts theme={null}
// server.ts
import { jwt } from "@toolgrain/jwt";
import { createServer } from "@toolgrain/mcp";
import { whoAmI } from "./tools/who-am-i.js";

export const server = createServer({
  name: "jwt-example",
  version: "1.0.0",
  auth: jwt({ issuer: process.env.JWT_ISSUER! }),
  tools: { whoAmI },
});
```

`JWT_ISSUER` is an application-defined environment variable containing your
authorization provider's exact issuer identifier. Toolgrain discovers `jwks_uri`
through OAuth authorization server metadata or OpenID Connect discovery, checking
that the document's issuer matches the configured issuer. Path issuers use the
standard discovery URL variants. `jwksUrl` is an optional explicit override for
issuers without discovery. Metadata retrieval is bounded by size and time and
does not follow redirects.

Verification uses `jose`, with cached discovery and remote signing keys, including
key rotation. The defaults allow RS256 and ES256. Override `algorithms` with
supported asymmetric algorithms; symmetric algorithms and `none` are rejected.
Tokens require a valid signature, the configured issuer, a matching resource
audience, and `exp`. Expiration and any `nbf` are checked. Standard claim types
are checked before exposure. Audience arrays contain only strings; `exp`, `iat`,
and `nbf` must be finite numbers when present. Unsupported JWT critical headers
are invalid tokens (401); signing-key retrieval/import failures remain provider
errors (503). `iss`, `aud`, and `exp` are required; `sub`, `iat`,
`nbf`, and `jti` are optional. Other claims have type `unknown`.

JWT verification does not issue tokens or implement login, consent, OAuth client
registration, or an authorization server. Use an OAuth provider supporting your
MCP clients' authorization and registration flows.

## Clerk

```ts theme={null}
// tools/who-am-i.ts
import { useAuth } from "@toolgrain/clerk";
import { tool } from "@toolgrain/mcp";
import { z } from "zod";

export const whoAmI = tool({
  input: z.object({}),
  execute(_input, ctx) {
    const auth = useAuth(ctx);
    return {
      userId: auth.userId,   // string
      clientId: auth.clientId, // string
      scopes: auth.scopes,   // readonly string[]
    };
  },
});
```

```ts theme={null}
// server.ts
import { clerk } from "@toolgrain/clerk";
import { createServer } from "@toolgrain/mcp";
import { whoAmI } from "./tools/who-am-i.js";

export const server = createServer({
  name: "clerk-example",
  version: "1.0.0",
  auth: clerk({
    publishableKey: process.env.CLERK_PUBLISHABLE_KEY!,
    secretKey: process.env.CLERK_SECRET_KEY!,
  }),
  tools: { whoAmI },
});
```

Get both keys from the same Clerk application/environment's API keys page. No
`CLERK_ISSUER` variable is needed: the publishable key identifies the Frontend API
and its issuer. Configure OAuth applications and client onboarding in Clerk for
the MCP clients you intend to support. See [Clerk's OAuth guide](https://clerk.com/docs/guides/configure/auth-strategies/oauth/how-clerk-implements-oauth).

For a runnable framework application and Inspector OAuth setup, see the
[Elysia Clerk example](https://github.com/keshavkatwe/toolgrain/blob/main/apps/elysia-example/README.md#clerk-authorization-and-inspector).

Enable `aud_claim_enabled` in Clerk's instance OAuth application settings. Clerk
then includes the requested OAuth `resource` in access tokens' audience. Both JWT
and opaque tokens must include the resolved MCP resource as their audience.
Otherwise token exchange can succeed while MCP requests return `401 invalid_token`.
Authorize again after enabling it to obtain a new token; existing tokens do not
acquire the missing audience.

Only OAuth access tokens for your resource are accepted. JWT OAuth tokens are
verified against the Clerk issuer's signing keys; opaque `oat_` tokens are
verified with Clerk's backend SDK and checked for audience, revocation, and
expiration. Session tokens, API keys, and M2M tokens are rejected. The context
contains plain identity data; credentials and SDK token-getter methods are not
exposed. Scopes are available to application code but Toolgrain v0 does not
implement per-tool scope policies.

## Standalone Node HTTP

`http()` from `@toolgrain/node` owns the HTTP listener, routing, host validation,
and shutdown. It works with both public and auth-configured servers:

```ts theme={null}
import { http } from "@toolgrain/node";
import { server } from "./server.js";

const transport = http({ port: 3007, path: "/mcp" });
await server.listen(transport);
console.log(transport.address());

// Your application decides when to stop, including process signal handling.
// await server.close();
```

The default bind hostname is `127.0.0.1`, port is `3000`, and endpoint path is
`/mcp`. Port `0` requests an available port; `transport.address()` returns a copied
Node address after startup and undefined before startup or after shutdown.
Default accepted hosts are `localhost`, `127.0.0.1`, and `[::1]` on the actual
listening port. `hostname` controls the bind address; `allowedHosts` separately
controls accepted request authorities. Closing the server aborts active requests
and verification, stops the listener, and closes its connections. Startup failures
release the listener too. `server.listen()` claims each transport instance for
its first server; that instance can be attached only once. Rejected reuse leaves the owning server running and
the other server free to attach a fresh transport. Ownership is shared across
compatible package copies and supports frozen custom transports. The transport
does not install global process handlers.

When auth is configured, Toolgrain automatically serves public GET/HEAD metadata
at `/.well-known/oauth-protected-resource/mcp`. Custom paths, root paths, and
significant trailing slashes produce their corresponding metadata paths. An
explicit provider `resource` determines the advertised public metadata path.
Unmatched routes return 404; a server without auth exposes no resource metadata.

## Combined handlers for existing applications

`createHttpHandler(server, options?)` returns
`(request: Request) => Promise<Response>` and includes the same MCP routing,
public metadata, host validation, and authentication. Mount it at the application
root so that both the endpoint and well-known metadata path reach it. A framework
mount that strips `/mcp` from the Request URL is unsuitable for this combined
handler. The returned `HttpHandler` also provides `matches(pathname)` for
framework middleware to identify the MCP and configured metadata paths before
consuming the request body. A Hono middleware can call
`handler.matches(c.req.path) ? handler(c.req.raw) : next()`.

For example, Elysia on Bun can mount the handler without consuming the body:

```ts theme={null}
import { createHttpHandler } from "@toolgrain/http";
import { Elysia } from "elysia";
import { server } from "./server.js";

const handler = createHttpHandler(server, {
  path: "/mcp",
  allowedHosts: ["localhost:3003", "127.0.0.1:3003"],
});

new Elysia()
  .get("/health", () => ({ status: "ok" }))
  .mount(handler)
  .onStop(() => server.close())
  .listen({ hostname: "127.0.0.1", port: 3003 });
```

`createNodeHttpHandler(server, options?)` from `@toolgrain/node` provides the
same combined handler for Node IncomingMessage/ServerResponse. Mount it at the
root before body parsing middleware. When Express supplies `next`, unmatched
paths are delegated immediately, with their request bodies untouched. Matched MCP
requests keep their authentication, protocol, and error responses, including 404
protocol errors. Without a middleware continuation, unmatched paths return 404.
These handler factories leave listener ownership with the application. Separate
`createProtectedResourceMetadataHandler(server, { allowedHosts? })` and
`createNodeProtectedResourceMetadataHandler(server, { allowedHosts? })` remain
available for frameworks that require explicit filesystem or route registration.
They apply host validation too; the hosting platform must route the public
well-known path to them.

## Host validation and resource inference

Handlers accept `path`, `allowedHosts`, `allowedOrigins`, and `maxMessageBytes`.
Paths default to `/mcp` and must be absolute URL paths without query strings,
fragments, whitespace, or dot-segment normalization. `allowedHosts` contains exact
hostnames or IP addresses, optionally with ports, for example
`["api.example.com", "[::1]:3003"]`. Matching normalizes case and standard URL
ports; wildcards, schemes, and paths are rejected. An empty list rejects every
host. Configured arrays are copied. Both the request URL authority and any
supplied Host header must be allowed. When a framework normalizes the URL host
(for example Next.js using localhost for loopback), the validated Host header
preserves the actual resource authority. Disallowed hosts receive 403 before
routing, authentication, or body parsing, including on metadata requests.

Standalone `http()` derives exact local authorities from the listening port.
Existing-application handlers default to the three loopback hostnames on any
port because they do not own the listener. Set `allowedHosts` for production or
to restrict development to a particular port. `allowedOrigins` separately checks
the browser Origin header on MCP requests; it is not a host allowlist or a CORS
implementation.

For `https://api.example.com/mcp`, the inferred resource is
`https://api.example.com/mcp`. Query parameters are excluded. Paths and significant
trailing slashes are preserved; a root resource uses the origin without `/`.
Metadata works before any authenticated request; no first-request origin is
cached. Metadata, challenges, and token audience verification use the same
resource.

Applications still supply deployment configuration and trusted proxy
normalization. Node uses the actual Host, target, port, and direct TLS scheme and
ignores Forwarded and X-Forwarded-\* headers. If a trusted proxy rewrites the
external scheme, host, or path, supply a normalized public Web Request (including
its Host header), or configure a canonical provider resource:

```ts theme={null}
clerk({
  publishableKey,
  secretKey,
  resource: "https://api.example.com/mcp",
});
```

The JWT adapter accepts the same optional `resource` override. Configure
`allowedHosts` for the authorities that reach the handler; a resource override
does not disable host validation. Auth URLs require HTTPS, with HTTP loopback
exceptions for development. Configured auth URLs reject credentials, queries,
fragments, and whitespace. Applications continue to own TLS, CORS, and any
framework listener. The [JWT example](https://github.com/keshavkatwe/toolgrain/blob/main/apps/jwt-example/README.md) demonstrates
managed Node HTTP and both protocol revisions.

## Request behavior

After endpoint checks, authentication runs before body parsing or tool work.
Credentials must be a bearer token in the `Authorization` header. Query tokens
are rejected; cookies, request `_meta`, and tool arguments do not establish an
identity. Each authenticated request gets its own copied, deeply frozen identity.
Calling another integration's `useAuth` or using a context without auth throws.

| Condition | HTTP status |
| - | - |
| Missing or invalid/expired access token | 401, with a resource metadata challenge |
| Malformed bearer credentials or a query token | 400 |
| Provider access denial | 403 |
| Provider/network failure or closed server | 503 |

These HTTP statuses are the same in both protocol eras. JWT and Clerk audience
failures include an actionable `error_description` in the JSON body and the
`WWW-Authenticate` challenge. Missing `aud` and an audience that does not match
the resource have distinct messages. For example:

```json theme={null}
{
  "error": "invalid_token",
  "error_description": "Access token is missing the aud claim. Configure the authorization server to include an audience and request a new token with resource=http://127.0.0.1:3007/mcp."
}
```

A mismatch reports the expected resource and asks the client to request a new
token for it. These descriptions include only the expected resource, never the
received token, its actual audience, or other claims. Signature verification or
Clerk backend verification runs before audience diagnostics; invalid credentials
and private provider errors retain generic responses. Raw error messages and
causes are never automatically exposed. Challenge descriptions use Bearer-safe
ASCII; the JSON body preserves the readable description.

Unexpected verification failures, including provider
`AuthError("temporarily_unavailable")` outages, reach the server's `onError(error)`
callback once with the original thrown value and its private cause, if present.
No tool/request details or credentials are added. Expected auth rejections
(`invalid_request`, `invalid_token`, and `access_denied`), closed-server rejections,
and failures after cancellation do not reach the callback. Callback throws and
asynchronous rejections cannot disrupt the response or cleanup.

Public metadata uses `Cache-Control: no-store` and is available without a bearer
token.

Verification, body reading, and execution share request cancellation and server
shutdown. The server stops waiting if verification cannot be cancelled by the
integration. Shared JWT discovery/key fetches may finish for other requests;
Clerk's backend SDK may finish its in-flight request. Late identities and handler
results are discarded. Tool execution remains cooperatively cancellable.

An auth-configured server accepts request transports such as `http()` and rejects
message transports such as `stdio()` before startup. This authorization API is
HTTP-only; stdio servers use their process/environment trust model.

## Custom providers and direct invocation

Integrations implement `AuthProvider<T>` from `@toolgrain/mcp`, with a registered
provider `type` symbol, optional canonical `resource`, nonempty
`authorizationServers`, and `verify(token, { resource, signal })`. Return plain
identity data and throw `AuthError` with `invalid_token`, `access_denied`, or
`temporarily_unavailable` as appropriate. `createAuthAccessor<T>(type)` creates a
typed accessor returning recursively readonly identity data. Matching compatible
package copies share provider/context symbols.

Custom providers can use `validateTokenAudience(verifiedAudience, resource)` from
`@toolgrain/mcp` after verifying a token's authenticity and validity. It accepts a
string or array of strings, requires a match for the expected resource, and throws
`AuthError` with public guidance for missing or mismatched audiences. It does not
verify signatures, issuers, expiration, or revocation itself.

`new AuthError(code, { description, cause })` allows an explicitly public
description separate from a private cause. `description` also becomes the error's
message and is included as `error_description` by HTTP adapters. Include only
information intended for clients; never copy raw provider errors or credentials
into it. Omitting `description` preserves the existing generic response.

The HTTP adapters call `server.authorize(token, { resource, signal? })`, then pass
the opaque scope to `server.handle(message, { auth: scope, protocolVersion? })`.
Custom transport integrations must dispose the scope in `finally`. Scopes are
single-use, server-bound, and cannot be fabricated or reused after disposal.
Calling `handle` without a valid scope on a protected server is rejected.
`getAuthMetadata()` returns copied configuration for HTTP adapters.

`toolContext()` creates an unauthenticated context. Directly calling a tool's raw
`execute` bypasses server authorization and validation; test protected execution
through `authorize` and `handle` instead.

[Lazy context providers](https://toolgrain.mintlify.site/guides/context.md) can read `useAuth(ctx)` and depend on other
providers through inferred async accessors. Authorization is still eagerly
verified before context resolution; synchronous `useAuth(ctx)` does no work.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.