Extend the module with hooks
The toolkit exposes two flavours of hooks:
- Build-time hooks on
NuxtHooks— fire duringnuxt build/nuxt prepare, useful to register additional definition directories. - Runtime hooks on
NitroRuntimeHooks— fire per request, inside Nitro plugins, to mutate the resolved config or reach the SDKMcpServerinstance.
User listeners are best-effort: a hook that throws is logged via consola and the MCP request continues.
Build-time hooks
mcp:definitions:paths
Add additional directories to scan for tool / resource / prompt / handler definitions. Useful for sharing definitions across Nuxt layers or shipping them from a custom module.
Hook Signature
nuxt.hook('mcp:definitions:paths', (paths: {
tools: string[]
resources: string[]
prompts: string[]
handlers: string[]
}) => {
// Mutate paths in place
})
Usage in nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxtjs/mcp-toolkit'],
hooks: {
'mcp:definitions:paths'(paths) {
paths.tools.push('shared/tools')
paths.tools.push('legacy/tools')
paths.resources.push('shared/resources')
paths.prompts.push('shared/prompts')
paths.handlers.push('custom/handlers')
},
},
})
Usage in a Custom Module
export default defineNuxtModule({
setup(options, nuxt) {
nuxt.hook('mcp:definitions:paths', (paths) => {
paths.tools.push('my-module/tools')
paths.resources.push('my-module/resources')
paths.prompts.push('my-module/prompts')
})
},
})
Path Structure
{
tools: string[] // Directories to scan for tools
resources: string[] // Directories to scan for resources
prompts: string[] // Directories to scan for prompts
handlers: string[] // Directories to scan for handlers
}
All paths are relative to the server/ directory of each Nuxt layer:
- Relative paths like
'tools'resolve toserver/tools/. - Absolute paths starting with
/resolve from project root. - Layer-specific — each Nuxt layer resolves paths relative to its own
server/directory.
Runtime hooks
Runtime hooks fire per MCP request, from inside the Nitro server. Subscribe to them from a Nitro plugin at server/plugins/.
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:server:created', ({ server, event }) => {
// ...
})
})
The hooks run in this order:
H3 Event
│
▼
defineMcpHandler middleware (if any)
│
▼
resolveDynamicDefinitions ──► mcp:config:resolved
│
▼
createMcpServer ──► mcp:server:created
│
▼
transport.handleRequest
mcp:config:resolved
Fired after dynamic tools / resources / prompts resolvers and enabled(event) guards have run, before the per-request McpServer is built. Mutate ctx.config in place to add, remove or transform definitions for this request only.
Hook signature
import type { McpResolvedConfig } from '@nuxtjs/mcp-toolkit/server'
import type { H3Event } from 'h3'
nitroApp.hooks.hook('mcp:config:resolved', (ctx: {
config: McpResolvedConfig
event: H3Event
}) => {
// Mutate ctx.config in place
})
Example: hide admin tools from anonymous clients
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:config:resolved', ({ config, event }) => {
if (!event.context.user) {
config.tools = config.tools.filter(
tool => !tool.tags?.includes('admin'),
)
}
})
})
Example: rebrand the server per request
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:config:resolved', ({ config, event }) => {
const tenant = event.context.tenant?.name
if (tenant) {
config.name = `${tenant} MCP`
config.instructions = `You are connected to ${tenant}'s MCP server.`
}
})
})
mcp:server:created
Fired after createMcpServer has registered every tool / resource / prompt and before the server is connected to the transport. Receives the SDK McpServer instance.
Use it to register definitions late or reach the underlying low-level Server via getSdkServer(ctx.server) to install custom JSON-RPC handlers.
Hook signature
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import type { H3Event } from 'h3'
nitroApp.hooks.hook('mcp:server:created', (ctx: {
server: McpServer
event: H3Event
}) => {
// Reach into the SDK
})
Example: register a tool dynamically
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:server:created', ({ server, event }) => {
server.registerTool(
'whoami',
{ description: 'Return the current user id' },
async () => ({
content: [{
type: 'text',
text: String(event.context.userId ?? 'anonymous'),
}],
}),
)
})
})
Example: instrument the low-level SDK server
import { getSdkServer } from '@nuxtjs/mcp-toolkit/server'
export default defineNitroPlugin((nitroApp) => {
nitroApp.hooks.hook('mcp:server:created', ({ server }) => {
const sdkServer = getSdkServer(server)
sdkServer.oninitialized = () => {
console.log('Client initialized')
}
})
})
event.context inside the hook — middleware (and any auth layer that runs before the MCP handler) has already populated it.Error handling
Listeners that throw are caught and reported via consola (tag mcp-toolkit):
[error] [mcp-toolkit] Hook "mcp:server:created" threw — request continues
The MCP request always proceeds. If you need an error to surface to the client, throw inside the corresponding tool / resource / prompt handler instead — the toolkit converts thrown errors (including H3 errors) into MCP-compliant isError results.
Next Steps
- Middleware - Run code per MCP request, including
next()interception. - Dynamic Definitions - Pick tools / resources / prompts dynamically from
defineMcpHandler. - Custom Paths - Customize where definitions are scanned from.
- Handlers - Create multiple MCP endpoints.