MCP
MCP provider plugin for Better Auth
OAuth MCP
This plugin will soon be deprecated in favor of the OAuth Provider Plugin.
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.
This plugin is based on OIDC Provider plugin. It'll be moved to the OAuth Provider Plugin in the future.
Installation
Add the Plugin
Add the MCP plugin to your auth configuration and specify the login page path.
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
})
]
});This doesn't have a client plugin, so you don't need to make any changes to your authClient.
Generate Schema
Run the migration or generate the schema to add the necessary fields and tables to the database.
npx auth migratenpx auth generateThe MCP plugin uses the same schema as the OIDC Provider plugin. See the OIDC Provider Schema section for details.
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:
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:
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.
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:
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:
Prop
Type
OIDC Configuration
The plugin supports additional OIDC configuration options through the oidcConfig parameter:
Prop
Type
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.
No additional packages needed — the MCP Client is included in better-auth.
Setup
Create the client
Point it at your Better Auth server's URL (the same baseURL + basePath from your auth config):
import { createMcpAuthClient } from "better-auth/plugins/mcp/client"
const mcpAuth = createMcpAuthClient({
authURL: "http://localhost:3000/api/auth"
}) Protect your MCP routes
Use the handler wrapper for Web Standard Request/Response (works with Deno, Bun, Cloudflare Workers, etc.):
const handler = mcpAuth.handler(async (req, session) => {
// session.userId, session.scopes, session.clientId
return new Response(JSON.stringify({
jsonrpc: "2.0",
result: { userId: session.userId },
id: 1
}))
})
Deno.serve(handler)Mount discovery endpoints
MCP clients need to discover your OAuth server. Mount these at the root of your MCP server:
const discovery = mcpAuth.discoveryHandler()
const protectedResource = mcpAuth.protectedResourceHandler("http://localhost:4000")
// GET /.well-known/oauth-authorization-server → proxied from Better Auth
// GET /.well-known/oauth-protected-resource → points MCP clients to Better AuthThese proxy and cache the metadata from your Better Auth server, so MCP clients can discover the authorization, token, and registration endpoints automatically.
Framework Adapters
import { Hono } from "hono"
import { mcpAuthHono } from "better-auth/plugins/mcp/client/adapters"
const app = new Hono()
const { middleware, discoveryRoutes } = mcpAuthHono({
authURL: "http://localhost:3000/api/auth"
})
// Mount OAuth discovery endpoints (required by MCP spec)
discoveryRoutes(app, "http://localhost:4000")
// Protect MCP routes
app.use("/mcp/*", middleware)
app.post("/mcp", (c) => {
const session = c.get("mcpSession")
// session.userId, session.scopes, etc.
})import express from "express"
import { createMcpAuthClient } from "better-auth/plugins/mcp/client"
const app = express()
const mcpAuth = createMcpAuthClient({
authURL: "http://localhost:3000/api/auth"
})
app.use("/mcp", mcpAuth.middleware())
app.post("/mcp", (req, res) => {
const session = req.mcpSession
// session.userId, session.scopes, etc.
})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"
const auth = mcpAuthOfficial({
authURL: "http://localhost:3000/api/auth"
})
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) => {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => crypto.randomUUID()
})
await mcpServer.connect(transport)
return transport.handleRequest(req)
}))Drop-in replacement for oauthWorkOSProvider, oauthSupabaseProvider, etc.:
import { MCPServer } from "mcp-use/server"
import { mcpAuthMcpUse } from "better-auth/plugins/mcp/client/adapters"
const server = new MCPServer({
name: "my-server",
version: "1.0.0",
oauth: mcpAuthMcpUse({
authURL: "http://localhost:3000/api/auth"
})
})Options
Prop
Type
Session Object
The session object returned by verifyToken and passed to handlers contains:
Prop
Type
Schema
The MCP plugin uses the same schema as the OIDC Provider plugin. See the OIDC Provider Schema section for details.