---
title: "Authenticate MCP clients"
description: "Secure your MCP endpoints with Bearer token authentication."
canonical_url: "https://mcp-toolkit.nuxt.dev/examples/authentication"
---
# 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

<prompt :actions="["copy","cursor","windsurf"]" description="Secure MCP endpoints with tokens" icon="i-lucide-shield-check">

Add authentication to my Nuxt MCP endpoints (@nuxtjs/mcp-toolkit).

1. Choose an auth strategy: Better Auth API keys or custom token validation
2. Create server/mcp/index.ts with defineMcpHandler and a middleware function
3. In the middleware, read the token from getHeader(event, 'authorization') or getHeader(event, 'x-api-key')
4. Validate the token and set event.context.user with the authenticated user
5. For this manually configured API-key recipe, use soft authentication and guard every protected operation; OAuth resource servers should return 401 with their metadata challenge
6. Access user context in tools via useEvent().context.user
7. Use the enabled guard on tools to hide them from unauthenticated users
8. Configure MCP clients to send the token in headers

Docs: [https://mcp-toolkit.nuxt.dev/examples/authentication](https://mcp-toolkit.nuxt.dev/examples/authentication)
Middleware: [https://mcp-toolkit.nuxt.dev/advanced/middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware)

</prompt>

<callout color="warning" icon="i-lucide-triangle-alert">

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

</callout>

## Using Better Auth API Keys

If you're using [Better Auth](https://www.better-auth.com), you can leverage the built-in [API Key plugin](https://www.better-auth.com/docs/plugins/api-key) for a complete solution.

### Server Configuration

Add the API Key plugin to your Better Auth configuration:

```typescript [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)
      },
    }),
  ],
})
```

<callout color="info" icon="i-lucide-info">

The API Key plugin has rate limiting enabled by default. Disable it for development or configure appropriate limits for production.

</callout>

### Client Configuration

Add the client plugin to use API key methods:

```typescript [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:

```typescript [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:

```typescript [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.

```typescript [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}`
  },
})
```

```typescript [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
  },
})
```

<callout color="info" icon="i-lucide-info">

The `enabled` guard runs after middleware, so it sees the user context set above. See [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) for more patterns.

</callout>

<callout color="info" icon="i-lucide-info">

Remember to enable `asyncContext` in your Nuxt config to use `useEvent()`:

```typescript [nuxt.config.ts]
export default defineNuxtConfig({
  nitro: {
    experimental: {
      asyncContext: true,
    },
  },
})
```

</callout>

## 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:

```typescript [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 }
}
```

```typescript [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`:

```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:

```json [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:

```typescript [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

- [Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) - Learn more about middleware options
- [Handlers](https://mcp-toolkit.nuxt.dev/handlers/overview) - Create custom authenticated handlers
- [TypeScript](https://mcp-toolkit.nuxt.dev/advanced/typescript) - Type-safe context definitions


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
