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
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

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 'Deletion cancelled'
    await deleteAllRecords()
    return 'All records deleted'
  },
})

Resources

server/mcp/resources/internal-logs.ts
import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server'

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
import { useEvent } from 'h3'
import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server'

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
import { getHeader } from 'h3'
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

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 { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'
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
import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server'

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
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

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.

Mid-Session Mutations

The enabled guard and dynamic handler definitions control which definitions are registered when the session starts. For cases where you need to add, remove, or update definitions during an active session, use the useMcpServer() composable.

useMcpServer() returns a helper with methods to register, remove, and manage tools, prompts, and resources. The SDK automatically sends notifications/tools/list_changed (or the equivalent for prompts/resources) to the client, prompting it to re-fetch the list.

API

MethodDescription
registerTool(name, config, handler)Register a new tool. Returns a RegisteredTool handle.
registerPrompt(name, config, handler)Register a new prompt. Returns a RegisteredPrompt handle.
registerResource(name, uri, config, handler)Register a new resource. Returns a RegisteredResource handle.
removeTool(name)Remove a tool by name. Returns true if found.
removePrompt(name)Remove a prompt by name. Returns true if found.
removeResource(name)Remove a resource by name. Returns true if found.
serverThe underlying McpServer instance for advanced SDK operations.

Registering a Tool Mid-Session

server/mcp/tools/create-shortcut.ts
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export default defineMcpTool({
  description: 'Create a shortcut tool for a specific query',
  inputSchema: {
    name: z.string().describe('Name for the shortcut tool'),
    query: z.string().describe('The query this shortcut runs'),
  },
  handler: async ({ name, query }) => {
    const mcp = useMcpServer()
    mcp.registerTool(name, {
      description: `Shortcut: ${query}`,
    }, async () => {
      const result = await runQuery(query)
      return { content: [{ type: 'text', text: JSON.stringify(result) }] }
    })
    return `Shortcut "${name}" created`
  },
})

After calling this tool, the client's tool list refreshes and includes the new shortcut.

Removing a Tool Mid-Session

Use removeTool(name) to remove a tool by name -- no need to store handles:

server/mcp/tools/manage-agents.ts
import { z } from 'zod'
import { createError } from 'h3'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server'

export const registerAgent = defineMcpTool({
  name: 'register-agent',
  inputSchema: {
    agentId: z.string(),
    agentName: z.string(),
  },
  handler: async ({ agentId, agentName }) => {
    const mcp = useMcpServer()
    mcp.registerTool(`call-${agentId}`, {
      description: `Call agent: ${agentName}`,
    }, async () => {
      const response = await callAgent(agentId)
      return { content: [{ type: 'text', text: response }] }
    })
    return `Agent "${agentName}" registered`
  },
})

export const unregisterAgent = defineMcpTool({
  name: 'unregister-agent',
  inputSchema: {
    agentId: z.string(),
  },
  handler: async ({ agentId }) => {
    const mcp = useMcpServer()
    const removed = mcp.removeTool(`call-${agentId}`)
    if (!removed) throw createError({ statusCode: 404, message: `Agent "${agentId}" not found` })
    return `Agent "${agentId}" unregistered`
  },
})

Updating and Toggling Definitions

For in-place updates, use the RegisteredTool handle returned by registerTool():

const mcp = useMcpServer()
const registered = mcp.registerTool('my-tool', { description: 'v1' }, handler)

// Update the description and handler
registered.update({ description: 'v2', callback: newHandler })

// Temporarily hide from the client
registered.disable()

// Re-enable
registered.enable()

The same pattern applies to registerPrompt() and registerResource(), which return handles with the same methods.

Requirements

  • nitro.experimental.asyncContext must be true in your Nuxt config (required for useMcpServer() to access the request context)
  • Mid-session mutations are most useful with sessions enabled, since the server instance persists across requests. Without sessions, each request creates a fresh server, so mutations only last for a single request.
Not all MCP clients support notifications/tools/list_changed. If a client doesn't support it, the user may need to reconnect to see updated definitions. Check your client's documentation for compatibility.

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