On this page
Better Auth MCP: Add OAuth to Your MCP Server with the 1.7 Plugin (Nov 2026)

Better Auth MCP is Better Auth's mcp() plugin, which since version 1.7 lives in its own package, @better-auth/mcp. It makes your app the OAuth 2.1 sign-in server that MCP clients use, and its requireMcpAuth wrapper checks the token on every request to your MCP route.

It isn't an MCP server you install. It's a library you add to your own TypeScript app. It fits when your users already sign in with Better Auth and you want AI apps to reach your MCP server as those users. The current release is 1.7.22 (12 November 2026), MIT licensed, from the better-auth/better-auth repo (about 30,700 stars).

Some background in plain words. An MCP server gives an AI app a set of tools. A remote one needs to know who is calling. The MCP spec answers that with OAuth: the client sends the user to a sign-in page, gets a token back, and sends that token with every request. Better Auth can be the part that signs people in and hands out the tokens. The full flow is in how MCP OAuth works.

Heads-up if you're following an older tutorial. Most Better Auth MCP posts online show the 1.6 plugin: import { mcp } from "better-auth/plugins" with withMcpAuth. That API changed in 1.7, released 17 August 2026. This guide uses the current one, and the migration table maps the old names to the new ones.

What the Better Auth MCP plugin does

mcp() switches on Better Auth's OAuth 2.1 provider with MCP settings. Per the plugin docs, Better Auth owns "OAuth discovery, client registration and discovery, PKCE authorization, resource binding, token issuance and refresh, and protected-resource challenges." Here's who does what:

Part Who handles it
Discovery documents (/.well-known/oauth-protected-resource, authorization server metadata) Better Auth
Knowing which client is asking (CIMD, or DCR if you turn it on) Better Auth
Sign-in with PKCE, tokens tied to your server's address, refresh Better Auth
401 and 403 answers on the MCP route Better Auth (requireMcpAuth)
The MCP protocol and transport The official MCP TypeScript SDK v2 (@modelcontextprotocol/server)
Your sign-in page, your consent page and the tools themselves You

The OAuth endpoints live under /oauth2/*: /oauth2/authorize, /oauth2/token, /oauth2/userinfo, and /oauth2/register only when you enable dynamic registration.

Better Auth MCP setup in 5 steps

The docs' example is a Next.js app. The same parts apply to other frameworks.

1. Install the packages

npm install @better-auth/mcp @better-auth/cimd @modelcontextprotocol/server zod

Keep better-auth and @better-auth/mcp on the same version. The 1.7.22 plugin asks for better-auth ^1.7.22.

2. Add the plugin to your auth config

This is the docs' auth.ts:

import { betterAuth } from "better-auth";
import { jwt } from "better-auth/plugins";
import { cimd } from "@better-auth/cimd";
import { mcp } from "@better-auth/mcp";
import { fetchClientMetadataResource } from "@better-auth/cimd/node";

export const auth = betterAuth({
    disabledPaths: ["/token"],
    plugins: [
        jwt({ disableSettingJwtHeader: true }),
        mcp({
            loginPage: "/sign-in",
            consentPage: "/consent",
            resource: "https://api.example.com/mcp",
        }),
        cimd({
            fetchClientMetadataResource,
            metadataProfile: "mcp-2026-07-28",
        }),
    ],
});

What each part does:

  • resource is your MCP server's public address. Tokens carry it as their audience, so a token for this server won't work anywhere else. It must be HTTPS; plain HTTP is accepted only on localhost.
  • jwt() is required. It signs the tokens and publishes the /jwks keys your MCP route checks them against.
  • disabledPaths: ["/token"] and disableSettingJwtHeader: true switch off the JWT plugin's own session tokens. The docs explain why: "Session JWTs share the access tokens' signing key", so a loose check could accept one as an access token. Don't drop these two lines.
  • cimd() lets MCP clients identify themselves with a document on their own website. More on that below.
  • Don't also add oauthProvider(). The docs are clear: "mcp() is the OAuth provider."

3. Create the tables

npx auth migrate

Or run npx auth generate to write the schema for your ORM. auth is Better Auth's own CLI package. It adds the OAuth tables: oauthClient, oauthAccessToken, oauthRefreshToken, oauthConsent, oauthClientAssertion, oauthResource and oauthClientResource.

4. Build the sign-in and consent pages

When a client starts sign-in, Better Auth sends the user to loginPage if they aren't signed in, then to consentPage to approve the client. Add the client plugin to the app that renders those pages:

import { createAuthClient } from "better-auth/client";
import { oauthProviderClient } from "@better-auth/oauth-provider/client";

export const authClient = createAuthClient({
    plugins: [oauthProviderClient()],
});

@better-auth/oauth-provider is the package @better-auth/mcp is built on; add it to your own dependencies since you import from it. On the consent page, show who is asking and which scopes they want, then call:

await authClient.oauth2.consent({ accept: true });

Pass scope with a space-separated subset to grant only some of the requested scopes. Client names and logos come from the client itself, so we'd also show the client ID. Under CIMD that's a web address the user can recognise.

5. Protect the MCP route

A trimmed version of the docs' app/api/mcp/route.ts:

import { auth } from "@/lib/auth";
import { requireMcpAuth } from "@better-auth/mcp";
import { createMcpHandler, McpServer } from "@modelcontextprotocol/server";
import * as z from "zod";

const resource = "https://api.example.com/mcp";

const mcpServerHandler = createMcpHandler(
    () => {
        const server = new McpServer({ name: "example-mcp-server", version: "1.0.0" });
        server.registerTool(
            "echo",
            { description: "Echo a message", inputSchema: z.object({ message: z.string() }) },
            async ({ message }) => ({ content: [{ type: "text", text: `Tool echo: ${message}` }] }),
        );
        return server;
    },
    { legacy: "reject" },
);

const POST = requireMcpAuth(auth, (request) => mcpServerHandler.fetch(request), {
    resource, // must match mcp({ resource })
});

export { POST };

What happens on each request:

  • requireMcpAuth reads the Authorization header and checks the token's signature, issuer, audience and expiry against your JWKS. It also enforces DPoP when a token is bound to a key.
  • No token: a 401 with a WWW-Authenticate header that tells the client where to sign in.
  • Missing scope: a 403 with insufficient_scope, so the client can ask for more. Set the scopes every call needs with requiredScopes.
  • Only POST is exported. MCP 2026-07-28 is stateless, so GET and DELETE get 405. The Streamable HTTP guide explains that change.

About legacy: "reject". It accepts only the 2026-07-28 protocol. The SDK's type docs say the default, "stateless", also answers older 2025-era clients one request at a time. If some of your users' clients haven't moved to 2026-07-28 yet, leave the option out.

If your auth lives under a base path such as /api/auth, check discovery. Better Auth serves the discovery documents, but only for requests that reach its handler, and most frameworks forward only /api/auth/*. For each well-known path outside it, add a route. The OAuth Provider docs show oauthProviderAuthServerMetadata(auth) at app/.well-known/oauth-authorization-server/api/auth/route.ts.

Client ID Metadata Documents vs dynamic client registration

Before an MCP client can sign anyone in, your server has to know the client. There are two ways.

  • Client ID Metadata Documents (CIMD). The client's ID is a web address. Your server fetches a small JSON document from it with the client's name and callback addresses. Nothing is stored ahead of time. The 2026-07-28 spec prefers this, and Better Auth calls it "the recommended client identity mechanism". metadataProfile: "mcp-2026-07-28" picks the matching version.
  • Dynamic client registration (DCR). The client posts its details to /oauth2/register and gets an ID back. The 2026-07-28 spec deprecates it, and "Better Auth never enables DCR implicitly."

To keep DCR for older clients that still need it, turn on both settings:

mcp({
    loginPage: "/sign-in",
    consentPage: "/consent",
    resource: "https://api.example.com/mcp",
    allowDynamicClientRegistration: true,
    allowUnauthenticatedClientRegistration: true,
    clientRegistrationDefaultApplicationType: "infer",
})

Know what that opens. The docs warn it "lets anyone on the internet register a client, with a name and logo they choose." Rate-limit /oauth2/register, or anyone can fill your database with client registrations. "infer" helps clients that register localhost or custom-scheme callbacks without saying they're desktop apps: they're registered as native instead of being rejected.

CIMD means your server fetches an address the client picked. On Node.js, the bundled fetchClientMetadataResource from @better-auth/cimd/node handles this. On Bun, Deno, Workers and other runtimes, the docs say you must supply a fetcher that "resolves each hostname once, rejects RFC 6890 special-use addresses, pins the approved address for the connection, and refuses redirects." Skip that and a fake client could make your server call addresses on your internal network.

Moving from the old Better Auth MCP plugin

The 1.6 docs warned: "This plugin will soon be deprecated in favor of the OAuth Provider Plugin." It wasn't dropped. In 1.7 it moved to @better-auth/mcp, now built on that OAuth Provider plugin. Better Auth's 1.7 notes say existing setups need a migration "because the package, setup, and OAuth paths have changed." From the 1.7 upgrade guide:

Old (1.6) New (1.7)
import { mcp } from "better-auth/plugins" import { mcp } from "@better-auth/mcp"
withMcpAuth requireMcpAuth
mcpHandler createMcpProtectedRequestHandler
createMcpAuthClient and its adapters Removed: use @modelcontextprotocol/client or @modelcontextprotocol/server
Options nested under oidcConfig Flat options on mcp({ ... })
No JWT plugin needed jwt() required
resourceMetadataMappings One HTTPS resource
Endpoints under /mcp/* /oauth2/* (clients find them through discovery)
Unauthenticated registration on by default Off unless you set both DCR flags
GET and DELETE exports, redisUrl session store POST only, no session store
Route scopes requiredScopes, plus challengeScopes for the 401 hint
oauthApplication table oauthClient: copy or re-register each client (redirectUrls becomes redirectUris)

The CLI doesn't copy client records for you. The guide says to expire the old access tokens, then drop or rename the old oauthAccessToken table before creating the 1.7 token tables. Expect your MCP users to sign in again after the switch.

Better Auth MCP security: advisories to patch

TrustedMCP doesn't grade Better Auth: it's a library, not an MCP server. These are the public signals we'd check for any auth library.

The repo lists 49 published security advisories (4 critical, 24 high) as of 12 November 2026. That's a lot. It also shows people are looking hard and fixes ship. What matters is your version. The latest batch, 12 advisories published on 3 November 2026, is fixed in 1.7.16. It includes a critical flaw in the Expo authorization proxy and an OAuth sign-in that could treat an unverified email as verified.

These touched the MCP plugin or the OAuth provider it now runs on:

Advisory What went wrong Fixed in
GHSA-86j7-9j95-vpqj (medium) OIDC and MCP plugins accepted javascript: redirect addresses that could run scripts 1.6.13, 1.7.0-beta.4
GHSA-pw9m-5jxm-xr6h (medium) OIDC provider and MCP plugins accepted refresh tokens without the client secret 1.6.11
GHSA-7w99-5wm4-3g79 (high) Parallel requests could reuse one authorization code oauth-provider 1.6.11
GHSA-392p-2q2v-4372 (high) Concurrent refreshes could create extra valid refresh tokens oauth-provider 1.6.11
GHSA-p2fr-6hmx-4528 (medium) Access tokens could target APIs the user didn't authorize oauth-provider 1.7.0-beta.4
GHSA-q84f-53jg-9ppm (high) Device login approval didn't show the requesting client or permissions 1.7.0-rc.3

None of the six affects a 1.7 stable release. Run 1.7.16 or later and check what you actually have:

npm ls better-auth @better-auth/mcp @better-auth/oauth-provider

If the same app also runs an MCP server on the official SDK, check that too: MCP TypeScript SDK vulnerabilities.

Check your Better Auth MCP setup before you ship

  1. Patched versions. better-auth, @better-auth/mcp and @better-auth/oauth-provider on 1.7.16 or later, all on the same version.
  2. Session tokens off. disabledPaths: ["/token"] and jwt({ disableSettingJwtHeader: true }) are both still in your config.
  3. One exact resource. Your MCP server's real HTTPS address, with the same value passed to requireMcpAuth.
  4. DCR off unless you need it. If a client you must support can't do CIMD and you turn DCR on, rate-limit /oauth2/register.
  5. A decision on refresh-token reuse. The plugin lets a client reuse a refresh token for 30 seconds so retries work. The docs warn that an attacker who replays a stolen one inside that window "gets the replacement refresh token". Set refreshTokenReuseInterval: 0 if strict replay handling matters more to you than retries on flaky networks.
  6. Small scopes. Require read scopes up front. For a tool that needs more, throw createInsufficientScopeError(["mcp:admin"]) from better-auth/oauth2 so the client asks for that scope. If granting a scope can't help (the record belongs to someone else), return a plain 403.
  7. A real test. Connect with the MCP Inspector and no token: you should get a 401 whose WWW-Authenticate header names your resource_metadata. Then run the full sign-in, and confirm a token issued for a different resource is refused.

The rest of the server-side list (input checks, timeouts, logging) is in how to create an MCP server, securely.

Running the MCP server on another host

requireMcpAuth checks tokens against your Better Auth JWKS, so the MCP route can run anywhere that can reach that URL. When it runs separately from Better Auth, the docs use createMcpProtectedRequestHandler with the checks spelled out:

import { createMcpProtectedRequestHandler } from "@better-auth/mcp";

const handler = createMcpProtectedRequestHandler(
    {
        issuer: "https://auth.example.com/api/auth",
        audience: "https://api.example.com/mcp",
        jwksUrl: "https://auth.example.com/api/auth/jwks",
        jwtVerifyOptions: { typ: "at+jwt", requiredClaims: ["azp"] },
    },
    async (request, accessTokenClaims) => {
        // accessTokenClaims holds the verified token claims
        return yourMcpHandler(request, accessTokenClaims);
    },
);

It sends the same 401 and 403 answers as requireMcpAuth.

A Python or Go MCP server can't use this helper, but it can make the same checks with any JWT library: signature against the /jwks URL, the issuer, the audience (your resource), expiry and the at+jwt token type. It must also send the 401 challenge itself.

When Better Auth isn't the right fit

  • Your company already has an identity provider (Okta, Microsoft Entra ID, Auth0 and so on). Point your server's protected resource metadata at it instead. See adding OAuth to your own MCP server.
  • Your app doesn't use Better Auth. Adding a whole auth library just for MCP sign-in means a second user database to keep in sync.
  • Your server is local. A stdio server on the user's own machine doesn't need OAuth. Read a key from the environment, as in keeping API keys out of your MCP config.

Searching "better auth mcp server" also turns up a few community GitHub repos with that name. They're separate projects, last updated in 2025, and not the plugin this guide covers.

Want OAuth added to an MCP server you already run, or a new server built around your app's sign-in? Tell us what it connects to.

Frequently asked questions

Is Better Auth MCP an MCP server?

No. It's a plugin for Better Auth, the TypeScript auth library. It makes your app the OAuth sign-in server for your own MCP server. You still write the MCP server, usually with the official TypeScript SDK.

Is the Better Auth MCP plugin deprecated?

No. The 1.6 docs said it would "soon be deprecated in favor of the OAuth Provider Plugin". Instead, 1.7 moved it to @better-auth/mcp, built on that OAuth Provider plugin. The old import from better-auth/plugins is the part that's gone.

Does Better Auth MCP support dynamic client registration?

Yes, but it's off by default. Set both allowDynamicClientRegistration and allowUnauthenticatedClientRegistration to turn it on, and rate-limit the registration endpoint. Client ID Metadata Documents are the recommended way.

Which MCP spec version does Better Auth MCP support?

Better Auth says @better-auth/mcp "supports the MCP 2026-07-28 authorization profile", the current spec version. Its example route rejects older 2025-era clients with legacy: "reject"; the MCP SDK's default, "stateless", still serves them.

Do I need the JWT plugin for Better Auth MCP?

Yes. In 1.7 jwt() is required. It signs the access tokens and publishes the keys your MCP route checks them with. Also turn off its /token route and set-auth-jwt header, as in the docs' config.

Can I use Better Auth MCP with a Python MCP server?

Yes, if Better Auth runs in a TypeScript app as the sign-in server. Your Python server then checks each token against Better Auth's /jwks URL, issuer and audience with a JWT library, and answers 401 with the WWW-Authenticate header when a token is missing.

How do I install the better-auth MCP plugin?

Run npm install @better-auth/mcp @better-auth/cimd @modelcontextprotocol/server zod, add mcp(), jwt() and cimd() to your betterAuth config, run npx auth migrate, then wrap your MCP route with requireMcpAuth. The five setup steps above show each part.