示例
验证 MCP 客户端
使用 Bearer 令牌身份验证保护您的 MCP 端点。
概述
可以使用 Bearer 令牌身份验证来保护 MCP 端点。本指南将展示如何:
- 为用户生成和管理 API 密钥
- 在 MCP 中间件中验证令牌
- 在您的工具中访问用户上下文
- 配置带有身份验证的 MCP 客户端
使用令牌保护 MCP 端点
重要提示: 对于缺失或无效的身份验证,MCP 中间件不应抛出错误。抛出 401 错误会导致 MCP 客户端进入 OAuth 发现模式,去寻找不存在的
.well-known/oauth-* 端点。相反,请使用一种“温和”的方法:在身份验证成功时设置上下文,否则允许请求继续。使用 Better Auth API 密钥
如果您正在使用 Better Auth,可以利用内置的 API Key 插件 获得完整的解决方案。
服务器配置
将 API Key 插件添加到您的 Better Auth 配置中:
server/utils/auth.ts
import { betterAuth } from 'better-auth'
import { apiKey } from '@better-auth/api-key'
export const auth = betterAuth({
// ... 您现有的配置
plugins: [
apiKey({
rateLimit: {
enabled: false, // 禁用速率限制(如果不需要)
},
}),
],
})
API Key 插件默认启用了速率限制。请在开发环境中禁用它,或为生产环境配置适当的限制。
客户端配置
添加客户端插件以使用 API 密钥方法:
composables/auth.ts
import { createAuthClient } from 'better-auth/client'
import { apiKeyClient } from '@better-auth/api-key/client'
const client = createAuthClient({
plugins: [
apiKeyClient(),
],
})
// 创建 API 密钥
const { data } = await client.apiKey.create({ name: 'My MCP Key' })
console.log(data.key) // 保存此密钥 - 仅显示一次!
// 列出 API 密钥
const { data: keys } = await client.apiKey.list()
// 删除 API 密钥
await client.apiKey.delete({ keyId: 'key-id' })
辅助函数
创建一个验证 API 密钥且不抛出错误的辅助函数:
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 处理程序
创建一个处理程序,在提供有效 API 密钥时设置用户上下文:
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
}
},
})
此方法:
- 在身份验证成功时设置
event.context.user和event.context.userId - 当未提供有效令牌时,将上下文保留为
undefined - 工具必须检查用户上下文,如果未通过身份验证则返回错误
在工具中使用上下文
您的工具可以从 event.context 访问已验证的用户。以下两种模式效果很好:
- 使用
enabled隐藏工具 —— 需要身份验证的工具不会出现在匿名调用者的tools/list中(推荐)。 - 返回对用户友好的消息 —— 工具仍然可见,但在没有用户上下文时会说明如何进行身份验证。避免在工具处理程序中抛出
401,以免 MCP 客户端进入 OAuth 发现。
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: '为已验证用户创建新的待办事项',
enabled: event => !!event.context.userId,
inputSchema: {
title: z.string().describe('待办事项的标题'),
content: z.string().optional().describe('可选的描述或内容'),
},
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 已创建:${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: '列出已验证用户的所有待办事项',
inputSchema: {},
handler: async () => {
const event = useEvent()
const userId = event.context.userId as string | undefined
if (!userId) {
return '需要身份验证。请使用 API 密钥(Authorization: Bearer …)配置您的 MCP 客户端,然后重试。'
}
const todos = await db.query.todos.findMany({
where: (todos, { eq }) => eq(todos.userId, userId),
})
return todos
},
})
enabled 守卫在中间件之后运行,因此它可以看到上面设置的用户上下文。更多模式请参见 Dynamic Definitions。请记得在 Nuxt 配置中启用
asyncContext 以使用 useEvent():nuxt.config.ts
export default defineNuxtConfig({
nitro: {
experimental: {
asyncContext: true,
},
},
})
自定义令牌验证
如果您没有使用 Better Auth,可以实现自己的令牌验证。请记住使用不抛出错误的“温和”方法:
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')
// 在您的数据库中查找令牌
const apiToken = await db.query.apiTokens.findFirst({
where: (tokens, { eq }) => eq(tokens.hash, tokenHash),
})
if (!apiToken) {
return null
}
// 检查过期时间
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
}
},
})
配置 MCP 客户端
Cursor
将您的 MCP 服务器添加到 .cursor/mcp.json:
.cursor/mcp.json
{
"mcpServers": {
"my-app": {
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}
}
Claude Desktop
添加到您的 Claude Desktop 配置中:
claude_desktop_config.json
{
"mcpServers": {
"my-app": {
"url": "http://localhost:3000/mcp",
"headers": {
"Authorization": "Bearer your-api-key-here"
}
}
}
}
其他客户端
大多数 MCP 客户端支持自定义请求头。请查阅您所用客户端的文档以获取确切的配置格式。
TypeScript
为了实现类型安全的上下文,请扩展 H3 事件上下文:
server/types.ts
declare module 'h3' {
interface H3EventContext {
user?: {
id: string
name: string
email: string
}
userId?: string
}
}
安全最佳实践
- 始终对令牌进行哈希处理 - 在数据库中存储哈希后的令牌,而非明文
- 设置过期日期 - API 密钥应设置过期时间以限制暴露风险
- 实施速率限制 - 通过每个密钥的请求限制来防止滥用
- 允许撤销密钥 - 用户应能够删除已泄露的密钥
- 记录密钥使用情况 - 跟踪密钥的使用时间以便进行安全审计
后续步骤
- Middleware - 了解更多中间件选项
- Handlers - 创建自定义的已验证处理程序
- TypeScript - 类型安全的上下文定义