Skip to main content
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

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

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. For a runnable framework application and Inspector OAuth setup, see the Elysia Clerk example. 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:
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:
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:
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 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. 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:
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 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.