Multi-handler organization
When you have more than one named handler (/mcp/admin, /mcp/apps, /mcp/api-v2…) you usually want every tool, resource, and prompt to land in exactly one place without writing manual filters.
The toolkit gives you one mechanism to attribute definitions and one escape hatch for everything else.
A. Folder convention (the way to attribute)
Place named-handler definitions under server/mcp/handlers/<name>/. Every file under tools/, resources/, or prompts/ is auto-attached to that handler via _meta.handler.
server/mcp/
├── tools/ # → default handler
├── resources/ # → default handler
├── prompts/ # → default handler
└── handlers/
├── admin/
│ ├── index.ts # defineMcpHandler({ middleware: requireAdmin })
│ ├── tools/
│ │ └── delete-user.ts # → handler 'admin' (auto)
│ └── prompts/
│ └── help.ts # → handler 'admin' (auto)
└── widgets/
├── index.ts # defineMcpHandler({})
└── tools/
└── carousel.ts # → handler 'widgets' (auto)
The handler name is inferred from the directory name and wins over anything you set in index.ts.
index.ts is required even if it's a one-liner: export default defineMcpHandler({}). It's what registers the /mcp/<name> route and lets you add middleware, description, experimental_codeMode, etc.B. Function form (the escape hatch)
For cross-cutting cases — "every tool tagged X", "every orphan", "everything except this group" — pass a function that calls one of the getMcp* helpers:
import { defineMcpHandler, getMcpTools } from '@nuxtjs/mcp-toolkit/server'
export default defineMcpHandler({
// Every tool tagged 'searchable', regardless of folder.
tools: event => getMcpTools({ event, tags: ['searchable'] }),
})
getMcpTools, getMcpResources, and getMcpPrompts return the raw definition objects (with handlers and Zod schemas intact) — exactly what defineMcpHandler expects. They accept the same options as their listMcp* counterparts: event, group, tags, handler, orphansOnly. See Listing definitions for the full reference.
Default handler strategy
The default /mcp route obeys mcp.defaultHandlerStrategy in nuxt.config.ts:
export default defineNuxtConfig({
mcp: {
defaultHandlerStrategy: 'orphans', // (default)
},
})
'orphans' behaves exactly like 'all' — every definition is an orphan, so it's exposed. Existing apps don't change behaviour after upgrading.Resolution rule
The toolkit picks where each definition shows up using a single rule, deterministic by file location:
| Handler config file | Default behaviour for tools | resources | prompts: undefined |
|---|---|
server/mcp/index.ts (default route) | Obeys defaultHandlerStrategy |
server/mcp/handlers/<name>/index.ts (folder handler) | Definitions attributed to <name> |
server/mcp/<name>.ts (top-level handler) | Every discovered definition (back-compat) |
| Any (array) | Used as-is |
Any (function (event) => T[]) | Called per request |
_meta.handler on folder handlers; the runtime reads it to choose the default.Migration tips
- Start small. Move one handler at a time into
handlers/<name>/. Existingtools: [...]andtools: ev => [...]keep working untouched. - Drop manual filters. If you were doing
tools: allTools.filter(t => t._meta?.group === 'apps'), move those tools tohandlers/apps/tools/and let the system attribute them automatically. - Wrap-everything handlers. A top-level handler that wraps every tool (e.g. a code-mode wrapper at
mcp/codemode.ts) keeps its current behaviour — top-level handlers default to the full pool. To filter, pass a function:tools: ev => getMcpTools({ event: ev, ... }). - Force the old behaviour. Set
mcp.defaultHandlerStrategy: 'all'to keep/mcpexposing everything even after you adopt folder handlers.
See also
- Listing definitions — programmatic access to summaries and raw defs (
listMcp*/getMcp*). - Sharing & practices — handler organization patterns and best practices.
- Dynamic definitions — per-request
enabled()guards on individual definitions.