MCP plugin enables OAuth provider for Model Context Protocol clients
The MCP plugin turns a Better Auth server into an OAuth authorization server and protected resource for Model Context Protocol (MCP) clients. It is built on the OAuth 2.1 Provider and allows MCP clients to discover endpoints and obtain resource-bound access tokens through standard OAuth flows.
MCP plugin installation requirements
Install @better-auth/mcp, @better-auth/cimd, @modelcontextprotocol/server (version 2 of the official MCP TypeScript SDK), and zod. The JWT plugin is required alongside MCP and provides the stable signing key for ID tokens and access tokens, and exposes the /jwks endpoint that resource servers use to verify them.
MCP plugin required configuration options
The MCP plugin requires three configuration options: loginPage (path to login page where users are redirected for authentication), consentPage (path to consent page where users grant requested scopes), and resource (the protected resource identifier that access tokens are bound to, must be an HTTPS URL with no query, fragment, or credentials; HTTP accepted only on loopback hosts for local development).
MCP plugin schema tables
The MCP plugin uses the same schema as the OAuth Provider plugin: oauthClient, oauthAccessToken, oauthRefreshToken, oauthConsent, and oauthClientAssertion tables.
MCP plugin OAuth endpoints
The mcp() plugin serves standard OAuth 2.1 endpoints under /oauth2/*: /oauth2/authorize (Authorization), /oauth2/token (Token), /oauth2/register (Dynamic registration, when explicitly enabled), and /oauth2/userinfo (UserInfo).
MCP plugin discovery endpoints follow RFC standards
Discovery follows OAuth 2.0 Authorization Server Metadata (RFC 8414) and Protected Resource Metadata (RFC 9728). The well-known URLs are derived from the issuer, so a server with a base path serves them at the issuer-inserted location rather than the bare root. Discovery advertises client_id_metadata_document_supported only when cimd() is installed, and registration_endpoint only when DCR is enabled.
Do not register separate oauthProvider plugin with MCP
mcp() is the OAuth provider. Do not also register a separate oauthProvider() plugin in the same app.
MCP plugin default refreshTokenReuseInterval
For every client configured through mcp(), the plugin defaults refreshTokenReuseInterval to 30 seconds. This lets a client retry a refresh with the old token and receive the same rotated token response when another request already consumed that refresh token. Set refreshTokenReuseInterval: 0 on mcp() to disable the overlap window.
MCP plugin OAuth configuration options
Common MCP/OAuth provider configuration options: scopes (string[], default: ["openid", "profile", "email", "offline_access"]), resources (Array<string | OAuthResourceInput>), accessTokenExpiresIn (number, default: 3600 seconds), idTokenExpiresIn (number, default: 36000 seconds), refreshTokenExpiresIn (number, default: 2592000 seconds), refreshTokenReuseInterval (number, default: 30 for MCP), and codeExpiresIn (number, default: 600 seconds).
MCP plugin registers resource as default client-registration resource
mcp() registers the resource identifier as a default client-registration resource. A dynamically registered client is linked to the MCP resource even when its registration request omits the non-standard resources field. Any clientRegistrationDefaultResources provided are preserved, and the MCP resource is appended once.
Enable DCR fallback for older MCP clients
If you deliberately support older clients that still require DCR, enable both provider controls explicitly: set allowDynamicClientRegistration: true and allowUnauthenticatedClientRegistration: true. The registration endpoint is absent from discovery unless DCR is enabled.
MCP 2026-07-28 uses stateless request-response model
MCP 2026-07-28 uses a stateless request and response model: every client JSON-RPC request or notification is an independent HTTP POST, and the server does not maintain a protocol-level session between requests.
requireMcpAuth wraps MCP route handlers
Use requireMcpAuth to wrap the MCP TypeScript SDK v2's POST handler. It reads the Authorization header, verifies access tokens against the authorization server's JWKS, and checks the signature, issuer, audience, and expiry. It also enforces RFC 9449 Demonstrating Proof of Possession (DPoP) when an access token is DPoP-bound.
requireMcpAuth unauthenticated request response
Unauthenticated requests to requireMcpAuth receive a JSON-RPC 401 with the RFC 9728 WWW-Authenticate header, so MCP clients can start the authorization flow. Tokens missing a required scope receive a 403 with an RFC 6750 insufficient_scope challenge, which MCP clients use to step up their authorization.
requireMcpAuth passes verified claims as callback argument
The requireMcpAuth wrapper passes the verified access-token claims as a second callback argument to the handler, not as a database record. requireMcpAuth never exposes a refresh token and checks the access token locally against the JWKS without a database round trip.
requireMcpAuth DPoP replay protection
DPoP replay protection in requireMcpAuth uses the auth instance's database adapter by default so it works across server instances. Pass dpop.replayStore only when you need a different shared store.
requireMcpAuth default issuer and JWKS resolution
By default, requireMcpAuth reads the server's resolved Better Auth URL from the auth context and uses it as the expected issuer, resource, and JWKS base. Override the issuer or JWKS URL when jwt.issuer is custom or the authorization server runs separately.
requireMcpAuth configuration options
requireMcpAuth accepts options: resource (protected resource identifier), issuer (override when jwt.issuer is custom), jwksUrl (override JWKS URL), challengeScopes (scopes advertised in 401 challenge), and requiredScopes (enforced against token's scope claim; token missing any scope receives 403 insufficient_scope challenge).
createInsufficientScopeError for dynamic scope requirements
When required scopes depend on the request (one tool needs more than another), throw createInsufficientScopeError from the handler to challenge for exactly those scopes: createInsufficientScopeError(["mcp:admin"]).
createMcpProtectedRequestHandler for remote MCP servers
createMcpProtectedRequestHandler verifies tokens against Better Auth server's JWKS, so the MCP route can run anywhere as long as it can reach that JWKS URL. When the resource server runs separately from the authorization server, use createMcpProtectedRequestHandler with explicit verification options instead of requireMcpAuth.
createMcpProtectedRequestHandler configuration options
createMcpProtectedRequestHandler accepts options: issuer (required, authorization server issuer), audience (required, protected resource identifier), jwksUrl (required, JWKS URL), requiredScopes (scopes to enforce), and challengeScopes (scope hints advertised on unauthenticated challenges).
MCP plugin with device authorization for CLI
The same authorization server can let a CLI authorize through a browser without opening a local callback listener. Add oauthDeviceAuthorization({ verificationUri: "/device" }) alongside mcp(). mcp() is already the OAuth Provider, so this composition does not add a separate oauthProvider() plugin.
MCP protected resource metadata RFC 9728
The RFC 9728 /.well-known/oauth-protected-resource document is served automatically at the well-known root and the resource-path-inserted alias. It tells MCP clients which authorization server protects the resource, which scopes it supports, which resource identifier their access tokens must be bound to, and which DPoP proof algorithms are supported.
MCP plugin does not enable DCR implicitly
MCP deprecates Dynamic Client Registration (DCR), so Better Auth never enables DCR implicitly. Only enable it when deliberately supporting older clients that still require it.
MCP protocol version 2026-07-28 and legacy protocol handling
Better Auth owns OAuth discovery, client registration and discovery, PKCE authorization, resource binding, token issuance and refresh, and protected-resource challenges. The official MCP TypeScript SDK v2 owns the stateless MCP protocol and transport. Configure servers with legacy: "reject" and pin SDK client version negotiation to 2026-07-28 when adopting this profile.
MCP resource cannot contain query, fragment, or credentials
The resource identifier must be an HTTPS URL with no query, fragment, or credentials; HTTP is accepted only on loopback hosts for local development. A resource whose URL requires a query component cannot use mcp(), requireMcpAuth, or createMcpProtectedRequestHandler.