---
title: MCP
description: 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/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

<Steps>
  <Step>
    ### 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

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

    <Tabs items={["migrate", "generate"]}>
      <Tab value="migrate">
        ```package-install
        npx auth migrate
        ```
      </Tab>

      <Tab value="generate">
        ```package-install
        npx auth generate
        ```
      </Tab>
    </Tabs>

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

## Usage

### 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

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

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

The MCP plugin accepts the following configuration options:

export const mcpPluginOptionsType = {
  loginPage: {
    description: "Path to the login page where users will be redirected for authentication",
    type: "string",
    required: true,
  },
  resource: {
    description: "The resource that should be returned by the protected resource metadata endpoint",
    type: "string",
    required: false,
  },
  oidcConfig: {
    description: "Optional OIDC configuration options",
    type: "object",
    required: false,
  },
}

<TypeTable type={mcpPluginOptionsType} />

### OIDC Configuration

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

export const mcpOidcConfigOptionsType = {
  codeExpiresIn: {
    description: "Expiration time for authorization codes in seconds",
    type: "number",
    default: 600,
  },
  accessTokenExpiresIn: {
    description: "Expiration time for access tokens in seconds",
    type: "number",
    default: 3600,
  },
  refreshTokenExpiresIn: {
    description: "Expiration time for refresh tokens in seconds",
    type: "number",
    default: 604800,
  },
  defaultScope: {
    description: "Default scope for OAuth requests",
    type: "string",
    default: "openid",
  },
  scopes: {
    description: "Additional scopes to support",
    type: "string[]",
    default: '["openid", "profile", "email", "offline_access"]',
  },
}

<TypeTable type={mcpOidcConfigOptionsType} />

## 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

<Steps>
  <Step>
    ### 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

    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

    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

<Tabs items={["Hono", "Express", "Official MCP SDK", "mcp-use"]}>
  <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

export const mcpClientOptionsType = {
  authURL: {
    description: "Full URL to Better Auth endpoints (baseURL + basePath)",
    type: "string",
    required: true,
  },
  resource: {
    description: "Resource identifier for the protected resource metadata. Defaults to the origin of the server URL.",
    type: "string",
    required: false,
  },
  allowedOrigin: {
    description: "Allowed CORS origin. Defaults to the authURL origin. Set to '*' to allow all origins (not recommended for production).",
    type: "string",
    required: false,
  },
  fetch: {
    description: "Custom fetch implementation. Defaults to global fetch.",
    type: "typeof fetch",
    required: false,
  },
}

<TypeTable type={mcpClientOptionsType} />

### Session Object

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

export const mcpSessionObjectType = {
  accessToken: {
    description: "The opaque access token",
    type: "string",
  },
  refreshToken: {
    description: "The refresh token",
    type: "string",
  },
  accessTokenExpiresAt: {
    description: "When the access token expires",
    type: "string",
  },
  refreshTokenExpiresAt: {
    description: "When the refresh token expires",
    type: "string",
  },
  clientId: {
    description: "The OAuth client ID that requested the token",
    type: "string",
  },
  userId: {
    description: "The authenticated user's ID",
    type: "string",
  },
  scopes: {
    description: "Space-separated list of granted scopes",
    type: "string",
  },
}

<TypeTable type={mcpSessionObjectType} />

## Schema

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