feat: MCP plugin (#2666)

* chore: wip

* wip

* feat: mcp plugin

* wip

* chore: fix lock file

* clean up

* schema

* docs

* chore: lint

* chore: release v1.2.9-beta.1

* blog

* chore: lint
This commit is contained in:
Bereket Engida
2025-05-23 12:44:51 -07:00
committed by GitHub
parent a12b7fc331
commit 9cc2e3d8ab
56 changed files with 4540 additions and 239 deletions

View File

@@ -0,0 +1,178 @@
---
title: Authenicating MCP servers
description: A deep dive into how to implement MCP auth with Better Auth & Vercel MCP adapter
date: 2025-05-19
image: /images/blogs/mcp-auth.png
author:
name: Bereket Engida
avatar: /avatars/beka.jpg
twitter: imbereket
tags:
- mcp
- vercel
- ai
- nextjs
- neon
---
## Introduction
[MCP](https://modelcontextprotocol.io) is an open protocol that standardizes how applications provide context to LLMs. It provides a standardized way to connect AI models to different data sources and tools. It's been sometime since the MCP spec by anthropic become a standard for building LLM based apps.
The protocol covers both client and server implementations. When you make a server for MCP clients to connect to, one of the requirements is to have a proper way to authenticate and authorize them. The MCP spec recommends using [OAuth 2.0](https://oauth.net/2/) for this purpose with some additional requirements.
In this article, we'll see how Better Auth MCP plugin integrates with your MCP server to authenticate and authorize MCP clients.
## How Better Auth MCP Plugin Works
The Better Auth MCP plugin implements the OAuth 2.0 authorization flow with some MCP-specific modifications. Let's break down how it works:
### 1. OAuth Discovery Endpoint
First, the plugin helps you expose an OAuth discovery endpoint at `/.well-known/oauth-authorization-server` that provides metadata about the authorization server:
```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);
```
This endpoint returns standard OAuth metadata including:
- Authorization endpoint (`/mcp/authorize`)
- Token endpoint (`/mcp/token`)
- Supported scopes (`openid`, `profile`, `email`, `offline_access`)
- Supported response types (`code`)
- PKCE challenge methods (`S256`)
### 2. Authorization Flow
When an MCP client (like Claude Desktop) wants to connect to your server, it initiates the OAuth flow:
1. The client makes a request to your authorization endpoint with:
- `client_id`: Unique identifier for the client
- `redirect_uri`: Where to send the authorization code
- `response_type`: Always "code" for MCP
- `code_challenge`: PKCE challenge for security
- `scope`: Requested permissions (e.g. "openid profile")
2. If the client isn't registered yet (no `client_id`), it first needs to register using the dynamic client registration endpoint:
```ts
// Client sends POST request to /mcp/register
{
"redirect_uris": ["https://client.example.com/callback"],
"client_name": "My MCP Client",
"logo_uri": "https://client.example.com/logo.png",
"token_endpoint_auth_method": "client_secret_basic",
"grant_types": ["authorization_code"],
"response_types": ["code"],
"scope": "openid profile"
}
// Server validates and responds with:
{
"client_id": "generated-client-id",
"client_secret": "generated-client-secret",
"client_id_issued_at": 1683900000,
"client_secret_expires_at": 0
}
```
3. Once registered (or if already registered), if the user isn't logged in, they're redirected to your login page:
```ts
await ctx.setSignedCookie(
'oidc_login_prompt',
JSON.stringify(ctx.query),
ctx.context.secret,
{
maxAge: 600,
path: '/',
sameSite: 'lax',
}
);
throw ctx.redirect(`${options.loginPage}?${queryFromURL}`);
```
4. After login, the plugin validates:
- Client ID exists and is enabled
- Redirect URI matches registered URIs
- Requested scopes are valid
- PKCE challenge is present (if required)
5. If everything is valid, it generates an authorization code:
```ts
const code = generateRandomString(32, "a-z", "A-Z", "0-9");
const codeExpiresInMs = opts.codeExpiresIn * 1000;
const expiresAt = new Date(Date.now() + codeExpiresInMs);
```
### 3. Protecting Your MCP Server
The plugin provides a `withMcpAuth` middleware to protect your MCP server routes:
```ts
import { withMcpAuth } from "better-auth/plugins";
const handler = withMcpAuth(auth, (req, session) => {
// session contains the access token with scopes and user ID
return createMcpHandler(
(server) => {
// Define your MCP tools here
server.tool("echo", "Echo a message",
{ message: z.string() },
async ({ message }) => {
return {
content: [{ type: "text", text: message }],
};
}
);
},
// ... rest of your MCP config
)(req);
});
```
or you can use `auth.api.getMcpSession` to get the session from the request headers.
```ts
const session = await auth.api.getMcpSession({
headers: req.headers
});
```
Make sure to handle the unauthenticated case properly by returning a 401 status code.
```ts
if (!session) {
return new Response(null, {
status: 401,
headers: {
"WWW-Authenticate": "Bearer"
}
});
}
```
### 4. Configuration Options
The plugin is highly configurable through the `mcp()` function:
```ts
mcp({
loginPage: "/sign-in", // Where to redirect for auth
oidcConfig: {
codeExpiresIn: 600, // Auth code expiry in seconds
accessTokenExpiresIn: 3600, // Access token expiry
refreshTokenExpiresIn: 604800, // Refresh token expiry
scopes: ["openid", "profile", "email"], // Supported scopes
requirePKCE: true, // Require PKCE security
}
})
```
## Conclusion
The Better Auth MCP plugin provides a secure and flexible way to authenticate and authorize MCP clients. It handles the OAuth flow, client registration, and session management, allowing you to focus on building your MCP server.

View File

@@ -0,0 +1,10 @@
{
"title": "Blog",
"description": "Latest updates, articles, and insights about Better Auth",
"items": [
{
"title": "Latest",
"items": []
}
]
}

View File

@@ -0,0 +1,224 @@
---
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
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);
```
### 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 { withMcpAuth } from "better-auth/plugins";
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
},
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.