# Better Auth MCP Server This project is a Model Context Protocol (MCP) server built with the HONC stack (Hono, OpenAPI, Neon, Cloudflare) that provides authenticated access to user data through MCP tools. The server implements GitHub OAuth authentication using better-auth and exposes user information and GitHub account details through standardized MCP tool interfaces. It runs as a Cloudflare Worker with Durable Objects to maintain MCP agent state. The application combines a traditional REST API built with Hono framework alongside MCP server functionality, demonstrating how to integrate authentication flows with the Model Context Protocol. The better-auth library handles session management and OAuth flows, while the MCP SDK provides the protocol implementation. The server stores user, session, and OAuth data in a PostgreSQL database (Neon) using Drizzle ORM, and leverages Cloudflare's edge infrastructure for global distribution. ## MCP Server Configuration Connect to the remote MCP server using the SSE endpoint ```json { "mcpServers": { "better-auth-mcp": { "command": "npx", "args": [ "mcp-remote", "https://better-auth-mcp.cjjdxhdjd.workers.dev/sse" ] } } } ``` ## OAuth Discovery Endpoint Returns OAuth 2.0 authorization server metadata for MCP clients ```typescript // GET /.well-known/oauth-authorization-server import { Hono } from "hono"; import { auth } from "./lib/auth"; import { oAuthDiscoveryMetadata } from "better-auth/plugins"; const router = new Hono(); router.get("/.well-known/oauth-authorization-server", async (c) => { const handler = oAuthDiscoveryMetadata(auth); return handler(c.req.raw); }); // Response example: // { // "issuer": "https://better-auth-mcp.cjjdxhdjd.workers.dev", // "authorization_endpoint": "https://better-auth-mcp.cjjdxhdjd.workers.dev/api/auth/authorize", // "token_endpoint": "https://better-auth-mcp.cjjdxhdjd.workers.dev/api/auth/token", // "scopes_supported": ["openid", "profile", "email"] // } ``` ## Authentication Configuration Configure better-auth with GitHub OAuth and MCP plugin support ```typescript // src/lib/auth.ts import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { mcp, openAPI } from "better-auth/plugins"; import * as schema from "@/db/schema"; import { db } from "@/db"; export const auth = betterAuth({ baseURL: process.env.BETTER_AUTH_URL, secret: process.env.BETTER_AUTH_SECRET, database: drizzleAdapter(db, { provider: "pg", usePlural: true, schema, }), socialProviders: { github: { clientId: process.env.GITHUB_CLIENT_ID, clientSecret: process.env.GITHUB_CLIENT_SECRET, }, }, plugins: [ openAPI(), mcp({ loginPage: "/sign-in", }), ], }); // Required environment variables: // DATABASE_URL=postgresql://user:pass@host/db // GITHUB_CLIENT_ID=your_github_oauth_app_id // GITHUB_CLIENT_SECRET=your_github_oauth_app_secret // BETTER_AUTH_SECRET=random_32_char_string // BETTER_AUTH_URL=https://your-domain.workers.dev ``` ## MCP Agent with Custom Tools Implement MCP agent with authentication-aware tools using Durable Objects ```typescript // src/mcp.ts import { McpAgent } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { OAuthAccessToken, withMcpAuth } from "better-auth/plugins"; import { Octokit } from "@octokit/rest"; import { db } from "@/db"; import { eq, and } from "drizzle-orm"; import { accounts, users } from "@/db/schema"; export class MyAgent extends McpAgent { server = new McpServer({ name: "Mcp Server", version: "1.0.0", }); async init() { // Tool to retrieve authenticated user details from database this.server.tool( "user-details", "Returns information about the authenticated user stored in database.", {}, async () => { const userId = this.props.session.userId; const user = await db.query.users.findFirst({ where: eq(users.id, userId), }); return { content: [ { type: "text", text: JSON.stringify({ ...user }), }, ], }; } ); // Tool to retrieve GitHub account information using stored access token this.server.tool( "github-details", "Returns github account information for the authenticated user", {}, async () => { const account = await db.query.accounts.findFirst({ where: and( eq(accounts.userId, this.props.session.userId), eq(accounts.providerId, "github") ), }); const octokit = new Octokit({ auth: account?.accessToken }); const user = await octokit.users.getAuthenticated(); return { content: [ { type: "text", text: JSON.stringify({ ...user.data }), }, ], }; } ); } } // MCP SSE endpoint with authentication wrapper const router = new Hono(); router.all("/sse/*", (c) => { const agent = (req: Request, session: OAuthAccessToken) => { (c.executionCtx as any).props = { session, }; const fetch = MyAgent.mount("/sse").fetch; return fetch(req, c.env, c.executionCtx); }; const handler = withMcpAuth(auth, agent); return handler(c.req.raw); }); // Tool usage from MCP client: // user-details → { id: "user_123", name: "John Doe", email: "john@example.com", emailVerified: true, ... } // github-details → { login: "johndoe", id: 12345, name: "John Doe", public_repos: 50, followers: 100, ... } ``` ## Session Middleware Attach user and session data to request context for authenticated routes ```typescript // src/middleware/with-session.ts import { auth } from "@/lib/auth"; import { AppBindings } from "@/types"; import { MiddlewareHandler } from "hono"; const withSession: MiddlewareHandler = async (c, next) => { const session = await auth.api.getSession({ headers: c.req.raw.headers }); if (!session) { c.set("user", null); c.set("session", null); return next(); } c.set("user", session.user); c.set("session", session.session); return next(); }; export default withSession; // Usage in routes: // router.get("/@me", async (c) => { // if (!c.var.user) { // return c.text("NOT AUTHENTICATED"); // } // return c.json(c.var.user); // }); ``` ## User API Routes RESTful endpoints for user data access with session-based authentication ```typescript // src/routes/user.ts import { db } from "@/db"; import { users } from "@/db/schema"; import { eq } from "drizzle-orm"; import { Hono } from "hono"; const router = new Hono(); // Get user by ID router.get("/:id", async (c) => { const { id } = c.req.param(); const user = await db.query.users.findFirst({ where: eq(users.id, id), }); return c.json(user); }); // Get current authenticated user router.get("/@me", async (c) => { if (!c.var.user) { return c.text("NOT AUTHENTICATED"); } return c.json(c.var.user); }); // Example requests: // curl https://your-domain.workers.dev/user/user_123 // → {"id":"user_123","name":"John Doe","email":"john@example.com","emailVerified":true,"image":"https://...","createdAt":"...","updatedAt":"..."} // curl -H "Cookie: better-auth.session_token=..." https://your-domain.workers.dev/user/@me // → {"id":"user_123","name":"John Doe",...} ``` ## Authentication Routes OAuth sign-in and sign-out endpoints with GitHub provider ```typescript // src/routes/auth.tsx import { auth } from "@/lib/auth"; import { Hono } from "hono"; import { html } from "hono/html"; const authRouter = new Hono(); authRouter.get("/sign-in", async (c) => { const githubAuthUrlResult = await auth.api.signInSocial({ body: { provider: "github", }, request: c.req.raw, }); const oauthUrl = githubAuthUrlResult.url; return c.html(html`

Sign In

Please sign in to authorize the application.

Sign in with GitHub
`); }); authRouter.get("/sign-out", async (c) => { await auth.api.signOut({ headers: c.req.raw.headers, }); return c.text("Signed Out"); }); // OAuth flow: // 1. User visits /sign-in // 2. Clicks GitHub button → redirects to GitHub OAuth // 3. GitHub redirects to /api/auth/callback/github with code // 4. Server exchanges code for tokens, creates session // 5. User can access authenticated MCP tools and API endpoints ``` ## Database Schema Drizzle ORM schema defining user, session, and OAuth tables ```typescript // src/db/schema.ts import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), emailVerified: boolean("email_verified").$defaultFn(() => false).notNull(), image: text("image"), createdAt: timestamp("created_at").$defaultFn(() => new Date()).notNull(), updatedAt: timestamp("updated_at").$defaultFn(() => new Date()).notNull(), }); export const sessions = pgTable("sessions", { id: text("id").primaryKey(), expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), createdAt: timestamp("created_at").notNull(), updatedAt: timestamp("updated_at").notNull(), ipAddress: text("ip_address"), userAgent: text("user_agent"), userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), }); export const accounts = pgTable("accounts", { id: text("id").primaryKey(), accountId: text("account_id").notNull(), providerId: text("provider_id").notNull(), userId: text("user_id") .notNull() .references(() => users.id, { onDelete: "cascade" }), accessToken: text("access_token"), refreshToken: text("refresh_token"), idToken: text("id_token"), accessTokenExpiresAt: timestamp("access_token_expires_at"), refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), createdAt: timestamp("created_at").notNull(), updatedAt: timestamp("updated_at").notNull(), }); // Database setup: // pnpm db:generate # Generate migrations // pnpm db:push # Push schema to database ``` ## Application Entry Point Main Hono application with CORS, authentication, and MCP routing ```typescript // src/index.ts import { createFiberplane, createOpenAPISpec } from "@fiberplane/hono"; import { auth } from "@/lib/auth"; import { Hono } from "hono"; import { cors } from "hono/cors"; import withSession from "@/middleware/with-session"; import userRouter from "@/routes/user"; import authRouter from "@/routes/auth"; import mcpRouter, { MyAgent } from "@/mcp"; const app = new Hono(); app.use("*", cors({ origin: "*", allowHeaders: ["Content-Type", "Authorization"], allowMethods: ["POST", "GET", "OPTIONS"], exposeHeaders: ["Content-Length"], maxAge: 600, credentials: true, })); app.use("*", withSession); // Handle better-auth MCP plugin redirect behavior app.use("/api/auth/*", async (c, next) => { await next(); if (c.res.status === 200 && c.req.path.startsWith("/api/auth/callback/")) { const clonedResponse = c.res.clone(); const jsonData = await clonedResponse.json() as { redirect: boolean; url: string }; if (jsonData && jsonData.redirect === true) { c.res = c.redirect(jsonData.url, 302); } } }); app.on(["POST", "GET"], "/api/auth/*", (c) => { return auth.handler(c.req.raw); }); app.route("/", mcpRouter); app.route("/", authRouter); app.route("/user", userRouter); export { MyAgent }; export default app; // Deploy to Cloudflare: // pnpm wrangler secret bulk .dev.vars # Set environment variables // pnpm deploy # Deploy to Cloudflare Workers ``` ## Cloudflare Worker Configuration Wrangler configuration for Durable Objects and MCP agent deployment ```jsonc // wrangler.jsonc { "$schema": "node_modules/wrangler/config-schema.json", "name": "better-auth-mcp", "main": "src/index.ts", "compatibility_date": "2025-05-05", "compatibility_flags": [ "nodejs_compat", "nodejs_compat_populate_process_env" ], "migrations": [ { "new_sqlite_classes": ["MyAgent"], "tag": "v1" } ], "durable_objects": { "bindings": [ { "class_name": "MyAgent", "name": "MCP_OBJECT" } ] }, "keep_vars": true, "dev": { "port": 3000 } } // Local development: // pnpm dev → runs on http://localhost:3000 ``` ## Summary This MCP server enables AI assistants and other MCP clients to securely access user data and GitHub account information through authenticated tool calls. The primary use cases include providing AI models with contextual user information, enabling personalized responses based on GitHub profile data, and demonstrating secure authentication patterns for MCP implementations. The server can be extended with additional tools to expose more user data or integrate with other OAuth providers beyond GitHub. The project demonstrates best practices for integrating authentication with the Model Context Protocol, including proper session management, OAuth token storage and refresh, and secure tool execution. The architecture separates concerns between the REST API layer (for traditional web clients) and the MCP layer (for AI assistants), while sharing the same authentication infrastructure. This pattern can be adapted for building authenticated MCP servers with various backend services, making it suitable for scenarios requiring secure, user-scoped data access in AI applications.