Dynamic Definitions
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:
enabledguard — A per-definition callback that controls visibility- Dynamic handler definitions — A function in
defineMcpHandlerthat returns definitions based on context
Both mechanisms run after middleware, so event.context (e.g. authentication data) is available.
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
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
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
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:
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)
}
},
})
useEvent() inside handlers, enable asyncContext in your Nuxt config: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.
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:
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
},
})
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
| Method | Description |
|---|---|
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. |
server | The underlying McpServer instance for advanced SDK operations. |
Registering a Tool Mid-Session
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:
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.asyncContextmust betruein your Nuxt config (required foruseMcpServer()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.
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:
declare module 'h3' {
interface H3EventContext {
user?: {
id: string
name: string
role: 'user' | 'admin'
}
}
}
Next Steps
- Middleware — Set up authentication context
- Sessions — Enable per-session state
- Handlers — Create custom MCP endpoints