Files
better-auth/docs/content/docs/plugins/mcp.mdx
Paolo Ricciuti 4b3642d324 fix(mcp): remove duplicate /api/auth from wwwAuthenticateValue and properly format the header (#4462)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
2025-09-05 10:32:57 -07:00

240 lines
7.1 KiB
Plaintext

---
title: MCP
description: MCP provider plugin for Better Auth
---
`OAuth` `MCP`
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.
## 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";
export const auth = betterAuth({
plugins: [
mcp({
loginPage: "/sign-in" // path to your login page
})
]
});
```
<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">
```bash
npx @better-auth/cli migrate
```
</Tab>
<Tab value="generate">
```bash
npx @better-auth/cli generate
```
</Tab>
</Tabs>
The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#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:
<TypeTable
type={{
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
}
}}
/>
### OIDC Configuration
The plugin supports additional OIDC configuration options through the `oidcConfig` parameter:
<TypeTable
type={{
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"]'
}
}}
/>
## Schema
The MCP plugin uses the same schema as the OIDC Provider plugin. See the [OIDC Provider Schema](#schema) section for details.