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