---
title: "Extend the module with hooks"
description: "Use Nuxt and Nitro hooks to extend and customize the MCP module."
canonical_url: "https://mcp-toolkit.nuxt.dev/advanced/hooks"
---
# Extend the module with hooks

> Use Nuxt and Nitro hooks to extend and customize the MCP module.

The toolkit exposes two flavours of hooks:

- **Build-time hooks** on `NuxtHooks` — fire during `nuxt build` / `nuxt prepare`, useful to register additional definition directories.
- **Runtime hooks** on `NitroRuntimeHooks` — fire **per request**, inside Nitro plugins, to mutate the resolved config, reach the SDK `McpServer` instance, or observe tool calls.

User listeners are best-effort: a hook that throws is logged via [consola](https://github.com/unjs/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

```typescript
nuxt.hook('mcp:definitions:paths', (paths: {
  tools: string[]
  resources: string[]
  prompts: string[]
  handlers: string[]
}) => {
  // Mutate paths in place
})
```

#### Usage in `nuxt.config.ts`

```typescript [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

```typescript [my-module.ts]
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

```typescript
{
  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:

1. **Relative paths** like `'tools'` resolve to `server/tools/`.
2. **Absolute paths** starting with `/` resolve from project root.
3. **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](https://nuxt.com/docs/guide/directory-structure/server#server-plugins) at `server/plugins/`.

```typescript [server/plugins/mcp.ts]
export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('mcp:server:created', ({ server, event }) => {
    // ...
  })
})
```

The hooks run in this order:

```text
H3 Event
   │
   ▼
defineMcpHandler middleware (if any)
   │
   ▼
resolveDynamicDefinitions  ──►  mcp:config:resolved
   │
   ▼
X-MCP-Tools allowlist (if the header is present)
   │
   ▼
createMcpServer            ──►  mcp:server:created
   │
   ▼
transport.handleRequest    ──►  mcp:tool:called (once per tools/call)
```

### `mcp:config:resolved`

Fired after dynamic `tools` / `resources` / `prompts` resolvers and `enabled(event)` guards have run, **before** the `X-MCP-Tools` allowlist and the per-request `McpServer` is built. Mutate `ctx.config` in place to add, remove or transform definitions for this request only.

#### Hook signature

```typescript
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

```typescript [server/plugins/mcp-auth.ts]
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

```typescript [server/plugins/mcp-rebrand.ts]
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`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/server/mcp.ts) instance.

Use it to register definitions late or reach the underlying low-level [`Server`](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/src/server/index.ts) via `getSdkServer(ctx.server)` to install custom JSON-RPC handlers.

#### Hook signature

```typescript
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

```typescript [server/plugins/mcp-whoami.ts]
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

```typescript [server/plugins/mcp-instrument.ts]
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')
    }
  })
})
```

### `mcp:tool:called`

Fired once per `tools/call`, after the tool settles into a result. Cache hits are included, so the hook sees every call, not only the ones that ran the handler. A thrown error has already been converted to an `isError` result, so `result.isError` tells a failed call apart. With [code mode](https://mcp-toolkit.nuxt.dev/advanced/code-mode), a batch counts as one call to the dispatcher tool.

#### Hook signature

```typescript
nitroApp.hooks.hook('mcp:tool:called', ({ name, result, durationMs, event }) => {
  // name: the tool name
  // result: the CallToolResult sent to the client
  // durationMs: time from the call to its result
  // event: the H3Event of the request
})
```

#### Example: track tool usage with Vercel Custom Metrics

```typescript [server/plugins/mcp-metrics.ts]
import { metric } from '@vercel/functions'

export default defineNitroPlugin((nitroApp) => {
  nitroApp.hooks.hook('mcp:tool:called', ({ name, result, durationMs }) => {
    metric('mcp.tool.duration_ms', durationMs, {
      toolName: name,
      outcome: result.isError ? 'error' : 'success',
    })
  })
})
```

Count the data points for call volume, use percentiles for latency, and group by `outcome` for errors.

<tip>

Need request- or session-scoped state? Pull it from `event.context` inside the hook — middleware (and any auth layer that runs before the MCP handler) has already populated it.

</tip>

## Error handling

Listeners that throw are caught and reported via consola (tag `mcp-toolkit`):

```text
[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](https://h3.unjs.io/utils/response#createerror)) into MCP-compliant `isError` results.

## Next Steps

- [Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) - Run code per MCP request, including `next()` interception.
- [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) - Pick tools / resources / prompts dynamically from `defineMcpHandler`.
- [Custom Paths](https://mcp-toolkit.nuxt.dev/advanced/custom-paths) - Customize where definitions are scanned from.
- [Handlers](https://mcp-toolkit.nuxt.dev/handlers/overview) - Create multiple MCP endpoints.


## Sitemap

See the full [sitemap](https://mcp-toolkit.nuxt.dev/sitemap.md) for all pages.
