进阶主题

Add middleware to MCP handlers

拦截 MCP 请求以添加身份验证、日志记录、分析等功能。

什么是中间件?

中间件允许你在 MCP 请求被处理之前(以及可选的之后)运行代码。

添加 MCP 服务器中间件和身份验证

这适用于以下场景:

  • 身份验证 - 验证令牌并设置用户上下文
  • 日志记录 - 跟踪请求耗时和分析数据
  • 上下文 - 通过 event.context 将数据传递给工具
  • 速率限制 - 控制请求频率
  • 错误处理 - 使用 try/catch 包装处理器

基本用法

使用 middleware 选项将中间件添加到你的处理器中:

server/mcp/index.ts
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event) => {
    // 设置工具可访问的上下文
    event.context.userId = 'user-123'
    event.context.startTime = Date.now()
  },
})
如果你不调用 next(),处理器将在你的中间件运行后自动被调用。这使得简单的用例更加直观。

简单中间件

对于大多数情况,你只需要在处理器运行之前设置上下文。采用处理方式——仅在认证成功时设置上下文,并允许未认证请求继续:

server/mcp/index.ts
import { getHeader } from 'h3'
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event) => {
    const apiKey = getHeader(event, 'x-api-key')
    if (!apiKey) return

    const user = await validateApiKey(apiKey).catch(() => null)
    if (!user) return

    event.context.apiKey = apiKey
    event.context.user = user
  },
})
不要在 MCP 中间件中抛出 401 许多 MCP 客户端会将 401 视为启动 OAuth 发现(查找 .well-known/oauth-* 端点)的信号。请以“软”方式设置上下文,并使用enabled 守卫或按工具检查来控制功能。完整模式请参见身份验证

你的工具随后可以访问这些上下文:

server/mcp/tools/my-tool.ts
import { useEvent } from 'h3'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  name: 'my-tool',
  description: '使用中间件上下文的工具',
  inputSchema: {},
  handler: async () => {
    const event = useEvent()
    const user = event.context.user
    return `Hello, ${user.name}!`
  },
})
要在你的工具中使用 useEvent(),请在 Nuxt 配置中启用 asyncContext
nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    experimental: {
      asyncContext: true,
    },
  },
})

使用 next() 的高级中间件

为了获得更精细的控制,可以显式调用 next() 以在处理器之前和之后运行代码:

server/mcp/index.ts
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event, next) => {
    const startTime = Date.now()
    console.log('[MCP] 请求开始:', event.path)

    // 调用处理器
    const response = await next()

    // 处理器执行后的代码
    const duration = Date.now() - startTime
    console.log(`[MCP] 请求在 ${duration}ms 内完成`)

    return response
  },
})

何时使用 next()

使用场景需要 next()
在处理器之前设置上下文
在处理器之前验证身份
记录请求耗时
修改响应
捕获错误

身份验证示例

当令牌有效时,将用户设置到上下文中;否则不执行任何操作。需要身份验证的工具可以检查 event.context.user,并使用enabled 守卫对匿名调用者隐藏自身:

server/mcp/index.ts
import { getHeader } from 'h3'
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event) => {
    const authHeader = getHeader(event, 'authorization')
    if (!authHeader?.startsWith('Bearer ')) return

    const token = authHeader.slice(7)
    const user = await verifyToken(token).catch(() => null)
    if (!user) return

    event.context.user = user
    event.context.userId = user.id
  },
})

请参阅身份验证指南获取完整演练(Better Auth API 密钥、自定义验证、客户端配置)。

日志和分析示例

对于结构化的广泛事件、汇聚器(Axiom、Sentry、OTLP、Datadog 等)以及按工具的上下文,请使用useMcpLogger()而不是下面的手动 console.log 模式。此处的中间件示例保持最小化,因此它不依赖 evlog
server/mcp/index.ts
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event, next) => {
    const requestId = crypto.randomUUID()
    const startTime = Date.now()

    event.context.requestId = requestId

    console.log(JSON.stringify({
      type: 'mcp_request_start',
      requestId,
      path: event.path,
      method: event.method,
      timestamp: new Date().toISOString(),
    }))

    const response = await next()

    console.log(JSON.stringify({
      type: 'mcp_request_end',
      requestId,
      duration: Date.now() - startTime,
      timestamp: new Date().toISOString(),
    }))

    return response
  },
})

提取工具名称

使用 extractToolNames 工具来检查当前请求中调用了哪些工具。它会解析 JSON-RPC 请求体,并返回任何 tools/call 消息中的工具名称。

server/mcp/index.ts
import { defineMcpHandler, extractToolNames } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event, next) => {
    const toolNames = await extractToolNames(event)

    if (toolNames.length > 0) {
      console.log(`[MCP] 正在调用工具:${toolNames.join(', ')}`)
    }

    return next()
  },
})

这适用于以下场景:

  • 日志记录 每次请求调用了哪些工具
  • 监控 工具的使用情况和频率
  • 访问控制 基于工具名称(例如,将某些工具限制为仅管理员可用)
server/mcp/index.ts
import { createError } from 'h3'
import { defineMcpHandler, extractToolNames } from '@nuxtjs/mcp-toolkit/server'

const ADMIN_TOOLS = ['delete-user', 'reset-database']

export default defineMcpHandler({
  middleware: async (event) => {
    const toolNames = await extractToolNames(event)
    const user = event.context.user

    if (toolNames.some(name => ADMIN_TOOLS.includes(name)) && user?.role !== 'admin') {
      throw createError({ statusCode: 403, message: '此工具需要管理员权限' })
    }
  },
})
对于特定的进行中工具调用,抛出 403 是安全的——当 tools/call 到达时,客户端已经完成初始化,不会进入 OAuth 发现流程。“不抛出”规则适用于在传输层缺少/无效认证时抛出 401
extractToolNames 在服务器上下文中是自动导入的 —— 在 server/ 目录中使用时无需手动导入。

自定义处理器的中间件

Middleware works the same way with custom handlers. For an admin-only endpoint mounted at a non-discovery route (e.g. /mcp/admin), throwing 403 is safe — the client targeted this route explicitly:

server/mcp/admin.ts
import { createError } from 'h3'
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  name: 'admin',
  middleware: async (event) => {
    const user = await getUser(event)

    if (user?.role !== 'admin') {
      throw createError({
        statusCode: 403,
        message: '需要管理员权限',
      })
    }

    event.context.user = user
  },
  tools: [adminTool1, adminTool2],
})

TypeScript

为了实现类型安全的上下文,请扩展 H3 上下文:

server/types.ts
declare module 'h3' {
  interface H3EventContext {
    user?: {
      id: string
      name: string
      role: 'user' | 'admin'
    }
    requestId?: string
    startTime?: number
  }
}

现在你的中间件和工具将拥有带类型的上下文:

server/mcp/index.ts
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpHandler({
  middleware: async (event) => {
    event.context.user = {
      id: 'user-123',
      name: 'John',
      role: 'admin', // TypeScript 会对此进行验证
    }
  },
})

最佳实践

  1. 保持中间件专注 - 做好一件事
  2. 不需要时不要调用 next() - 让它自动调用
  3. 始终返回 next() 的结果 - 如果你调用了 next(),请返回它的结果
  4. 优雅地处理错误 - 使用 createError 处理 HTTP 错误
  5. 为上下文添加类型 - 扩展 H3EventContext 以确保类型安全

下一步

  • Handlers - 了解自定义处理器
  • TypeScript - 类型安全的定义
  • Tools - 创建使用中间件上下文的工具