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
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
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
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:
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 { 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:
export default defineMcpHandler({
middleware: async (event) => {
event.context.user = await getUser(event)
},
tools: async (event) => {
const allTools = await loadToolsFromConfig()
return allTools
},
})
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:
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