Advanced Topics

Add middleware to MCP handlers

Intercept MCP requests to add authentication, logging, analytics, and more.

What is Middleware?

Middleware allows you to run code before (and optionally after) MCP requests are processed.

Add MCP server middleware and auth

This is useful for:

  • Authentication - Validate tokens and set user context
  • Logging - Track request timing and analytics
  • Context - Pass data to your tools via event.context
  • Rate limiting - Control request frequency
  • Error handling - Wrap handlers with try/catch

Basic Usage

Add middleware to your handler using the middleware option:

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

export default defineMcpHandler({
  middleware: async (event) => {
    // Set context that tools can access
    event.context.userId = 'user-123'
    event.context.startTime = Date.now()
  },
})
If you don't call next(), the handler is called automatically after your middleware runs. This makes simple use cases straightforward.

Simple Middleware

For most cases, you just need to set context before the handler runs. Use a soft approach — set context only when authentication succeeds, and let unauthenticated requests continue:

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
  },
})
Do not throw 401 from MCP middleware. Many MCP clients treat a 401 as a signal to start OAuth discovery (looking for .well-known/oauth-* endpoints). Set context softly and use enabled guards or per-tool checks to gate functionality. See Authentication for the full pattern.

Your tools can then access this context:

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

export default defineMcpTool({
  name: 'my-tool',
  description: 'A tool that uses middleware context',
  inputSchema: {},
  handler: async () => {
    const event = useEvent()
    const user = event.context.user
    return `Hello, ${user.name}!`
  },
})
To use useEvent() in your tools, enable asyncContext in your Nuxt config:
nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    experimental: {
      asyncContext: true,
    },
  },
})

Advanced Middleware with next()

For more control, call next() explicitly to run code before and after the handler:

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] Request started:', event.path)

    // Call the handler
    const response = await next()

    // Code after the handler
    const duration = Date.now() - startTime
    console.log(`[MCP] Request completed in ${duration}ms`)

    return response
  },
})

When to use next()

Use CaseNeed next()?
Set context before handlerNo
Validate auth before handlerNo
Log request timingYes
Modify responseYes
Catch errorsYes

Authentication Example

Set the user on context when the token is valid; do nothing otherwise. Tools that require auth can check event.context.user and use enabled guards to hide themselves from anonymous callers:

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
  },
})

See the Authentication guide for a complete walkthrough (Better Auth API keys, custom validation, configuring clients).

Logging & Analytics Example

For structured wide events, drains (Axiom, Sentry, OTLP, Datadog…), and per-tool context, use useMcpLogger() instead of the manual console.log pattern below. The middleware example here is kept minimal so it doesn't depend on 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
  },
})

Extracting Tool Names

Use the extractToolNames utility to inspect which tools are being called in the current request. It parses the JSON-RPC body and returns the tool names from any tools/call messages.

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] Calling tools: ${toolNames.join(', ')}`)
    }

    return next()
  },
})

This is useful for:

  • Logging which tools are called per request
  • Monitoring tool usage and frequency
  • Access control based on tool names (e.g. restricting certain tools to admin users)
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: 'Admin access required for this tool' })
    }
  },
})
Throwing 403 for a specific in-flight tool call is safe — by the time tools/call arrives, the client has already initialized and won't enter OAuth discovery. The "no throw" rule applies to 401 on missing/invalid auth at the transport level.
extractToolNames is auto-imported in the server context — no import needed when using it in your server/ directory.

Middleware with Custom Handlers

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: 'Admin access required',
      })
    }

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

TypeScript

For type-safe context, extend the H3 context:

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

Now your middleware and tools will have typed context:

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 will validate this
    }
  },
})

Best Practices

  1. Keep middleware focused - Do one thing well
  2. Don't call next() if you don't need it - Let it be called automatically
  3. Always return next() result - If you call next(), return its result
  4. Handle errors gracefully - Use createError for HTTP errors
  5. Type your context - Extend H3EventContext for type safety

Next Steps

  • Handlers - Learn about custom handlers
  • TypeScript - Type-safe definitions
  • Tools - Create tools that use middleware context
Copyright © 2026