Advanced Topics

Dynamic Definitions

Conditionally register tools, resources, and prompts based on authentication, roles, or request context.

Overview

By default, every tool, resource, and prompt defined in server/mcp/ is registered for all clients. Dynamic definitions let you control which definitions are visible based on request context — for example, showing admin-only tools to authenticated admins while hiding them from regular users.

There are two complementary mechanisms:

  1. enabled guard — A per-definition callback that controls visibility
  2. Dynamic handler definitions — A function in defineMcpHandler that returns definitions based on context

Both mechanisms run after middleware, so event.context (e.g. authentication data) is available.

If you only need to change tool/prompt behavior (not visibility), you can already do that by reading event.context inside your handler via useEvent(). Dynamic definitions are for controlling which definitions appear in tools/list, prompts/list, and resources/list.

The enabled Guard

Add an enabled callback to any tool, resource, or prompt definition. When the callback returns false, the definition is hidden from the client.

Tools

server/mcp/tools/delete-all.ts
export default defineMcpTool({
  name: 'delete-all',
  description: 'Delete all records (admin only)',
  inputSchema: {
    confirm: z.boolean().describe('Confirm deletion'),
  },
  enabled: event => event.context.user?.role === 'admin',
  handler: async ({ confirm }) => {
    if (!confirm) return textResult('Deletion cancelled')
    await deleteAllRecords()
    return textResult('All records deleted')
  },
})

Resources

server/mcp/resources/internal-logs.ts
export default defineMcpResource({
  name: 'internal-logs',
  description: 'Application logs (admin only)',
  uri: 'app://logs',
  enabled: event => event.context.user?.role === 'admin',
  handler: async (uri) => ({
    contents: [{ uri: uri.toString(), text: await readLogs() }],
  }),
})

Prompts

server/mcp/prompts/onboarding.ts
export default defineMcpPrompt({
  name: 'onboarding',
  description: 'Personalized onboarding (authenticated users only)',
  enabled: event => !!event.context.user,
  handler: async () => {
    const event = useEvent()
    return {
      messages: [{
        role: 'user',
        content: {
          type: 'text',
          text: `Welcome ${event.context.user.name}! Here's how to get started...`,
        },
      }],
    }
  },
})

Middleware Setup

The enabled guard runs after middleware, so set up your auth context in middleware:

server/mcp/index.ts
export default defineMcpHandler({
  middleware: async (event) => {
    const token = getHeader(event, 'authorization')?.replace('Bearer ', '')
    if (token) {
      event.context.user = await verifyToken(token)
    }
  },
})
To use useEvent() inside handlers, enable asyncContext in your Nuxt config:
nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    experimental: {
      asyncContext: true,
    },
  },
})

Dynamic Handler Definitions

For more control, pass a function as tools, resources, or prompts in defineMcpHandler. The function receives the H3 event and returns an array of definitions.

server/mcp/index.ts
import { adminTools } from './admin-tools'
import { publicTools } from './public-tools'

export default defineMcpHandler({
  middleware: async (event) => {
    event.context.user = await getUser(event)
  },
  tools: async (event) => {
    const base = [...publicTools]
    if (event.context.user?.role === 'admin') {
      base.push(...adminTools)
    }
    return base
  },
  prompts: async (event) => {
    if (event.context.user) {
      return [authenticatedPrompt, dashboardPrompt]
    }
    return [guestPrompt]
  },
})

This is useful when you need to:

  • Build the tool list programmatically from a database or config
  • Compose definitions from multiple modules
  • Apply complex filtering logic

Combining Both Approaches

The enabled guard and dynamic handler definitions work together. When you use dynamic handler definitions, each returned definition's enabled guard is still evaluated:

server/mcp/index.ts
export default defineMcpHandler({
  middleware: async (event) => {
    event.context.user = await getUser(event)
  },
  tools: async (event) => {
    const allTools = await loadToolsFromConfig()
    return allTools
  },
})
server/mcp/tools/admin-delete.ts
export default defineMcpTool({
  name: 'admin-delete',
  enabled: event => event.context.user?.role === 'admin',
  handler: async () => { /* ... */ },
})

In this case, admin-delete is loaded by auto-discovery (or dynamically) and filtered by its enabled guard.

Session Behavior

When sessions are enabled, the MCP server is created on the first request of a session. Dynamic definitions are resolved at that point, and the same tool set persists for the session's lifetime.

This means:

  • An admin who connects gets admin tools for the entire session
  • A regular user who connects never sees admin tools, even if they gain admin access mid-session
  • Different sessions can have different tool sets

Without sessions, a new server is created per request, so definitions can vary per request.

TypeScript

For type-safe context, extend the H3 event context:

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

Next Steps

Copyright © 2026