Examples

Authentication

Secure your MCP endpoints with Bearer token authentication.

Overview

MCP endpoints can be secured using Bearer token authentication. This guide shows how to:

  1. Generate and manage API keys for users
  2. Validate tokens in MCP middleware
  3. Access user context in your tools
  4. Configure MCP clients with authentication
Important: MCP middleware should not throw errors for missing or invalid authentication. Throwing a 401 error will cause MCP clients to enter OAuth discovery mode, looking for .well-known/oauth-* endpoints that don't exist. Instead, use a "soft" approach that sets context when auth succeeds but allows requests to continue otherwise.

Using Better Auth API Keys

If you're using Better Auth, you can leverage the built-in API Key plugin for a complete solution.

Server Configuration

Add the API Key plugin to your Better Auth configuration:

server/utils/auth.ts
import { betterAuth } from 'better-auth'
import { apiKey } from 'better-auth/plugins'

export const auth = betterAuth({
  // ... your existing config
  plugins: [
    apiKey({
      rateLimit: {
        enabled: false, // Disable rate limiting (if not needed)
      },
    }),
  ],
})
The API Key plugin has rate limiting enabled by default. Disable it for development or configure appropriate limits for production.

Client Configuration

Add the client plugin to use API key methods:

composables/auth.ts
import { createAuthClient } from 'better-auth/client'
import { apiKeyClient } from 'better-auth/client/plugins'

const client = createAuthClient({
  plugins: [
    apiKeyClient(),
  ],
})

// Create an API key
const { data } = await client.apiKey.create({ name: 'My MCP Key' })
console.log(data.key) // Save this - only shown once!

// List API keys
const { data: keys } = await client.apiKey.list()

// Delete an API key
await client.apiKey.delete({ keyId: 'key-id' })

Helper Function

Create a helper function that validates API keys without throwing errors:

server/utils/auth.ts
export async function getApiKeyUser(event: H3Event) {
  const authHeader = getHeader(event, 'authorization')

  if (!authHeader?.startsWith('Bearer ')) {
    return null
  }

  const key = authHeader.slice(7)
  const result = await auth.api.verifyApiKey({ body: { key } })

  if (!result.valid || !result.key) {
    return null
  }

  const user = await db.query.user.findFirst({
    where: (users, { eq }) => eq(users.id, result.key!.userId),
  })

  if (!user) {
    return null
  }

  return { user, apiKey: result.key }
}

MCP Handler with Authentication

Create a handler that sets user context when a valid API key is provided:

server/mcp/index.ts
export default defineMcpHandler({
  middleware: async (event) => {
    const result = await getApiKeyUser(event)
    if (result) {
      event.context.user = result.user
      event.context.userId = result.user.id
    }
  },
})

This approach:

  • Sets event.context.user and event.context.userId when authentication succeeds
  • Allows requests without authentication to continue (context will be undefined)
  • Tools can check for user context and handle unauthorized access as needed

Using Context in Tools

Your tools can access the authenticated user from event.context. For tools that require authentication, check if the user exists:

server/mcp/tools/create-todo.ts
export default defineMcpTool({
  name: 'create_todo',
  description: 'Create a new todo for the authenticated user',
  inputSchema: {
    title: z.string().describe('The title of the todo'),
    content: z.string().optional().describe('Optional description or content'),
  },
  handler: async ({ title, content }) => {
    const event = useEvent()
    const userId = event.context.userId as string

    if (!userId) {
      return textResult('Authentication required. Please provide a valid API key.')
    }

    const [todo] = await db.insert(schema.todos).values({
      title,
      content: content || null,
      userId,
      createdAt: new Date(),
      updatedAt: new Date(),
    }).returning()

    return textResult(`Todo created: ${todo.title}`)
  },
})

For tools that work with or without authentication:

server/mcp/tools/list-todos.ts
export default defineMcpTool({
  name: 'list_todos',
  description: 'List all todos for the authenticated user',
  inputSchema: {},
  handler: async () => {
    const event = useEvent()
    const userId = event.context.userId as string

    if (!userId) {
      return textResult('Authentication required. Please provide a valid API key.')
    }

    const todos = await db.query.todos.findMany({
      where: (todos, { eq }) => eq(todos.userId, userId),
    })

    return textResult(JSON.stringify(todos, null, 2))
  },
})
Remember to enable asyncContext in your Nuxt config to use useEvent():
nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    experimental: {
      asyncContext: true,
    },
  },
})

Custom Token Validation

If you're not using Better Auth, you can implement your own token validation. Remember to use a soft approach that doesn't throw errors:

server/utils/auth.ts
import { createHash } from 'node:crypto'

export async function getTokenUser(event: H3Event) {
  const authHeader = getHeader(event, 'authorization')

  if (!authHeader?.startsWith('Bearer ')) {
    return null
  }

  const token = authHeader.slice(7)
  const tokenHash = createHash('sha256').update(token).digest('hex')

  // Look up the token in your database
  const apiToken = await db.query.apiTokens.findFirst({
    where: (tokens, { eq }) => eq(tokens.hash, tokenHash),
  })

  if (!apiToken) {
    return null
  }

  // Check expiration
  if (apiToken.expiresAt && apiToken.expiresAt < new Date()) {
    return null
  }

  return { userId: apiToken.userId }
}
server/mcp/index.ts
export default defineMcpHandler({
  middleware: async (event) => {
    const result = await getTokenUser(event)
    if (result) {
      event.context.userId = result.userId
    }
  },
})

Configuring MCP Clients

Cursor

Add your MCP server to .cursor/mcp.json:

.cursor/mcp.json
{
  "mcpServers": {
    "my-app": {
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer your-api-key-here"
      }
    }
  }
}

Claude Desktop

Add to your Claude Desktop configuration:

claude_desktop_config.json
{
  "mcpServers": {
    "my-app": {
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer your-api-key-here"
      }
    }
  }
}

Other Clients

Most MCP clients support custom headers. Check your client's documentation for the exact configuration format.

TypeScript

For type-safe context, extend the H3 event context:

server/types.ts
declare module 'h3' {
  interface H3EventContext {
    user?: {
      id: string
      name: string
      email: string
    }
    userId?: string
  }
}

Security Best Practices

  1. Always hash tokens - Store hashed tokens in your database, not plaintext
  2. Set expiration dates - API keys should expire to limit exposure
  3. Use HTTPS - Always use HTTPS in production to protect tokens in transit
  4. Implement rate limiting - Prevent abuse with request limits per key
  5. Allow key revocation - Users should be able to delete compromised keys
  6. Log key usage - Track when keys are used for security auditing

Next Steps