Handlers

Multi-handler organization

Auto-attribute auto-discovered definitions to multiple MCP handlers via folder convention, with a function-based escape hatch for cross-cutting cases.

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:

server/mcp/handlers/searchable/index.ts
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:

`'orphans'`
default
Only definitions not attached to any named handler are exposed. Each definition shows up in exactly one place.
`'all'`
Every discovered definition is exposed (the pre-multi-handler behaviour). Useful when you want a "kitchen sink" route on top of specialized ones.
nuxt.config.ts
export default defineNuxtConfig({
  mcp: {
    defaultHandlerStrategy: 'orphans', // (default)
  },
})
Zero-effort back-compat: when no definition uses the folder convention, '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 fileDefault 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
The distinction between "folder handler" and "top-level handler" is purely about where the file lives — not magic. The loader injects _meta.handler on folder handlers; the runtime reads it to choose the default.

Migration tips

  1. Start small. Move one handler at a time into handlers/<name>/. Existing tools: [...] and tools: ev => [...] keep working untouched.
  2. Drop manual filters. If you were doing tools: allTools.filter(t => t._meta?.group === 'apps'), move those tools to handlers/apps/tools/ and let the system attribute them automatically.
  3. 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, ... }).
  4. Force the old behaviour. Set mcp.defaultHandlerStrategy: 'all' to keep /mcp exposing everything even after you adopt folder handlers.

See also

Copyright © 2026