Examples

Authenticate MCP clients

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

Secure MCP endpoints with tokens

This recipe uses manually configured API keys, without OAuth discovery. Its soft authentication sets context on success and leaves each protected operation responsible for refusing unauthenticated access. A 401 can trigger OAuth discovery. For an OAuth resource server, that is intentional: return 401 with WWW-Authenticate and publish the protected-resource metadata. Nitro’s createMcpOAuth implements that separate flow.

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/api-key'

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/api-key/client'

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!.referenceId),
  })

  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
  • Leaves context as undefined when no valid token is provided
  • Tools must check for user context and return an error if not authenticated

Using Context in Tools

Your tools can access the authenticated user from event.context. Two patterns work well:

  1. Hide the tool with enabled — auth-required tools never appear in tools/list for anonymous callers (recommended).
  2. Return a user-friendly message — the tool stays visible, but explains how to authenticate when called without a user. Avoid throwing 401 from a tool handler so MCP clients don't enter OAuth discovery.
server/mcp/tools/create-todo.ts
import { z } from 'zod'
import { useEvent } from 'h3'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  name: 'create_todo',
  description: 'Create a new todo for the authenticated user',
  enabled: event => !!event.context.userId,
  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

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

    return `Todo created: ${todo.title}`
  },
})
server/mcp/tools/list-todos.ts
import { useEvent } from 'h3'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

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

    if (!userId) {
      return 'Authentication required. Configure your MCP client with an API key (Authorization: Bearer …) and try again.'
    }

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

    return todos
  },
})
The enabled guard runs after middleware, so it sees the user context set above. See Dynamic Definitions for more patterns.
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. For this API-key recipe, set user context on success and keep authorization guards on every protected operation:

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. Implement rate limiting - Prevent abuse with request limits per key
  4. Allow key revocation - Users should be able to delete compromised keys
  5. Log key usage - Track when keys are used for security auditing

Next Steps

Copyright © 2026