> You are reading Better Auth documentation for `v1.6`. This is not the current stable release. APIs may differ from the latest stable version.

# MCP (/docs/1.6/plugins/mcp)

MCP provider plugin for Better Auth



`OAuth` `MCP`

<Callout type="warn">
  This plugin will soon be deprecated in favor of the [OAuth Provider Plugin](/docs/1.6/plugins/oauth-provider).
</Callout>

The **MCP** plugin lets your app act as an OAuth provider for MCP clients. It handles authentication and makes it easy to issue and manage access tokens for MCP applications.

<Callout type="warn">
  This plugin is based on OIDC Provider plugin. It'll be moved to the OAuth Provider Plugin in the future.
</Callout>

## Installation [#installation]

<Steps>
  <Step>
    ### Add the Plugin [#add-the-plugin]

    Add the MCP plugin to your auth configuration and specify the login page path.

    ```ts title="auth.ts"
    import { betterAuth } from "better-auth";
    import { mcp } from "better-auth/plugins"; // [!code highlight]

    export const auth = betterAuth({
        plugins: [
            mcp({ // [!code highlight]
                loginPage: "/sign-in" // path to your login page // [!code highlight]
            }) // [!code highlight]
        ]
    });
    ```

    <Callout>
      This doesn't have a client plugin, so you don't need to make any changes to your authClient.
    </Callout>
  </Step>

  <Step>
    ### Generate Schema [#generate-schema]

    Run the migration or generate the schema to add the necessary fields and tables to the database.

    <Tabs items="[&#x22;migrate&#x22;, &#x22;generate&#x22;]">
      <Tab value="migrate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth migrate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth migrate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>

      <Tab value="generate">
        <CodeBlockTabs defaultValue="npm" groupId="persist-install">
          <CodeBlockTabsList>
            <CodeBlockTabsTrigger value="npm">
              npm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="pnpm">
              pnpm
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="yarn">
              yarn
            </CodeBlockTabsTrigger>

            <CodeBlockTabsTrigger value="bun">
              bun
            </CodeBlockTabsTrigger>
          </CodeBlockTabsList>

          <CodeBlockTab value="npm">
            ```bash
            npx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="pnpm">
            ```bash
            pnpm dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="yarn">
            ```bash
            yarn dlx auth generate
            ```
          </CodeBlockTab>

          <CodeBlockTab value="bun">
            ```bash
            bun x auth generate
            ```
          </CodeBlockTab>
        </CodeBlockTabs>
      </Tab>
    </Tabs>

    The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](/docs/1.6/plugins/oidc-provider#schema) section for details.
  </Step>
</Steps>

## Usage [#usage]

### OAuth Discovery Metadata [#oauth-discovery-metadata]

Better Auth already handles the `/api/auth/.well-known/oauth-authorization-server` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-authorization-server` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:

```ts title=".well-known/oauth-authorization-server/route.ts"
import { oAuthDiscoveryMetadata } from "better-auth/plugins";
import { auth } from "../../../lib/auth";

export const GET = oAuthDiscoveryMetadata(auth);
```

### OAuth Protected Resource Metadata [#oauth-protected-resource-metadata]

Better Auth already handles the `/api/auth/.well-known/oauth-protected-resource` route automatically but some client may fail to parse the `WWW-Authenticate` header and default to `/.well-known/oauth-protected-resource` (this can happen, for example, if your CORS configuration doesn't expose the `WWW-Authenticate`). For this reason it's better to add a route to expose OAuth metadata for MCP clients:

```ts title="/.well-known/oauth-protected-resource/route.ts"
import { oAuthProtectedResourceMetadata } from "better-auth/plugins";
import { auth } from "@/lib/auth";

export const GET = oAuthProtectedResourceMetadata(auth);
```

### MCP Session Handling [#mcp-session-handling]

You can use the helper function `withMcpAuth` to get the session and handle unauthenticated calls automatically.

```ts title="api/[transport]/route.ts"
import { auth } from "@/lib/auth";
import { createMcpHandler } from "@vercel/mcp-adapter";
import { withMcpAuth } from "better-auth/plugins";
import { z } from "zod";

const handler = withMcpAuth(auth, (req, session) => {
    // session contains the access token record with scopes and user ID
    return createMcpHandler(
        (server) => {
            server.tool(
                "echo",
                "Echo a message",
                { message: z.string() },
                async ({ message }) => {
                    return {
                        content: [{ type: "text", text: `Tool echo: ${message}` }],
                    };
                },
            );
        },
        {
            capabilities: {
                tools: {
                    echo: {
                        description: "Echo a message",
                    },
                },
            },
        },
        {
            redisUrl: process.env.REDIS_URL,
            basePath: "/api",
            verboseLogs: true,
            maxDuration: 60,
        },
    )(req);
});

export { handler as GET, handler as POST, handler as DELETE };
```

You can also use `auth.api.getMcpSession` to get the session using the access token sent from the MCP client:

```ts title="api/[transport]/route.ts"
import { auth } from "@/lib/auth";
import { createMcpHandler } from "@vercel/mcp-adapter";
import { z } from "zod";

const handler = async (req: Request) => {
     // session contains the access token record with scopes and user ID
    const session = await auth.api.getMcpSession({
        headers: req.headers
    })
    if(!session){
        //this is important and you must return 401
        return new Response(null, {
            status: 401
        })
    }
    return createMcpHandler(
        (server) => {
            server.tool(
                "echo",
                "Echo a message",
                { message: z.string() },
                async ({ message }) => {
                    return {
                        content: [{ type: "text", text: `Tool echo: ${message}` }],
                    };
                },
            );
        },
        {
            capabilities: {
                tools: {
                    echo: {
                        description: "Echo a message",
                    },
                },
            },
        },
        {
            redisUrl: process.env.REDIS_URL,
            basePath: "/api",
            verboseLogs: true,
            maxDuration: 60,
        },
    )(req);
}

export { handler as GET, handler as POST, handler as DELETE };
```

## Configuration [#configuration]

The MCP plugin accepts the following configuration options:



<TypeTable type="mcpPluginOptionsType" />

### OIDC Configuration [#oidc-configuration]

The plugin supports additional OIDC configuration options through the `oidcConfig` parameter:



<TypeTable type="mcpOidcConfigOptionsType" />

## Remote MCP Client [#remote-mcp-client]

The examples above use `withMcpAuth` which requires the Better Auth instance to be in the **same process** as the MCP server. If your MCP server runs as a separate service (different repo, different runtime, different language), you can use the **MCP Client** — a lightweight, framework-agnostic HTTP client that validates Bearer tokens against a remote Better Auth server.

<Callout>
  No additional packages needed — the MCP Client is included in `better-auth`.
</Callout>

### Setup [#setup]

<Steps>
  <Step>
    ### Create the client [#create-the-client]

    Point it at your Better Auth server's URL (the same `baseURL` + `basePath` from your auth config):

    ```ts title="mcp-server.ts"
    import { createMcpAuthClient } from "better-auth/plugins/mcp/client" // [!code highlight]

    const mcpAuth = createMcpAuthClient({ // [!code highlight]
        authURL: "http://localhost:3000/api/auth" // [!code highlight]
    }) // [!code highlight]
    ```
  </Step>

  <Step>
    ### Protect your MCP routes [#protect-your-mcp-routes]

    Use the `handler` wrapper for Web Standard `Request`/`Response` (works with Deno, Bun, Cloudflare Workers, etc.):

    ```ts title="mcp-server.ts"
    const handler = mcpAuth.handler(async (req, session) => { // [!code highlight]
        // session.userId, session.scopes, session.clientId
        return new Response(JSON.stringify({
            jsonrpc: "2.0",
            result: { userId: session.userId },
            id: 1
        }))
    }) // [!code highlight]

    Deno.serve(handler)
    ```
  </Step>

  <Step>
    ### Mount discovery endpoints [#mount-discovery-endpoints]

    MCP clients need to discover your OAuth server. Mount these at the root of your MCP server:

    ```ts title="mcp-server.ts"
    const discovery = mcpAuth.discoveryHandler() // [!code highlight]
    const protectedResource = mcpAuth.protectedResourceHandler("http://localhost:4000") // [!code highlight]

    // GET /.well-known/oauth-authorization-server → proxied from Better Auth
    // GET /.well-known/oauth-protected-resource → points MCP clients to Better Auth
    ```

    These proxy and cache the metadata from your Better Auth server, so MCP clients can discover the authorization, token, and registration endpoints automatically.
  </Step>
</Steps>

### Framework Adapters [#framework-adapters]

<Tabs items="[&#x22;Hono&#x22;, &#x22;Express&#x22;, &#x22;Official MCP SDK&#x22;, &#x22;mcp-use&#x22;]">
  <Tab value="Hono">
    ```ts title="mcp-server.ts"
    import { Hono } from "hono"
    import { mcpAuthHono } from "better-auth/plugins/mcp/client/adapters" // [!code highlight]

    const app = new Hono()
    const { middleware, discoveryRoutes } = mcpAuthHono({ // [!code highlight]
        authURL: "http://localhost:3000/api/auth" // [!code highlight]
    }) // [!code highlight]

    // Mount OAuth discovery endpoints (required by MCP spec)
    discoveryRoutes(app, "http://localhost:4000") // [!code highlight]

    // Protect MCP routes
    app.use("/mcp/*", middleware) // [!code highlight]

    app.post("/mcp", (c) => {
        const session = c.get("mcpSession") // [!code highlight]
        // session.userId, session.scopes, etc.
    })
    ```
  </Tab>

  <Tab value="Express">
    ```ts title="mcp-server.ts"
    import express from "express"
    import { createMcpAuthClient } from "better-auth/plugins/mcp/client" // [!code highlight]

    const app = express()
    const mcpAuth = createMcpAuthClient({ // [!code highlight]
        authURL: "http://localhost:3000/api/auth" // [!code highlight]
    }) // [!code highlight]

    app.use("/mcp", mcpAuth.middleware()) // [!code highlight]

    app.post("/mcp", (req, res) => {
        const session = req.mcpSession // [!code highlight]
        // session.userId, session.scopes, etc.
    })
    ```
  </Tab>

  <Tab value="Official MCP SDK">
    ```ts title="mcp-server.ts"
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
    import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
    import { mcpAuthOfficial } from "better-auth/plugins/mcp/client/adapters" // [!code highlight]

    const auth = mcpAuthOfficial({ // [!code highlight]
        authURL: "http://localhost:3000/api/auth" // [!code highlight]
    }) // [!code highlight]

    const mcpServer = new McpServer({ name: "my-server", version: "1.0.0" })
    const app = express() // your HTTP framework

    app.post("/mcp", auth.handler(async (req, session) => { // [!code highlight]
        const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: () => crypto.randomUUID()
        })
        await mcpServer.connect(transport)
        return transport.handleRequest(req)
    }))
    ```
  </Tab>

  <Tab value="mcp-use">
    Drop-in replacement for `oauthWorkOSProvider`, `oauthSupabaseProvider`, etc.:

    ```ts title="mcp-server.ts"
    import { MCPServer } from "mcp-use/server"
    import { mcpAuthMcpUse } from "better-auth/plugins/mcp/client/adapters" // [!code highlight]

    const server = new MCPServer({
        name: "my-server",
        version: "1.0.0",
        oauth: mcpAuthMcpUse({ // [!code highlight]
            authURL: "http://localhost:3000/api/auth" // [!code highlight]
        }) // [!code highlight]
    })
    ```
  </Tab>
</Tabs>

### Options [#options]



<TypeTable type="mcpClientOptionsType" />

### Session Object [#session-object]

The session object returned by `verifyToken` and passed to handlers contains:



<TypeTable type="mcpSessionObjectType" />

## Schema [#schema]

The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](/docs/1.6/plugins/oidc-provider#schema) section for details.

