# Introduction ## What is Model Context Protocol? The [Model Context Protocol (MCP)](https://modelcontextprotocol.io){rel=""nofollow""} is an open protocol that enables AI assistants to securely access external data sources and tools. It provides a standardized way for AI applications to: - **Access Tools**: Execute functions and operations - **Read Resources**: Access files, databases, APIs, and other data sources - **Use Prompts**: Leverage predefined prompt templates MCP servers act as bridges between AI assistants and external systems, enabling them to interact with your application's data and functionality in a secure, controlled manner. ## What is Nuxt MCP Toolkit? The Nuxt MCP Toolkit makes it incredibly easy to create MCP servers directly in your Nuxt application. Instead of building a separate MCP server, you can define tools, resources, and prompts right alongside your Nuxt application code. ### Key Benefits ::card-group :::card{icon="i-lucide-zap" title="Zero Configuration"} Automatic discovery of definitions. Just create files in the right directories and they're automatically registered. ::: :::card{icon="i-lucide-type" title="TypeScript First"} Full TypeScript support with auto-imports and complete type safety. All helpers are available globally in your server files. ::: :::card{icon="i-lucide-code" title="Simple API"} Intuitive API that matches the MCP SDK structure, making it easy to migrate existing code or learn from examples. ::: :::card{icon="i-lucide-layers" title="Flexible Architecture"} Support for multiple MCP handlers in a single application, custom paths, and hooks for advanced use cases. ::: :: ## How It Works The module automatically: 1. **Scans** your `server/mcp/` directory (or custom path) for definitions 2. **Discovers** tools, resources, and prompts from your files 3. **Registers** them with the MCP server 4. **Exposes** an HTTP endpoint for MCP clients to connect ```text server/ └── mcp/ ├── tools/ │ ├── echo.ts │ └── calculator.ts ├── resources/ │ ├── readme.ts │ └── files.ts └── prompts/ ├── greeting.ts └── summarize.ts ``` ## Core Concepts ### Tools Tools are functions that AI assistants can call. They accept input parameters (validated with Zod) and return results. ```typescript import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'calculate-bmi', inputSchema: { weightKg: z.number(), heightM: z.number(), }, handler: async ({ weightKg, heightM }) => { const bmi = weightKg / (heightM * heightM) return `BMI: ${bmi}` }, }) ``` ### Resources Resources provide access to data via URIs. They can be static files or dynamic data sources. ```typescript import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'readme', uri: 'file:///README.md', handler: async (uri: URL) => { const content = await readFile(uri.pathname, 'utf-8') return { contents: [{ uri: uri.toString(), text: content }], } }, }) ``` ### Prompts Prompts are reusable message templates that can include dynamic arguments. ```typescript import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'greeting', inputSchema: { name: z.string(), }, handler: async ({ name }) => { return { messages: [{ role: 'user', content: { type: 'text', text: `Hello, ${name}!` }, }], } }, }) ``` ### Apps MCP Apps are interactive UI widgets that hosts like Cursor and ChatGPT render inline. Author them as Vue Single-File Components in `app/mcp/` — the toolkit bundles them and serves them as MCP UI resources. ```vue [app/mcp/color-picker.vue] ``` ## Next Steps Ready to get started? Check out: - [Installation Guide](https://mcp-toolkit.nuxt.dev/getting-started/installation) - Set up the module in your project - [Configuration](https://mcp-toolkit.nuxt.dev/getting-started/configuration) - Configure the module - [Tools Guide](https://mcp-toolkit.nuxt.dev/tools/overview) - Create your first MCP tool - [Apps Guide](https://mcp-toolkit.nuxt.dev/apps/overview) - Build interactive UI widgets for AI hosts # Install Nuxt MCP Toolkit ::prompt --- actions: - copy - cursor - windsurf description: Set up an MCP server in a Nuxt app icon: i-lucide-download --- Set up an MCP server in my Nuxt app using @nuxtjs/mcp-toolkit. - Auto install the module using `npx nuxt add mcp` - Create the server/mcp/ directory with tools/, resources/, and prompts/ subdirectories - defineMcpTool, defineMcpResource, defineMcpPrompt, and defineMcpHandler are auto-imported - Create a test tool in server/mcp/tools/test.ts using defineMcpTool with a Zod inputSchema - Start the dev server and verify the MCP endpoint at {rel=""nofollow""} - Connect your IDE (Cursor or VS Code) to {rel=""nofollow""} Docs: {rel=""nofollow""} :: ## Try the Documentation MCP Server Before installing the module, you can try connecting to this documentation's MCP server to explore the available tools and prompts: :install-button{name="nuxt-mcp-toolkit-docs" url="https://mcp-toolkit.nuxt.dev/mcp"} This will give you access to prompts like `setup-mcp-server`, `create-tool`, `create-resource`, and `troubleshoot` to help you get started. ## Prerequisites - Nuxt 3.x or 4.x - Node.js 18.x or higher - A package manager (npm, pnpm, yarn, or bun) If you enable Code Mode, that feature specifically requires Node.js `>=18.16.0`. ## Installation ::steps ### Install the module You can install the module automatically or manually. #### Automatic Installation Use the `nuxt` command to install the module and add it to your configuration automatically: ```bash npx nuxt add mcp ``` #### Manual Installation Install `@nuxtjs/mcp-toolkit` and its peer dependency `zod`: :::code-group ```bash [pnpm] pnpm add @nuxtjs/mcp-toolkit zod ``` ```bash [npm] npm install @nuxtjs/mcp-toolkit zod ``` ```bash [yarn] yarn add @nuxtjs/mcp-toolkit zod ``` ```bash [bun] bun add @nuxtjs/mcp-toolkit zod ``` ::: ### Add to Nuxt config Add the module to your `nuxt.config.ts`: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], }) ``` ### Configure the module (optional) The module works with sensible defaults, but you can customize it: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { name: 'My MCP Server', route: '/mcp', // Default route for the MCP server dir: 'mcp', // Base directory for MCP definitions (relative to server/) }, }) ``` :: ## Verify Installation After installation, you can verify everything is working by: 1. **Checking the server route**: Start your Nuxt dev server and visit `http://localhost:3000/mcp` (or your custom route). You should be redirected to your configured `browserRedirect` URL. 2. **Creating a test tool**: Create a simple tool to test: ```typescript [server/mcp/tools/test.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'test', description: 'A simple test tool', inputSchema: { message: z.string(), }, handler: async ({ message }) => `Test successful: ${message}`, }) ``` 3. **Checking auto-imports**: The `defineMcpTool`, `defineMcpResource`, `defineMcpPrompt`, and `defineMcpHandler` functions should be auto-imported in your server files. ## Project Structure After installation, your project structure should look like this: ```text your-project/ ├── server/ │ └── mcp/ │ ├── tools/ │ │ └── echo.ts # Your tool definitions │ ├── resources/ │ │ └── readme.ts # Your resource definitions │ └── prompts/ │ └── greeting.ts # Your prompt definitions ├── nuxt.config.ts └── package.json ``` ## Connect Your IDE Once your Nuxt app is running, connect your AI assistant to the MCP server: :install-button{ide="cursor" name="local-mcp" url="http://localhost:3000/mcp"} :install-button{ide="vscode" name="local-mcp" url="http://localhost:3000/mcp"} For manual configuration, the [add-mcp](https://npmx.dev/package/add-mcp){rel=""nofollow""} CLI, and install buttons for your own documentation, see the [Connection](https://mcp-toolkit.nuxt.dev/getting-started/connection) guide. ## Next Steps Now that you have the module installed: - [Configuration](https://mcp-toolkit.nuxt.dev/getting-started/configuration) - Learn about all configuration options - [Connection](https://mcp-toolkit.nuxt.dev/getting-started/connection) - Connect AI assistants to your MCP server and add install buttons to your documentation - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Create your first tool # Configure the module ## Basic Configuration Add the module to your `nuxt.config.ts`: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { name: 'My MCP Server', }, }) ``` The module works with sensible defaults, so minimal configuration is required. ## Configuration Options All available configuration options: ::field-group :::field{name="enabled" type="boolean"} Default: `true` Enable or disable the MCP server. ::: :::field{name="route" type="string"} Default: `'/mcp'` The HTTP route where the MCP server will be accessible. ::: :::field{name="browserRedirect" type="string"} Default: `'/'` URL to redirect browsers when they access the MCP endpoint. ::: :::field{name="name" type="string"} Default: `''` The name of your MCP server (used in the MCP protocol handshake). ::: :::field{name="version" type="string"} Default: `'1.0.0'` The version of your MCP server (semantic versioning). ::: :::field{name="description" type="string"} Optional. A human-readable description of the server, sent as part of `serverInfo` during initialization. Clients display it in their UI (server lists, tooltips, install prompts). ::: :::field{name="instructions" type="string"} Optional. Operational instructions injected by clients into the model's system prompt. Use this to guide LLMs on workflows, tool relationships, or constraints — not to identify the server (use `description` for that). ```ts [nuxt.config.ts] export default defineNuxtConfig({ mcp: { description: 'Read and update todos for the current user.', instructions: 'Always call list-todos before create-todo. Group results by status.', }, }) ``` See the [MCP lifecycle spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization){rel=""nofollow""}. ::: :::field{name="icons" type="McpIcon[]"} Optional. Icons for the server, displayed by clients in their UI. Each entry needs `src` and `mimeType`, with optional `sizes` and `theme`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ mcp: { icons: [ { src: 'https://example.com/icon-light.png', mimeType: 'image/png', sizes: ['64x64'], theme: 'light' }, { src: 'https://example.com/icon-dark.png', mimeType: 'image/png', sizes: ['64x64'], theme: 'dark' }, ], }, }) ``` ::: :::field{name="dir" type="string"} Default: `'mcp'` Base directory for MCP definitions (relative to `server/`). The module expects: - `{dir}/tools/` - Tool definitions - `{dir}/resources/` - Resource definitions - `{dir}/prompts/` - Prompt definitions - `{dir}/handlers//` - Optional named handler folders ([organization](https://mcp-toolkit.nuxt.dev/handlers/organization)) ::: :::field{name="appsDir" type="string"} Default: `'mcp'` Base directory for [MCP Apps](https://mcp-toolkit.nuxt.dev/apps/overview) `.vue` files (relative to Nuxt's `app/` directory). Scanned across layers. Defaults to `app/mcp/`. The MCP Apps pipeline only runs when this directory exists. ::: :::field{name="defaultHandlerStrategy" type="'orphans' | 'all'"} Default: `'orphans'` Controls which auto-discovered definitions land on the default `/mcp` route when [named handlers](https://mcp-toolkit.nuxt.dev/handlers/organization) exist: - `'orphans'` — only definitions not attached to any named handler (no folder attribution). When no folder handler exists, every definition is an orphan, so this naturally falls back to "expose everything" — zero-effort back-compat. - `'all'` — every discovered definition, including those attributed to named handlers. Useful when you want a "kitchen sink" route in addition to specialized ones. ::: :::field{name="autoImports" type="boolean"} Default: `true` Auto-import MCP helpers (`defineMcpTool`, `defineMcpResource`, `defineMcpHandler`, `defineMcpApp`, …), types (`McpRequestExtra`, …), composables (`useMcpSession`, `useMcpServer`, `useMcpLogger`, `useMcpElicitation`), and the `InstallButton` component. Set to `false` to disable all auto-imports and require explicit imports from `@nuxtjs/mcp-toolkit/server`. ::: :::field{name="logging" type="boolean"} Optional. Server-side observability for every MCP request via [evlog](https://evlog.dev){rel=""nofollow""}, an **optional peer dependency**. - `undefined` (default) — auto-detect: on if the `evlog/nuxt` module is registered, off otherwise. - `true` — assert `evlog/nuxt` is registered; throw at build otherwise. - `false` — opt out entirely. When active, every MCP request emits a wide event tagged with `mcp.transport`, `mcp.method`, `mcp.tool`, `mcp.session_id`, `mcp.request_id`, etc. Use [`useMcpLogger()`](https://mcp-toolkit.nuxt.dev/advanced/logging) to push extra fields and discrete events. ::: :::field{name="sessions" type="boolean | object"} Default: `false` Enable [MCP session management](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management){rel=""nofollow""} (stateful transport). When enabled, the server assigns session IDs via the `MCP-Session-Id` header and maintains state across requests, enabling SSE streaming, server-to-client notifications, and session continuity. Pass `true` for defaults or an object with: - `enabled` - Enable or disable sessions - `maxDuration` - Session timeout in milliseconds (default: `1800000` / 30 minutes) - `maxSessions` - Maximum concurrent sessions before new session creation returns `503` (default: `1000`). Enforced on the **Node** Nitro server only; Cloudflare Workers use the `agents/mcp` path in this module, which does not apply this cap. ::: :::field{name="security" type="object"} Optional. Hardens [Streamable HTTP](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#security){rel=""nofollow""} requests. - `allowedOrigins` — `undefined` (default): allow requests with no `Origin` header (typical for same-origin and CLI); otherwise require `Origin` to match the server origin (scheme + host + port). Use the literal `'*'` in config to disable Origin checks (explicit opt-out). `string[]` — allow only listed origins (each entry is normalized to an origin URL). Cross-site browser clients must send an allowed `Origin` or receive **403**. ::: :: ## Common Configuration Scenarios ### Custom Route Change the MCP endpoint route: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { route: '/api/mcp', // Custom route }, }) ``` ### Custom Directory Use a different directory for MCP definitions: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { dir: 'my-mcp', // Look in server/my-mcp/ instead of server/mcp/ }, }) ``` This will look for definitions in: - `server/my-mcp/tools/` - `server/my-mcp/resources/` - `server/my-mcp/prompts/` ### Browser Redirect Redirect browsers to a custom URL: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { browserRedirect: '/docs/mcp', // Redirect browsers to documentation }, }) ``` ### Streamable HTTP security (`allowedOrigins`) When browsers call your MCP endpoint from another site, they send an `Origin` header. By default the module requires that origin to match your app. Allow a SPA on another host (or disable checks only in controlled environments): ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { security: { allowedOrigins: ['https://my-app.vercel.app'], // allowedOrigins: '*' // explicit opt-out — use with care }, }, }) ``` ### Session Management Enable stateful sessions to support SSE streaming, server-to-client notifications, and per-session state: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { sessions: true, }, }) ``` With sessions enabled, the server assigns an `MCP-Session-Id` during initialization. Clients include this ID in subsequent requests, allowing the server to maintain state across the session lifecycle. ::callout{color="info" icon="i-lucide-info"} See the [Sessions guide](https://mcp-toolkit.nuxt.dev/advanced/sessions) for the full `useMcpSession()` API, use cases, and examples. :: ### Disable Auto-Imports If you prefer explicit imports over auto-imports: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { autoImports: false, }, }) ``` With auto-imports disabled, import helpers and types explicitly: ```typescript [server/mcp/tools/echo.ts] import { z } from 'zod' import { defineMcpTool, type McpRequestExtra } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ description: 'Echo back a message', inputSchema: { message: z.string() }, handler: async ({ message }, extra: McpRequestExtra) => { return `Echo: ${message}` }, }) ``` ### Disable the Module Temporarily disable the MCP server: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], mcp: { enabled: false, // Disable the MCP server }, }) ``` ## Runtime Configuration Access configuration at runtime: ```typescript [server/api/config.ts] export default defineEventHandler((event) => { const config = useRuntimeConfig(event).mcp return { name: config.name, version: config.version, route: config.route, } }) ``` ## Next Steps - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Learn how to create tools - [Resources](https://mcp-toolkit.nuxt.dev/resources/overview) - Create resources - [Prompts](https://mcp-toolkit.nuxt.dev/prompts/overview) - Create prompts - [Sessions](https://mcp-toolkit.nuxt.dev/advanced/sessions) - Per-session state with `useMcpSession()` - [Custom Paths](https://mcp-toolkit.nuxt.dev/advanced/custom-paths) - Advanced path configuration # Debug with the MCP Inspector The module includes a built-in integration with the [MCP Inspector](https://github.com/modelcontextprotocol/inspector){rel=""nofollow""}, a visual debugging tool that allows you to interactively test and debug your MCP server directly from Nuxt DevTools. ::u-color-mode-image --- alt: MCP Inspector preview class: w-full rounded-lg my-6 grayscale dark: /mcp-devtools-dark.png light: /mcp-devtools-light.png --- :: ## How to use it 1. **Enable DevTools** - Make sure DevTools are enabled in your `nuxt.config.ts`: ```typescript \[nuxt.config.ts] export default defineNuxtConfig({ modules: ['@nuxtjs/mcp-toolkit'], devtools: { enabled: true }, }) ``` 2. **Launch** - Open Nuxt DevTools and navigate to the **MCP Inspector** tab in the **Server** section, then click **Launch Inspector**. 3. **Test** - Use the inspector to browse tools, resources, and prompts, test them with custom parameters, and view request/response history. The inspector automatically connects to your MCP server endpoint with the correct configuration - no setup needed. ## Why use it? The inspector provides a visual interface to: - Browse all available tools, resources, and prompts - Test tools with custom parameters and see responses - View request/response history for debugging - Debug connection issues and errors It's the easiest way to test and validate your MCP server during development. ## Configuration By default, the inspector uses ports `6274` (UI) and `6277` (proxy). To customize them: ::code-group ```bash [pnpm] CLIENT_PORT=8080 SERVER_PORT=9000 pnpm dev ``` ```bash [npm] CLIENT_PORT=8080 SERVER_PORT=9000 npm run dev ``` ```bash [yarn] CLIENT_PORT=8080 SERVER_PORT=9000 yarn dev ``` ```bash [bun] CLIENT_PORT=8080 SERVER_PORT=9000 bun dev ``` :: For advanced configuration options, see the [MCP Inspector documentation](https://github.com/modelcontextprotocol/inspector){rel=""nofollow""}. # Connect MCP clients ## Overview Once your MCP server is running, you can connect AI assistants like Cursor, VS Code, and ChatGPT to use your tools, resources, and prompts. The module also provides components and routes to help your users install your MCP server in one click. ## add-mcp The [add-mcp](https://npmx.dev/package/add-mcp){rel=""nofollow""} CLI can register a **remote** (streamable HTTP) MCP server with Cursor, Claude Code, VS Code, Codex, and [several other coding agents](https://npmx.dev/package/add-mcp){rel=""nofollow""} in one step. Use the URL that matches where the app runs: **`http://…` for local dev**, **`https://…` in production** (public MCP endpoints should use HTTPS). If you changed `mcp.route` in `nuxt.config`, append that path instead of `/mcp`. ::code-group ```bash [Local dev] npx add-mcp http://localhost:3000/mcp ``` ```bash [Production] npx add-mcp https://your-app.example.com/mcp ``` :: Run the command from any directory; it updates agent config files (e.g. project `.cursor/mcp.json`). Options such as `-a cursor`, `-y`, or `--header` for authenticated endpoints are described in the [add-mcp package docs](https://npmx.dev/package/add-mcp){rel=""nofollow""}. ## Share your MCP with users (production) When your Nuxt app is deployed, you give **your users** a stable MCP URL. They will plug the same shape of URL into their assistant as for localhost—only the host and scheme change. ### What to publish | You share | Example | | ------------------- | --------------------------------------------------------------- | | Public MCP endpoint | `https://your-product.com/mcp` | | Custom route | `https://your-product.com/api/mcp` if `mcp.route` is `/api/mcp` | Prefer **HTTPS** on the public internet. Your hosting provider’s assigned URL (e.g. `https://my-app.vercel.app/mcp`) is fine until you add a custom domain. ### One-liner for users ([add-mcp](https://npmx.dev/package/add-mcp){rel=""nofollow""}) Document this in your README or docs so people can register your server quickly: ```bash npx add-mcp https://your-product.com/mcp ``` If your MCP is behind auth, document the required headers and point people to `add-mcp --header 'Authorization: Bearer …'` (or your provider’s pattern). ### Buttons and badges for docs / README Give non-CLI users a single click: - **[`InstallButton`](https://mcp-toolkit.nuxt.dev/#installbutton-component)** in Vue or MDC — set `url` to your **production** MCP URL (see examples with `https://my-app.com/mcp` below). - **[README badges](https://mcp-toolkit.nuxt.dev/#readme-badge)** — Markdown badges that open the IDE installer and work in GitHub or any site. Example for your landing or documentation: :install-button{name="your-product-mcp" url="https://your-product.com/mcp"} :install-button{ide="vscode" name="your-product-mcp" url="https://your-product.com/mcp"} ### Manual config you can copy for users Same JSON as in the [IDE sections below](https://mcp-toolkit.nuxt.dev/#connect-your-ide), but with your live URL: ```json { "url": "https://your-product.com/mcp" } ``` Cursor and VS Code expect this inside their respective `mcpServers` / `servers` shapes — mirror the full examples under [Cursor](https://mcp-toolkit.nuxt.dev/#cursor) and [VS Code](https://mcp-toolkit.nuxt.dev/#vs-code) and replace the localhost URL. ## Connect Your IDE The steps below use **localhost** while you develop. To publish instructions for your audience, reuse the same patterns with your **production** `https://…` URL — see [Share your MCP with users (production)](https://mcp-toolkit.nuxt.dev/#share-your-mcp-with-users-production). ### Cursor Click the button below to add your local MCP server to Cursor: :install-button{ide="cursor" name="local-mcp" url="http://localhost:3000/mcp"} Or manually add it to your Cursor settings (`~/.cursor/mcp.json`): ```json [~/.cursor/mcp.json] { "mcpServers": { "my-nuxt-app": { "url": "http://localhost:3000/mcp" } } } ``` ### VS Code Click the button below to add your local MCP server to VS Code: :install-button{ide="vscode" name="local-mcp" url="http://localhost:3000/mcp"} Or manually add the server to your VS Code MCP configuration (`.vscode/mcp.json`): ```json [.vscode/mcp.json] { "servers": { "my-nuxt-app": { "type": "http", "url": "http://localhost:3000/mcp" } } } ``` ::callout{color="info" icon="i-lucide-info"} Replace `my-nuxt-app` with your project name and update the URL if you're using a custom route or port. :: ## InstallButton Component The module provides an `InstallButton` component that you can use in your documentation to let users install your MCP server in one click. ### Supported IDEs | IDE | Value | Status | | ------- | -------- | --------- | | Cursor | `cursor` | Supported | | VS Code | `vscode` | Supported | ### In Vue Templates ```vue ``` ### In Markdown (MDC Syntax) If you're using [Nuxt Content](https://content.nuxt.com){rel=""nofollow""}, use the MDC syntax: ```md ::install-button --- url: "https://my-app.com/mcp" --- :: ::install-button --- url: "https://my-app.com/mcp" ide: "vscode" --- :: ::install-button --- url: "https://my-app.com/mcp" label: "Add to Cursor" --- :: ``` ### Props Reference | Prop | Type | Default | Description | | ---------- | --------------------- | -------------- | ------------------------------ | | `url` | `string` | required | URL of the MCP server endpoint | | `ide` | `'cursor' | 'vscode'` | `'cursor'` | Target IDE | | `label` | `string` | Auto-generated | Button label | | `showIcon` | `boolean` | `true` | Show the IDE icon | ### Customization The component uses CSS classes that you can override: ```css /* Override default styles */ .mcp-install-button { background-color: #your-brand-color; border-radius: 9999px; } ``` Or use the slot for completely custom content: ```vue Add to Cursor ``` ## README Badge For README files and documentation outside of Vue/Nuxt, the module provides server routes to generate badges. ### Badge Routes The module exposes two routes: | Route | Description | | ---------------- | -------------------------------------- | | `/mcp/deeplink` | Redirects to the IDE deeplink | | `/mcp/badge.svg` | Returns a customizable SVG badge image | ### Basic Usage Add this to your README: ```md [![Install in Cursor](https://your-app.com/mcp/badge.svg)](https://your-app.com/mcp/deeplink) ``` This will display a badge that, when clicked, opens the IDE and installs your MCP server. ### VS Code Badge ```md [![Install in VS Code](https://your-app.com/mcp/badge.svg?ide=vscode)](https://your-app.com/mcp/deeplink?ide=vscode) ``` ### Both IDEs ```md [![Install in Cursor](https://your-app.com/mcp/badge.svg)](https://your-app.com/mcp/deeplink) [![Install in VS Code](https://your-app.com/mcp/badge.svg?ide=vscode)](https://your-app.com/mcp/deeplink?ide=vscode) ``` ### Customization Options | Parameter | Default | Description | | ------------- | -------------- | --------------------------------- | | `ide` | `cursor` | Target IDE (`cursor` or `vscode`) | | `label` | Auto-generated | Badge text | | `color` | `171717` | Background color (hex without #) | | `textColor` | `ffffff` | Text color (hex without #) | | `borderColor` | `404040` | Border color (hex without #) | | `icon` | `true` | Show IDE icon (`true` or `false`) | ### Custom Badge Examples **Custom label:** ```md [![Add to Cursor](https://your-app.com/mcp/badge.svg?label=Add%20to%20Cursor)](https://your-app.com/mcp/deeplink) ``` **Custom colors:** ```md [![Install](https://your-app.com/mcp/badge.svg?color=0ea5e9&borderColor=0284c7)](https://your-app.com/mcp/deeplink) ``` **Without icon:** ```md [![Install](https://your-app.com/mcp/badge.svg?icon=false)](https://your-app.com/mcp/deeplink) ``` ::callout{color="info" icon="i-lucide-info"} Replace `https://your-app.com` with your actual domain. The badge route uses the server name from your `mcp.name` config. :: ## Deeplink Formats For reference, here are the deeplink formats used by each IDE: ### Cursor ```text cursor://anysphere.cursor-deeplink/mcp/install?name=SERVER_NAME&config=BASE64_CONFIG ``` The config is Base64-encoded JSON containing `{ type: 'http', url: 'MCP_URL' }`. ### VS Code ```text vscode:mcp/install?URL_ENCODED_JSON ``` The config is URL-encoded JSON containing `{ name: 'SERVER_NAME', type: 'http', url: 'MCP_URL' }`. ## Next Steps - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Create your first tool - [Resources](https://mcp-toolkit.nuxt.dev/resources/overview) - Expose data to AI assistants - [Prompts](https://mcp-toolkit.nuxt.dev/prompts/overview) - Create reusable message templates # Agent Skills Nuxt MCP Toolkit includes agent skills that help AI assistants build, review, and troubleshoot MCP servers in your Nuxt application. This documentation site is built with [Docus](https://docus.dev){rel=""nofollow""}, which publishes skills under [`/.well-known/skills/`](https://mcp-toolkit.nuxt.dev/.well-known/skills/index.json){rel=""nofollow""} following the [Agent Skills discovery](https://docus.dev/en/ai/skills){rel=""nofollow""} convention. That lets the [`skills` CLI](https://agentskills.io){rel=""nofollow""} install them from the production URL below. ## What are Agent Skills? [Agent Skills](https://agentskills.io/){rel=""nofollow""} is an open specification for packaging AI assistant capabilities. Skills provide: - **Domain knowledge**: Best practices for MCP tools, resources, and prompts - **Guided development**: Step-by-step help creating and configuring MCP servers - **Code review**: Identify anti-patterns and suggest improvements - **Troubleshooting**: Diagnose common issues with auto-imports, endpoints, and validation ## Available Skills | Skill | Description | | ------------------- | ----------------------------------------------------------------- | | `skills/manage-mcp` | Setup, create, review, troubleshoot, and test MCP servers in Nuxt | ## Installing the Skill Compatible agents (Cursor, Claude Code, etc.) can discover and use skills automatically. Install from this site’s production URL (recommended): ```bash [Terminal] npx skills add https://mcp-toolkit.nuxt.dev ``` The CLI fetches the catalog from [`/.well-known/skills/index.json`](https://mcp-toolkit.nuxt.dev/.well-known/skills/index.json){rel=""nofollow""} and installs the `manage-mcp` skill and its reference files. For pull request previews, use the same command with your preview deployment URL ([Docus: preview and versioning](https://docus.dev/en/ai/skills){rel=""nofollow""}). ## What the Skill Does ### Setup & Configure The skill guides you through: - **Installing** `@nuxtjs/mcp-toolkit` in your Nuxt app - **Configuring** `nuxt.config.ts` with MCP options - **Creating** the `server/mcp/` directory structure - **Verifying** the MCP endpoint is accessible ### Create Definitions The skill helps you build: - **Tools**: Functions AI assistants can call, with Zod validation and error handling - **Resources**: Read-only data exposed via URIs (static or dynamic with templates) - **Prompts**: Reusable message templates with dynamic arguments - **Middleware**: Authentication, rate limiting, logging, and CORS handlers ### Review & Audit The skill analyzes your codebase for: - **Missing descriptions**: Tools or resources without clear descriptions - **Weak validation**: Missing `.describe()` on Zod fields - **Error handling gaps**: Missing `isError: true` on error responses - **Security issues**: Exposed sensitive data in resources or tools - **Performance**: Missing caching on expensive operations ### Troubleshoot The skill diagnoses: - **Auto-imports not working**: Module configuration and file placement issues - **Endpoint not accessible**: Server configuration and routing problems - **Validation errors**: Schema mismatches and type issues - **Tool not discovered**: File naming, exports, and directory structure problems ### Test with Evals The skill helps you: - Set up Evalite for MCP tool selection testing - Write eval scenarios to validate tool selection - Configure CI/CD integration for automated testing ### Example Prompts Ask your AI assistant: ::prompt --- actions: - copy - cursor - windsurf description: Example — set up an MCP server icon: i-lucide-sparkles --- Setup an MCP server in my Nuxt app :: ::prompt --- actions: - copy - cursor - windsurf description: Example — database tool icon: i-lucide-sparkles --- Create a tool to fetch user data from my database :: ::prompt --- actions: - copy - cursor - windsurf description: Example — review MCP code icon: i-lucide-sparkles --- Review my MCP implementation for best practices :: ::prompt --- actions: - copy - cursor - windsurf description: Example — troubleshoot auto-imports icon: i-lucide-sparkles --- My auto-imports aren't working, help me troubleshoot :: ::prompt --- actions: - copy - cursor - windsurf description: Example — MCP eval tests icon: i-lucide-sparkles --- Create eval tests for my MCP tools :: ## Skill Structure In the [mcp-toolkit repository](https://github.com/nuxt-modules/mcp-toolkit){rel=""nofollow""}, skill sources live under the docs app: ```text apps/docs/skills/ └── manage-mcp/ ├── SKILL.md # Main skill instructions └── references/ ├── middleware.md # Middleware patterns & examples ├── tools.md # Tool examples ├── resources.md # Resource examples ├── prompts.md # Prompt examples ├── testing.md # Testing guide with Evalite └── troubleshooting.md # Troubleshooting guide ``` Deployed files are served as `/.well-known/skills/manage-mcp/...` on [mcp-toolkit.nuxt.dev](https://mcp-toolkit.nuxt.dev){rel=""nofollow""}. ## Reference Documents The skill includes reference documents that provide: ### middleware.md - Authentication patterns (API keys, JWT) - Rate limiting and CORS configuration - Logging and request tracking - Security best practices ### tools.md - Tool definition patterns with various input types - Error handling and caching examples - Real-world tool implementations ### resources.md - Static and dynamic resource patterns - File, API, and database resource examples - URI template usage ### prompts.md - Static and dynamic prompt patterns - Multi-message conversation templates - Code review and documentation generator examples ### testing.md - Evalite setup and configuration - Test scenario patterns - CI/CD integration ### troubleshooting.md - Auto-import resolution steps - Endpoint debugging guide - Validation error fixes - Performance optimization tips ## Next Steps - [Installation](https://mcp-toolkit.nuxt.dev/getting-started/installation) - Get started with Nuxt MCP Toolkit - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Learn how to create MCP tools - [Resources](https://mcp-toolkit.nuxt.dev/resources/overview) - Expose data via MCP resources - [Prompts](https://mcp-toolkit.nuxt.dev/prompts/overview) - Create reusable prompt templates - [Apps](https://mcp-toolkit.nuxt.dev/apps/overview) - Ship interactive UI widgets to AI hosts # Tools ## What are Tools? Tools are functions that AI assistants can call to perform actions or retrieve information. They accept validated input parameters and return structured results. ::prompt --- actions: - copy - cursor - windsurf description: Scaffold a new MCP tool icon: i-lucide-wrench --- Create a new MCP tool in my Nuxt app using @nuxtjs/mcp-toolkit. - Create a file in server/mcp/tools/ (e.g. server/mcp/tools/my-tool.ts) - Use defineMcpTool (auto-imported) with a description and handler - Import Zod with `import { z } from 'zod'` and define input parameters in inputSchema (e.g. z.string().describe('...')) - The handler receives validated input and returns a string, number, boolean, object, or full CallToolResult - Throw errors with createError({ statusCode, message }) from h3 for error cases - Name and title are auto-generated from the filename (e.g. my-tool.ts → name: 'my-tool', title: 'My Tool') - Add annotations for behavioral hints (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) - Use subdirectories to auto-infer groups (e.g. tools/admin/delete-user.ts → group: 'admin') Docs: {rel=""nofollow""} :: ## Basic Tool Definition Here's a simple tool that echoes back a message: ```typescript [server/mcp/tools/echo.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'echo', description: 'Echo back a message', inputSchema: { message: z.string().describe('The message to echo back'), }, handler: async ({ message }) => `Echo: ${message}`, }) ``` ## Auto-Generated Name and Title You can omit `name` and `title` - they will be automatically generated from the filename: ```typescript [server/mcp/tools/list-documentation.ts] import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ // name and title are auto-generated from filename: // name: 'list-documentation' // title: 'List Documentation' description: 'List all documentation files', handler: async () => { // ... }, }) ``` The filename `list-documentation.ts` automatically becomes: - `name`: `list-documentation` (kebab-case) - `title`: `List Documentation` (title case) You can still provide `name` or `title` explicitly to override the auto-generated values. ## Tool Structure A tool definition consists of: ::code-group ```typescript [Required Fields] export default defineMcpTool({ name: 'tool-name', // Unique identifier (optional - auto-generated from filename) inputSchema: { ... }, // Zod schema for input validation handler: async (args) => { return 'result' // string, number, boolean, object, or CallToolResult }, }) ``` ```typescript [Optional Fields] export default defineMcpTool({ name: 'tool-name', // Optional - auto-generated from filename title: 'Tool Title', // Optional - auto-generated from filename description: 'Tool description', // What the tool does inputSchema: { ... }, // Optional - Zod schema for input validation outputSchema: { ... }, // Zod schema for structured output annotations: { ... }, // Behavioral hints for clients inputExamples: [{ ... }], // Concrete usage examples handler: async (args) => { ... }, }) ``` :: ## Going further Once you've authored a few tools, branch out: ::card-group :::card --- color: neutral icon: i-lucide-globe title: Integrate external APIs to: https://mcp-toolkit.nuxt.dev/examples/api-integration --- Call third-party services, use `useEvent()` , and cache responses with Nitro. ::: :::card --- color: neutral icon: i-lucide-shield-check title: Authenticate clients to: https://mcp-toolkit.nuxt.dev/examples/authentication --- Bearer tokens, Better Auth API keys, and per-tool `enabled` guards. ::: :::card --- color: neutral icon: i-lucide-terminal title: Use Code Mode to: https://mcp-toolkit.nuxt.dev/advanced/code-mode --- Let the LLM orchestrate multiple tool calls in a single sandboxed JS execution. ::: :::card --- color: neutral icon: i-lucide-network title: Multi-handler organization to: https://mcp-toolkit.nuxt.dev/handlers/organization --- Attribute tools to dedicated MCP routes via the `handlers//` folder convention. ::: :: # Schema, handler & returns ## Input Schema The `inputSchema` is optional and uses Zod to define and validate input parameters. When provided, each field must be a Zod schema. Tools without parameters can omit `inputSchema` entirely: ```typescript [server/mcp/tools/echo.ts] import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'echo', description: 'Echo back a message', handler: async () => 'Echo: test', }) ``` For tools with parameters, define them using Zod schemas: ```typescript [server/mcp/tools/calculator.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'calculator', inputSchema: { // String input operation: z.string().describe('Operation to perform'), // Number input a: z.number().describe('First number'), b: z.number().describe('Second number'), // Optional field precision: z.number().optional().describe('Decimal precision'), // Enum input format: z.enum(['decimal', 'fraction']).describe('Output format'), // Array input numbers: z.array(z.number()).describe('List of numbers'), }, handler: async ({ operation, a, b, precision, format, numbers }) => { // Handler implementation }, }) ``` ### Common Zod Types | Zod Type | Example | Description | | -------------- | ----------------------------- | ---------------------- | | `z.string()` | `z.string().min(1).max(100)` | String with validation | | `z.number()` | `z.number().min(0).max(100)` | Number with validation | | `z.boolean()` | `z.boolean()` | Boolean value | | `z.array()` | `z.array(z.string())` | Array of values | | `z.object()` | `z.object({ ... })` | Nested object | | `z.enum()` | `z.enum(['a', 'b'])` | Enumeration | | `z.optional()` | `z.string().optional()` | Optional field | | `z.default()` | `z.string().default('value')` | Field with default | ## Output Schema Define structured output using `outputSchema`: ```typescript [server/mcp/tools/bmi.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'calculate-bmi', description: 'Calculate Body Mass Index', inputSchema: { weightKg: z.number().describe('Weight in kilograms'), heightM: z.number().describe('Height in meters'), }, outputSchema: { bmi: z.number(), category: z.string(), }, handler: async ({ weightKg, heightM }) => { const bmi = weightKg / (heightM * heightM) let category = 'Normal' if (bmi < 18.5) category = 'Underweight' else if (bmi >= 25) category = 'Overweight' else if (bmi >= 30) category = 'Obese' return { structuredContent: { bmi: Math.round(bmi * 100) / 100, category, }, } }, }) ``` The `structuredContent` field provides structured data that matches your `outputSchema`, making it easier for AI assistants to work with the results. ## Handler Function The handler is an async function that receives validated input and returns results. You can return simplified values directly — they are automatically wrapped into the MCP `CallToolResult` format. ### Simplified Returns Return a `string`, `number`, `boolean`, object, or array directly from your handler: ::code-group ```typescript [String] handler: async ({ name }) => `Hello ${name}` // → { content: [{ type: 'text', text: 'Hello World' }] } ``` ```typescript [Number] handler: async ({ a, b }) => a + b // → { content: [{ type: 'text', text: '10' }] } ``` ```typescript [Object / Array] handler: async ({ id }) => { const user = await getUser(id) return user } // → { content: [{ type: 'text', text: '{ "id": ... }' }] } ``` ```typescript [Boolean] handler: async ({ id }) => await exists(id) // → { content: [{ type: 'text', text: 'true' }] } ``` :: You can also return the full `CallToolResult` format when you need more control (e.g., images, multiple content items, `structuredContent`). ### Content Types For advanced use cases, return a full `CallToolResult` with typed content: ::code-group ```typescript [Image Content] return { content: [{ type: 'image', data: base64ImageData, mimeType: 'image/png', }], } ``` ```typescript [Structured Content] return { structuredContent: { bmi: 25.5, category: 'Normal', }, } // text content is auto-generated as fallback for older clients ``` ```typescript [Resource Reference] return { content: [{ type: 'resource', resource: { uri: 'file:///path/to/file', text: 'File content', mimeType: 'text/plain', }, }], } ``` :: ### Result Helpers The module provides `imageResult` and `audioResult` helpers for binary media in tool responses (base64-encoded data plus MIME type): ```typescript import { z } from 'zod' import { defineMcpTool, imageResult, audioResult } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ description: 'Generate chart', inputSchema: { data: z.array(z.number()) }, handler: async ({ data }) => { const base64 = await generateChart(data) return imageResult(base64, 'image/png') }, }) ``` ```typescript import { z } from 'zod' import { defineMcpTool, audioResult } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ description: 'Text to speech', inputSchema: { text: z.string() }, handler: async ({ text }) => { const base64 = await synthesizeSpeech(text) return audioResult(base64, 'audio/mp3') }, }) ``` ::callout{color="info" icon="i-lucide-info"} The `textResult` , `jsonResult` , and `errorResult` helpers are deprecated. Return values directly from your handler instead, and throw errors for error cases (see [Errors & caching](https://mcp-toolkit.nuxt.dev/tools/errors-caching) ). :: # Annotations & input examples ## Tool Annotations Annotations are behavioral hints that tell MCP clients how a tool behaves. Clients can use them to decide when to prompt users for confirmation (human-in-the-loop). ```typescript [server/mcp/tools/delete-user.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'delete-user', description: 'Delete a user account', inputSchema: { userId: z.string(), }, annotations: { readOnlyHint: false, // Tool modifies state destructiveHint: true, // Tool performs destructive updates idempotentHint: true, // Deleting the same user twice has no additional effect openWorldHint: false, // Tool does not interact with external systems }, handler: async ({ userId }) => { // ... }, }) ``` ### Annotation Reference | Annotation | Type | Default | Description | | ----------------- | --------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `readOnlyHint` | `boolean` | `false` | If `true`, the tool only reads data without modifying any state (safe to retry). | | `destructiveHint` | `boolean` | `true` | If `true`, the tool may perform destructive operations like deleting data. Only meaningful when `readOnlyHint` is `false`. | | `idempotentHint` | `boolean` | `false` | If `true`, calling the tool multiple times with the same arguments has no additional effect beyond the first call. Only meaningful when `readOnlyHint` is `false`. | | `openWorldHint` | `boolean` | `true` | If `true`, the tool may interact with the outside world (external APIs, internet). If `false`, it only operates on local/internal data. | Here are common annotation patterns for typical tools: ::code-group ```typescript [Read-only tool] // Search, list, lookup, calculate... annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false, } ``` ```typescript [Create tool] // Creates a new record each time annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false, } ``` ```typescript [Update tool] // Updates are idempotent (same input → same result) annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false, } ``` ```typescript [Delete tool] // Destructive and idempotent (deleting twice is the same) annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false, } ``` :: ::callout{color="info" icon="i-lucide-info"} All annotations are **hints** — they are not guaranteed to be respected by every MCP client. Clients should never make security-critical decisions based on annotations from untrusted servers. :: ## Input Examples You can provide concrete usage examples for your tools using `inputExamples`. These examples are type-safe (matching your `inputSchema`) and are transmitted to clients via `_meta.inputExamples`. Input examples help AI models understand how to correctly fill in tool parameters, especially for tools with optional fields or complex inputs. ```typescript [server/mcp/tools/create-todo.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ description: 'Create a new todo', inputSchema: { title: z.string().describe('The title of the todo'), content: z.string().optional().describe('Optional description'), }, inputExamples: [ { title: 'Buy groceries', content: 'Milk, eggs, bread' }, { title: 'Fix login bug' }, // content is optional ], handler: async ({ title, content }) => { // ... }, }) ``` ::callout{color="warning" icon="i-lucide-lightbulb"} `inputExamples` are particularly useful for tools with optional parameters, enums, or complex nested inputs where showing concrete values helps models pick the right format. :: # Errors & caching ## Error Handling Throw errors directly from your handlers — just like in Nitro event handlers. Thrown errors are automatically caught and converted into MCP-compliant `isError` results. ### H3 Errors (Recommended) Use `createError()` from H3 for errors with status codes: ```typescript [server/mcp/tools/get-user.ts] import { z } from 'zod' import { createError } from 'h3' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'get-user', description: 'Get a user by ID', inputSchema: { id: z.string(), }, handler: async ({ id }) => { const user = await findUser(id) if (!user) { throw createError({ statusCode: 404, message: 'User not found' }) } return user }, }) // Error result: { isError: true, content: [{ type: 'text', text: '[404] User not found' }] } ``` H3 errors can also include structured data: ```typescript throw createError({ statusCode: 400, message: 'Validation failed', data: { fields: ['name', 'email'] }, }) // Error text: '[400] Validation failed\n{ "fields": ["name", "email"] }' ``` ### Plain Errors Regular `Error` instances work too: ```typescript [server/mcp/tools/safe-divide.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'safe-divide', inputSchema: { a: z.number(), b: z.number(), }, handler: async ({ a, b }) => { if (b === 0) throw new Error('Division by zero') return a / b }, }) ``` ## Response Caching You can cache tool responses using Nitro's caching system. The `cache` option accepts three formats: ### Simple Duration Use a string duration (parsed by [`ms`](https://www.npmjs.com/package/ms){rel=""nofollow""}) or a number in milliseconds: ```typescript [server/mcp/tools/cached-data.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ description: 'Fetch data with 1 hour cache', inputSchema: { id: z.string(), }, cache: '1h', // or '30m', '2 days', 3600000, etc. handler: async ({ id }) => { return await fetchExpensiveData(id) }, }) ``` ### Full Cache Options For more control, use an object with all Nitro cache options: ```typescript [server/mcp/tools/cached-pages.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ description: 'Get page with custom cache key', inputSchema: { path: z.string(), }, cache: { maxAge: '1h', getKey: args => `page-${args.path}`, swr: true, // stale-while-revalidate }, handler: async ({ path }) => { // ... }, }) ``` ### Cache Options Reference | Option | Type | Required | Description | | ------------- | ------------------ | -------- | ---------------------------------------------------------------------- | | `maxAge` | `string | number` | Yes | Cache duration (e.g., `'1h'`, `3600000`) | | `getKey` | `(args) => string` | No | Custom cache key generator | | `staleMaxAge` | `number` | No | Duration for stale-while-revalidate | | `swr` | `boolean` | No | Enable stale-while-revalidate (defaults to `false`, see warning below) | | `name` | `string` | No | Cache name (auto-generated from tool name) | | `group` | `string` | No | Cache group (default: `'mcp'`) | ::callout{color="info" icon="i-lucide-info"} See the [Nitro Cache documentation](https://nitro.build/guide/cache#options){rel=""nofollow""} for all available options. :: ::callout{color="warning" icon="i-lucide-triangle-alert"} `swr` defaults to `false` (Nitro itself defaults to `true` ). With `swr: true` , stale hits return immediately and the handler refreshes in the background after the request is answered, so request-scoped writes (structured logs, traces) may be dropped. Opt in only when you accept that trade-off. :: # Groups, files & dynamic registration ## Advanced Examples ### Tool with API Integration Here's an example showing a typical API-backed tool: ```typescript [server/mcp/tools/get-weather.ts] import { z } from 'zod' import { createError } from 'h3' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'get-weather', description: 'Get current weather for a city', inputSchema: { city: z.string().describe('City name'), }, handler: async ({ city }) => { const data = await $fetch(`/api/weather/${city}`) if (!data) throw createError({ statusCode: 404, message: `City "${city}" not found` }) return data }, }) ``` ## Groups and Tags Organize your tools with `group` and `tags` for filtering and progressive discovery. Groups and tags are exposed in `_meta` and will map to [SEP-1300](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1300){rel=""nofollow""} when adopted. ### Explicit Group and Tags Set `group` and `tags` directly on the definition: ```typescript [server/mcp/tools/delete-user.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ group: 'admin', tags: ['destructive', 'user-management'], description: 'Delete a user account', inputSchema: { userId: z.string(), }, handler: async ({ userId }) => { // ... }, }) ``` ### Auto-Inferred Group from Directory Place tools in subdirectories and the group is inferred automatically: ```text server/mcp/tools/ ├── admin/ │ ├── delete-user.ts → group: 'admin' │ └── stats.ts → group: 'admin' ├── content/ │ └── list-pages.ts → group: 'content' └── search.ts → no group ``` An explicit `group` on the definition always takes precedence over the directory-inferred value. ### How Clients See Groups and Tags Groups and tags are included in the `_meta` field of `tools/list` responses: ```json { "name": "delete-user", "_meta": { "group": "admin", "tags": ["destructive", "user-management"] } } ``` MCP clients can use these values to filter, sort, or group tools in their UI. ::callout{color="info" icon="i-lucide-info"} For **resources** and **prompts** , `group` and `tags` are stored on the definition objects but are not yet exposed in protocol responses ( `resources/list` , `prompts/list` ). This will be supported when [SEP-1300](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1300){rel=""nofollow""} is adopted by the MCP SDK. :: ## File Organization Organize your tools in the `server/mcp/tools/` directory. Both flat and nested layouts are supported: ```text server/ └── mcp/ └── tools/ ├── echo.ts ├── calculator.ts ├── admin/ │ ├── delete-user.ts │ └── stats.ts └── content/ └── list-pages.ts ``` Each file should export a default tool definition. Subdirectories automatically set the `group` for all tools within them. ## Type Safety The module provides full TypeScript type inference: ```typescript // Input types are inferred from inputSchema handler: async ({ message }) => { // message is typed as string } // Output types are inferred from outputSchema const result = { structuredContent: { bmi: 25.5, // number category: '...', // string }, } ``` ## Conditional Registration You can control whether a tool is visible to clients using the `enabled` guard: ```typescript [server/mcp/tools/admin-tool.ts] import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export default defineMcpTool({ name: 'admin-tool', description: 'Admin-only tool', enabled: event => event.context.user?.role === 'admin', handler: async () => { // ... }, }) ``` When `enabled` returns `false`, the tool is hidden from `tools/list` and cannot be called. ::callout{color="primary" icon="i-lucide-book-open"} See the [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) guide for detailed documentation on auth-based filtering. :: ## Next Steps - [Resources](https://mcp-toolkit.nuxt.dev/resources/overview) - Create resources to expose data - [Prompts](https://mcp-toolkit.nuxt.dev/prompts/overview) - Create reusable prompts - [Handlers](https://mcp-toolkit.nuxt.dev/handlers/overview) - Create custom MCP endpoints - [Code Mode](https://mcp-toolkit.nuxt.dev/advanced/code-mode) - Let LLMs orchestrate tools with JavaScript - [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) - Conditionally register definitions - [Examples](https://mcp-toolkit.nuxt.dev/examples/common-patterns) - See more tool examples # Resources ## What are Resources? Resources are a standardized way for MCP servers to expose **read-only data** to clients. They provide contextual information that can help AI models understand your application, such as files, database schemas, configuration, or any data accessible via a URI. ::prompt --- actions: - copy - cursor - windsurf description: Scaffold a new MCP resource icon: i-lucide-package --- Create a new MCP resource in my Nuxt app using @nuxtjs/mcp-toolkit. - Create a file in server/mcp/resources/ (e.g. server/mcp/resources/readme.ts) - Use defineMcpResource (auto-imported) with a description - For local files, use the file property: file: 'README.md' (URI, MIME type, and handler are auto-generated) - For custom data, define uri and handler manually, returning { contents: [{ uri, text, mimeType }] } - For dynamic resources, use ResourceTemplate from @modelcontextprotocol/sdk/server/mcp.js with URI variables - Name and title are auto-generated from the filename - Use subdirectories to auto-infer groups (e.g. resources/config/app.ts → group: 'config') Docs: {rel=""nofollow""} :: ::callout{color="primary" icon="i-lucide-lightbulb"} **Key concept** : Unlike [tools](https://mcp-toolkit.nuxt.dev/tools/overview) which are invoked directly by the AI to perform actions, resources are **application-driven** . The host application (not the AI) decides when and how to fetch and include resource content in the conversation. :: Each resource is uniquely identified by a URI (e.g., `file:///project/README.md` or `api://users/123`). ## Resources vs Tools Understanding the difference between resources and tools is essential: | Aspect | Resources | Tools | | -------------- | ----------------------------------------- | ---------------------------------- | | **Purpose** | Provide context and data | Perform actions | | **Invocation** | Application-driven (user or app selects) | AI-driven (model decides to call) | | **Nature** | Read-only data access | Can read and modify state | | **Control** | User/application controls what's included | AI decides when to use | | **Examples** | Files, configs, DB schemas, logs | Send email, create file, query API | **When to use resources:** - Exposing project files or documentation - Sharing database schemas or configurations - Providing logs or system information as context **When to use tools:** - Performing actions that modify state - Executing operations the AI should decide to trigger - Interacting with external APIs or services ## How Resources are Used Resources follow an **application-driven** model. The typical flow: 1. **Discovery** — the host application calls `resources/list` to discover available resources. 2. **Selection** — the host displays resources in a UI (tree view, search, list) and the user (or application logic) picks which ones to include. 3. **Reading** — the host fetches the selected URIs via `resources/read`. 4. **Context inclusion** — the host injects the contents into the AI conversation as context. ::callout{color="info" icon="i-lucide-info"} The AI model never directly requests resources. It's always the application that decides which resources to include based on user selection, heuristics, or automatic context detection. :: ## Going further ::card-group :::card --- color: neutral icon: i-lucide-file title: File operations example to: https://mcp-toolkit.nuxt.dev/examples/file-operations --- Read local files as MCP resources with graceful error handling. ::: :::card --- color: neutral icon: i-lucide-wrench title: Tools to: https://mcp-toolkit.nuxt.dev/tools/overview --- Pair resources with actions — tools are AI-driven, resources are application-driven. ::: :::card --- color: neutral icon: i-lucide-toggle-right title: Dynamic definitions to: https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions --- Hide resources from anonymous clients with the `enabled` guard. ::: :::card --- color: neutral icon: i-lucide-list title: Listing definitions to: https://mcp-toolkit.nuxt.dev/advanced/listing-definitions --- Read your discovered catalog from your own server routes. ::: :: If you are new to MCP in Nuxt, start with [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) — resources complement tools but follow a different discovery model. # Static resources & structure ## Static Resources Static resources have a fixed URI that doesn't change. ### Simple File Resources The easiest way to expose a local file is using the `file` property. This automatically handles the URI generation, MIME type detection, and file reading. ```typescript [server/mcp/resources/readme.ts] import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'readme', description: 'Project README file', file: 'README.md', // Relative to project root }) ``` This generates: - **URI**: `file:///path/to/project/README.md` - **Handler**: Automatically reads the file content - **MIME Type**: Automatically detected (e.g., `text/markdown`) ### Custom Static Resources For more control, you can define the `uri` and `handler` manually: ```typescript [server/mcp/resources/custom-readme.ts] import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'custom-readme', title: 'README', description: 'Project README file', uri: 'file:///README.md', metadata: { mimeType: 'text/markdown', }, handler: async (uri: URL) => { const filePath = fileURLToPath(uri) const content = await readFile(filePath, 'utf-8') return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: content, }], } }, }) ``` ## Auto-Generated Name and Title You can omit `name` and `title` - they will be automatically generated from the filename: ```typescript [server/mcp/resources/project-readme.ts] import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ // name and title are auto-generated from filename: // name: 'project-readme' // title: 'Project Readme' file: 'README.md' }) ``` The filename `project-readme.ts` automatically becomes: - `name`: `project-readme` (kebab-case) - `title`: `Project Readme` (title case) You can still provide `name` or `title` explicitly to override the auto-generated values. ## Resource Structure A resource definition consists of: ::code-group ```typescript [File Resource] import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'resource-name', file: 'path/to/file.txt', // Local file path metadata: { ... } }) ``` ```typescript [Custom Resource] import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'resource-name', // Unique identifier uri: 'uri://...', // Static URI or ResourceTemplate handler: async (uri) => { // Handler function return { contents: [...] } }, }) ``` :: # Templates & handlers ## Dynamic Resources with Templates Use `ResourceTemplate` to create dynamic resources that accept variables: ```typescript [server/mcp/resources/file.ts] import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js' import type { Variables } from '@modelcontextprotocol/sdk/shared/uriTemplate.js' import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'file', title: 'File Resource', uri: new ResourceTemplate('file:///project/{+path}', { list: async () => { // Return list of available resources return { resources: [ { uri: 'file:///project/README.md', name: 'README.md' }, { uri: 'file:///project/src/index.ts', name: 'src/index.ts' }, ], } }, }), handler: async (uri: URL, variables: Variables) => { const path = variables.path as string const filePath = join(process.cwd(), path) const content = await readFile(filePath, 'utf-8') return { contents: [{ uri: uri.toString(), mimeType: 'text/plain', text: content, }], } }, }) ``` ## ResourceTemplate `ResourceTemplate` allows you to create resources with variable parts in the URI: ```typescript new ResourceTemplate('file:///project/{+path}', { list: async () => { // Optional: Return list of available resources return { resources: [ { uri: 'file:///project/file1.txt', name: 'File 1' }, { uri: 'file:///project/file2.txt', name: 'File 2' }, ], } }, }) ``` ### Template Variables Variables in the URI are defined with `{variableName}`: ```typescript // Single variable new ResourceTemplate('file:///project/{path}', { ... }) // Variable allowing slashes (reserved expansion) new ResourceTemplate('file:///project/{+path}', { ... }) // Multiple variables new ResourceTemplate('api://users/{userId}/posts/{postId}', { ... }) ``` ## Handler Function The handler receives the resolved URI and optional variables: ```typescript // Static resource handler handler: async (uri: URL) => { return { contents: [{ uri: uri.toString(), mimeType: 'text/plain', text: 'Content', }], } } // Dynamic resource handler handler: async (uri: URL, variables: Variables) => { const path = variables.path as string // Use variables to resolve the resource return { contents: [{ uri: uri.toString(), mimeType: 'text/plain', text: 'Content', }], } } ``` # Metadata, content & errors ## Resource Metadata Add a `metadata` block to help clients render the resource correctly. It carries the MIME type, behavior annotations, and any extra fields you want to surface in `resources/list`: ```typescript [server/mcp/resources/readme.ts] import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'readme', description: 'Project README file', file: 'README.md', metadata: { mimeType: 'text/markdown', annotations: { audience: ['user', 'assistant'], priority: 0.8, lastModified: new Date().toISOString(), }, }, }) ``` | Field | Type | Description | | ----------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------- | | `metadata.mimeType` | `string` | Hint to clients about the content type. Auto-detected when you use `file: '…'`; set explicitly for custom URIs. | | `metadata.annotations.audience` | `('user' | 'assistant')[]` | Who should see this resource — the user, the AI, or both. | | `metadata.annotations.priority` | `number` (0–1) | Suggested importance when clients have to choose which resources to include. | | `metadata.annotations.lastModified` | `string` (ISO 8601) | When the resource last changed — clients may use this for caching. | Anything else you put inside `metadata` is forwarded verbatim under `_meta` in the listing response, so you can carry custom fields for your own UI. ## Content Types Resources can return different MIME types: ::code-group ```typescript [Text/Markdown] return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: '# Markdown content', }], } ``` ```typescript [JSON] return { contents: [{ uri: uri.toString(), mimeType: 'application/json', text: JSON.stringify({ key: 'value' }), }], } ``` ```typescript [Binary Data] return { contents: [{ uri: uri.toString(), mimeType: 'image/png', blob: Buffer.from(binaryData), }], } ``` :: ## Error Handling Handle errors gracefully in your handlers: ```typescript [server/mcp/resources/custom-readme.ts] import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ name: 'readme', uri: 'file:///README.md', handler: async (uri: URL) => { try { const filePath = fileURLToPath(uri) const content = await readFile(filePath, 'utf-8') return { contents: [{ uri: uri.toString(), mimeType: 'text/markdown', text: content, }], } } catch (error) { return { contents: [{ uri: uri.toString(), mimeType: 'text/plain', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }], isError: true, } } }, }) ``` # Groups & organization ## Groups and Tags Organize your resources with `group` and `tags` for categorization. These fields work the same way as for [tools](https://mcp-toolkit.nuxt.dev/tools/groups-organization#groups-and-tags). ```typescript [server/mcp/resources/config/app-settings.ts] import { defineMcpResource } from '@nuxtjs/mcp-toolkit/server' export default defineMcpResource({ group: 'config', tags: ['readonly', 'settings'], description: 'Application settings', file: 'config/app.json', }) ``` You can also place resource files in subdirectories to auto-infer the group: ```text server/mcp/resources/ ├── config/ │ └── app-settings.ts → group: 'config' ├── docs/ │ └── readme.ts → group: 'docs' └── schema.ts → no group ``` ::callout{color="info" icon="i-lucide-info"} Resource `group` and `tags` are stored on the definition objects but are not yet included in `resources/list` protocol responses. This will be supported when [SEP-1300](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1300){rel=""nofollow""} is adopted by the MCP SDK. :: ## File Organization Organize your resources in the `server/mcp/resources/` directory. Both flat and nested layouts are supported: ```text server/ └── mcp/ └── resources/ ├── readme.ts ├── file.ts └── config/ └── app-settings.ts ``` Each file should export a default resource definition. Subdirectories automatically set the `group`. ## URI Schemes You can use any URI scheme that makes sense for your use case: - `file://` - File system resources - `api://` - API endpoints - `http://` / `https://` - Web resources - `custom://` - Custom schemes ## Conditional Registration You can control whether a resource is visible to clients using the `enabled` guard: ```typescript [server/mcp/resources/internal-data.ts] export default defineMcpResource({ name: 'internal-data', uri: 'app://internal', enabled: event => event.context.user?.role === 'admin', handler: async (uri) => ({ contents: [{ uri: uri.toString(), text: 'Internal data...' }], }), }) ``` ::callout{color="primary" icon="i-lucide-book-open"} See the [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) guide for detailed documentation on auth-based filtering. :: ## Next Steps - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Create tools to perform actions - [Prompts](https://mcp-toolkit.nuxt.dev/prompts/overview) - Create reusable prompts - [Handlers](https://mcp-toolkit.nuxt.dev/handlers/overview) - Create custom MCP endpoints - [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) - Conditionally register definitions - [Examples](https://mcp-toolkit.nuxt.dev/examples/file-operations) - More resource examples # Prompts ## What are Prompts? Prompts are reusable message templates that can be used by AI assistants. They can include dynamic arguments and return pre-formatted messages. ::prompt --- actions: - copy - cursor - windsurf description: Scaffold a new MCP prompt icon: i-lucide-message-square --- Create a new MCP prompt in my Nuxt app using @nuxtjs/mcp-toolkit. - Create a file in server/mcp/prompts/ (e.g. server/mcp/prompts/review-code.ts) - Use defineMcpPrompt (auto-imported) with a description and handler - The handler can return a string (auto-wrapped as a user message) or a full GetPromptResult with messages array - Define arguments with `import { z } from 'zod'` in inputSchema (prompt arguments must be strings) - Use the role option ('user' or 'assistant') to control the default role for string returns - Use completable() to provide autocompletion suggestions for arguments - Return multiple messages to create a conversation flow - Name and title are auto-generated from the filename - Prompts appear in Cursor and VS Code when typing / in the chat Docs: {rel=""nofollow""} :: ## Why Use Prompts? MCP prompts offer several advantages over ad-hoc instructions: ::card-group :::card{icon="i-lucide-repeat" title="Reusability"} Define once, use everywhere. Share prompts across your team for consistent AI interactions. ::: :::card{icon="i-lucide-check-square" title="Standardization"} Ensure consistent formatting and context for specific tasks like code reviews or documentation. ::: :::card{icon="i-lucide-settings-2" title="Customization"} Use arguments to adapt prompts to different contexts while maintaining structure. ::: :::card{icon="i-lucide-plug" title="IDE Integration"} Prompts appear in Cursor, VS Code, and Visual Studio for easy access during development. ::: :: ## IDE Integration MCP prompts integrate seamlessly with modern development environments. When your MCP server is connected, prompts become available directly in your IDE. ### Using Prompts in Cursor / VS Code 1. **Type `/`** — in the chat, type `/` to see all available MCP prompts. 2. **Select a prompt** — choose from the list (e.g. `local-mcp/setup-mcp-server`). 3. **Fill arguments** — for parameterized prompts, a dialog appears to collect the values defined in your `inputSchema`. 4. **Send** — the prompt's messages are inserted into the conversation as if you had typed them. Prompts surface with the prefix `/`, so renaming your handler in `defineMcpHandler({ name })` changes how they appear in clients. ## Going further ::card-group :::card --- color: neutral icon: i-lucide-pen-line title: Authoring & structure to: https://mcp-toolkit.nuxt.dev/prompts/authoring --- Auto-generated names, simple prompts, default roles, and prompt structure. ::: :::card --- color: neutral icon: i-lucide-layers title: Input, handler & messages to: https://mcp-toolkit.nuxt.dev/prompts/input-handler-messages --- Zod arguments, completable autocomplete, return types, and multi-message conversations. ::: :::card --- color: neutral icon: i-lucide-line-chart title: Patterns & advanced to: https://mcp-toolkit.nuxt.dev/prompts/patterns-advanced --- Real-world examples, groups, type safety, best practices, and conditional registration. ::: :::card --- color: neutral icon: i-lucide-message-square title: Prompt examples to: https://mcp-toolkit.nuxt.dev/examples/prompt-examples --- Code review, documentation, email, and commit-message prompts you can copy. ::: :: ## Next Steps - [Authoring & structure](https://mcp-toolkit.nuxt.dev/prompts/authoring) — write your first prompt. - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) — pair prompts with actions the AI can take. - [Resources](https://mcp-toolkit.nuxt.dev/resources/overview) — feed context alongside your prompts. # Authoring & structure ## Auto-Generated Name and Title You can omit `name` and `title` - they will be automatically generated from the filename: ```typescript [server/mcp/prompts/greeting.ts] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ // name and title are auto-generated from filename: // name: 'greeting' // title: 'Greeting' description: 'Generate a personalized greeting message', handler: async () => { // ... }, }) ``` The filename `greeting.ts` automatically becomes: - `name`: `greeting` (kebab-case) - `title`: `Greeting` (title case) You can still provide `name` or `title` explicitly to override the auto-generated values. ## Simple Prompt (No Arguments) Create a prompt without arguments. Handlers can return a simple string — it will be automatically wrapped into a single user message: ```typescript [server/mcp/prompts/greeting.ts] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'greeting', title: 'Greeting', description: 'Generate a personalized greeting message', handler: async () => { const hour = new Date().getHours() const timeOfDay = hour < 12 ? 'morning' : hour < 18 ? 'afternoon' : 'evening' return `Good ${timeOfDay}! How can I help you today?` }, }) ``` ### Default Role When a handler returns a string, it is wrapped with the `user` role by default. Use the `role` option to change this: ```typescript [server/mcp/prompts/code-reviewer.ts] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ role: 'assistant', description: 'Code review assistant persona', handler: async () => 'I am a code review assistant. Share your code and I will review it for best practices.', }) ``` ::callout{color="info" icon="i-lucide-info"} The `role` option only affects string returns. When returning a full `GetPromptResult` , define roles directly in the `messages` array. :: ## Prompt with Arguments Create a prompt that accepts arguments: ```typescript [server/mcp/prompts/summarize.ts] import { z } from 'zod' import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'summarize', title: 'Text Summarizer', description: 'Summarize any text content', inputSchema: { text: z.string().describe('The text to summarize'), maxLength: z.string().optional().describe('Maximum length of summary in words'), }, handler: async ({ text, maxLength }) => { const words = text.split(/\s+/) const maxWords = maxLength ? Number.parseInt(maxLength) : Math.ceil(words.length * 0.3) const summary = words.slice(0, maxWords).join(' ') return `Summary (${maxWords} words): ${summary}${words.length > maxWords ? '...' : ''}` }, }) ``` ## Prompt Structure A prompt definition consists of: ::code-group ```typescript [Simple Prompt] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'prompt-name', // Unique identifier handler: async () => 'Your prompt text here', }) ``` ```typescript [With Role] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'prompt-name', role: 'assistant', // Role for string returns (default: 'user') handler: async () => 'I am an assistant persona.', }) ``` ```typescript [Prompt with Arguments] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'prompt-name', title: 'Prompt Title', // Human-readable title description: 'Description', // What the prompt does inputSchema: { ... }, // Zod schema for arguments handler: async (args) => { // Handler with arguments return `Prompt text with ${args.param}` }, }) ``` :: # Input, handler & messages ## Input Schema Use Zod to define and validate prompt arguments: ```typescript [server/mcp/prompts/translate.ts] import { z } from 'zod' import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'translate', inputSchema: { // Required string argument text: z.string().describe('Text to translate'), // Required enum argument targetLanguage: z.enum(['en', 'fr', 'es', 'de']).describe('Target language'), // Optional argument sourceLanguage: z.string().optional().describe('Source language (auto-detect if not provided)'), // Optional with default formality: z.enum(['formal', 'informal']).default('formal'), }, handler: async ({ text, targetLanguage, sourceLanguage, formality }) => { // Implementation }, }) ``` ### Common Argument Types | Zod Type | Example | Description | | -------------- | ----------------------------- | ---------------------- | | `z.string()` | `z.string().min(1)` | String with validation | | `z.enum()` | `z.enum(['a', 'b'])` | Enumeration | | `z.optional()` | `z.string().optional()` | Optional field | | `z.default()` | `z.string().default('value')` | Field with default | ::callout{color="info" icon="i-lucide-info"} **Note** : Prompt arguments must be strings. Use `z.string()` and convert to other types in your handler if needed. :: ### Argument Autocompletion Wrap a schema field with `completable()` to provide autocompletion suggestions when clients fill in prompt arguments: ```typescript [server/mcp/prompts/review-code.ts] export default defineMcpPrompt({ description: 'Review code for best practices', inputSchema: { language: completable( z.string().describe('Programming language'), value => ['typescript', 'javascript', 'python', 'rust', 'go'] .filter(lang => lang.startsWith(value)), ), }, handler: async ({ language }) => { return `Review the following ${language} code for best practices and potential issues.` }, }) ``` The `completable` helper is auto-imported and re-exported from the MCP SDK. The callback receives the current input value and returns matching suggestions. ## Handler Function The handler receives validated arguments (if `inputSchema` is provided) and returns a prompt result. ### Return Types Handlers support two return types: ::code-group ```typescript [String (recommended)] // Return a string — auto-wrapped into a single user message handler: async () => 'You are a helpful assistant.' // With arguments handler: async ({ topic }) => `Help me understand ${topic}.` ``` ```typescript [Full GetPromptResult] // Return the full MCP result for multi-message or assistant-role prompts handler: async () => ({ messages: [ { role: 'user', content: { type: 'text', text: 'Review this code.' } }, { role: 'assistant', content: { type: 'text', text: 'I will review it.' } }, ], }) ``` :: ::callout{color="info" icon="i-lucide-info"} When returning a string, it is automatically wrapped into `{ messages: [{ role, content: { type: 'text', text: '...' } }] }` using the `role` option (defaults to `'user'` ). :: ### Handler Arguments ```typescript // Without inputSchema — no arguments handler: async () => 'Message text' // With inputSchema — receives validated arguments handler: async (args, extra) => { // args: Validated arguments matching inputSchema // extra: Request handler extra information return `Message with ${args.param}` } ``` ## Message Roles Prompts can return messages with different roles: ::code-group ```typescript [User Message] return { messages: [{ role: 'user', content: { type: 'text', text: 'User message with instructions', }, }], } ``` ```typescript [Assistant Message] return { messages: [{ role: 'assistant', content: { type: 'text', text: 'Pre-filled assistant response', }, }], } ``` :: ::callout{color="info" icon="i-lucide-info"} **Note** : The MCP specification only supports `user` and `assistant` roles. To provide context or instructions, include them in the `user` message text. :: ## Multiple Messages Return multiple messages to create a conversation flow: ```typescript [server/mcp/prompts/conversation.ts] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ name: 'conversation-starter', inputSchema: { topic: z.string().describe('Conversation topic'), }, handler: async ({ topic }) => { return { messages: [ { role: 'user', content: { type: 'text', text: `You are a helpful assistant. Let's discuss ${topic}.`, }, }, { role: 'assistant', content: { type: 'text', text: `I'd be happy to discuss ${topic} with you.`, }, }, ], } }, }) ``` # Patterns & advanced ## Use Cases Prompts are particularly useful for: ### 1. Setup and Onboarding Help new developers or AI assistants understand how to work with your codebase: ```typescript [server/mcp/prompts/setup-guide.ts] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ description: 'Provide complete setup instructions for this project', handler: async () => `You are setting up this Nuxt project. Here's what you need to know: 1. Install dependencies: \`pnpm install\` 2. Start dev server: \`pnpm dev\` 3. Project structure follows Nuxt conventions 4. MCP tools are available in server/mcp/ Ask me what you'd like to build!`, }) ``` ### 2. Code Review Standards Ensure consistent code review criteria: ```typescript [server/mcp/prompts/review-standards.ts] import { z } from 'zod' import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ description: 'Apply team code review standards', inputSchema: { focus: z.enum(['security', 'performance', 'maintainability', 'all']).default('all'), }, handler: async ({ focus }) => `You are a code reviewer following our team standards. Focus on: ${focus}. Review the code I provide, checking for best practices and potential issues.`, }) ``` ### 3. Documentation Generation Standardize documentation format: ```typescript [server/mcp/prompts/generate-docs.ts] import { z } from 'zod' import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ description: 'Generate documentation in team format', inputSchema: { type: z.enum(['api', 'component', 'function']).describe('What to document'), }, handler: async ({ type }) => { const templates = { api: 'Document this API endpoint with: endpoint, method, parameters, response format, and examples.', component: 'Document this Vue component with: props, emits, slots, and usage examples.', function: 'Document this function with: parameters, return value, and usage examples.', } return templates[type] }, }) ``` ### 4. Troubleshooting Workflows Guide debugging for common issues: ```typescript [server/mcp/prompts/debug-helper.ts] import { z } from 'zod' import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ description: 'Help debug common issues', inputSchema: { area: z.enum(['api', 'auth', 'database', 'frontend']).describe('Area of the issue'), }, handler: async ({ area }) => `You are debugging a ${area} issue. Ask clarifying questions and suggest diagnostic steps.`, }) ``` ## Groups and Tags Organize your prompts with `group` and `tags` for categorization. These fields work the same way as for [tools](https://mcp-toolkit.nuxt.dev/tools/groups-organization#groups-and-tags). ```typescript [server/mcp/prompts/onboarding/setup-guide.ts] import { defineMcpPrompt } from '@nuxtjs/mcp-toolkit/server' export default defineMcpPrompt({ tags: ['getting-started'], description: 'Help new developers set up the project', handler: async () => 'You are setting up this project...', }) ``` Placing the file in `prompts/onboarding/` auto-infers `group: 'onboarding'`. ::callout{color="info" icon="i-lucide-info"} Prompt `group` and `tags` are stored on the definition objects but are not yet included in `prompts/list` protocol responses. This will be supported when [SEP-1300](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1300){rel=""nofollow""} is adopted by the MCP SDK. :: ## File Organization Organize your prompts in the `server/mcp/prompts/` directory. Both flat and nested layouts are supported: ```text server/ └── mcp/ └── prompts/ ├── greeting.ts ├── summarize.ts ├── onboarding/ │ └── setup-guide.ts └── debugging/ └── troubleshoot.ts ``` Each file should export a default prompt definition. Subdirectories automatically set the `group`. ## Type Safety The module provides full TypeScript type inference: ```typescript // Argument types are inferred from inputSchema handler: async ({ text, maxLength }) => { // text is typed as string // maxLength is typed as string | undefined } ``` ## Best Practices ### 1. Design for AI Understanding Write prompts that give the AI clear context and expectations: ```typescript // Good: Clear context and instructions handler: async ({ code }) => `You are a senior developer reviewing code for a Nuxt application. Review this code for Vue 3 best practices:\n\n${code}` // Less effective: Vague instructions handler: async ({ code }) => code ``` ### 2. Use Descriptive Arguments Always use `.describe()` on Zod fields to help both users and AI understand what's expected: ```typescript inputSchema: { // Good: Clear descriptions language: z.enum(['typescript', 'javascript']).describe('Programming language of the code'), strict: z.boolean().default(true).describe('Whether to enforce strict TypeScript rules'), // Less helpful: No descriptions lang: z.string(), s: z.boolean(), } ``` ### 3. Use Conversation Flow Use user and assistant messages to guide the AI: ```typescript // Effective: User provides context, assistant acknowledges messages: [ { role: 'user', content: { type: 'text', text: 'You are an expert in accessibility. Review this HTML for a11y issues.' } }, { role: 'assistant', content: { type: 'text', text: 'I\'ll analyze the HTML for accessibility issues.' } }, ] ``` ### 4. Keep Prompts Focused Each prompt should have a single, clear purpose. Create multiple prompts instead of one complex one: ```typescript // Good: Separate focused prompts // server/mcp/prompts/review-security.ts // server/mcp/prompts/review-performance.ts // server/mcp/prompts/review-style.ts // Less maintainable: One complex prompt trying to do everything ``` ### 5. Provide Default Values Use `.default()` for optional arguments to improve usability: ```typescript inputSchema: { format: z.enum(['brief', 'detailed']).default('detailed').describe('Output format'), language: z.string().default('en').describe('Response language'), } ``` ### 6. Include Examples in Complex Prompts For prompts that need specific output formats, include examples: ```typescript handler: async () => `Generate a commit message following this format: type(scope): description Example: feat(auth): add OAuth2 login support Types: feat, fix, docs, style, refactor, test, chore` ``` ## Conditional Registration You can control whether a prompt is visible to clients using the `enabled` guard: ```typescript [server/mcp/prompts/admin-prompt.ts] export default defineMcpPrompt({ name: 'admin-prompt', description: 'Admin-only prompt', enabled: event => event.context.user?.role === 'admin', handler: async () => 'Admin instructions...', }) ``` ::callout{color="primary" icon="i-lucide-book-open"} See the [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) guide for detailed documentation on auth-based filtering. :: ## Next Steps - [Prompt Examples](https://mcp-toolkit.nuxt.dev/examples/prompt-examples) - See advanced prompt examples - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Create tools to perform actions - [Resources](https://mcp-toolkit.nuxt.dev/resources/overview) - Create resources to expose data - [Handlers](https://mcp-toolkit.nuxt.dev/handlers/overview) - Create custom MCP endpoints - [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) - Conditionally register definitions # Handlers ## What are Handlers? Handlers allow you to create **multiple MCP endpoints** in a single Nuxt application. Each handler has its own route, name, version, and can include its own set of tools, resources, and prompts. ::prompt --- actions: - copy - cursor - windsurf description: Create a custom MCP handler and endpoint icon: i-lucide-server --- Create a custom MCP handler with its own endpoint and tools using @nuxtjs/mcp-toolkit. - Create a .ts file at the root of server/mcp/ (e.g. server/mcp/admin.ts) — files in subdirectories like tools/, resources/, prompts/ are definitions, not handlers - Use defineMcpHandler (auto-imported) with name, route, tools, resources, and prompts - Set the route explicitly (e.g. route: '/mcp/admin') - Define tools inline with defineMcpTool or import shared tools - Override the default handler by creating server/mcp/index.ts with defineMcpHandler - Add middleware for handler-specific authentication or logging - Set version and browserRedirect per handler Docs: {rel=""nofollow""} :: This is useful when you want to: - Separate different MCP functionalities into different endpoints - Create versioned MCP APIs - Organize tools/resources by domain or feature ## Guides ::card-group :::card --- color: neutral icon: i-lucide-network title: Multi-handler organization to: https://mcp-toolkit.nuxt.dev/handlers/organization --- Folder convention to attribute tools, resources, and prompts to MCP routes — plus the `getMcp*` escape hatch. ::: :::card --- color: neutral icon: i-lucide-toggle-left title: Default & custom handlers to: https://mcp-toolkit.nuxt.dev/handlers/default-and-custom --- Override `/mcp` via `server/mcp/index.ts` , or expose new endpoints with `defineMcpHandler` . ::: :::card --- color: neutral icon: i-lucide-sliders-horizontal title: Structure & options to: https://mcp-toolkit.nuxt.dev/handlers/structure-and-options --- Every `defineMcpHandler` field — `name` , `route` , `middleware` , `tools` , `experimental_codeMode` . ::: :::card --- color: neutral icon: i-lucide-share-2 title: Sharing & practices to: https://mcp-toolkit.nuxt.dev/handlers/sharing-practices --- Cross-handler tool sharing, file layout, and use cases. ::: :: # Default & custom handlers ## Default Handler By default, the module creates a single MCP endpoint at `/mcp` (or your configured route) that includes all tools, resources, and prompts from the `server/mcp/` directory. ### Overriding the Default Handler You can override the default handler's configuration by creating an `index.ts` file in `server/mcp/`: ```typescript [server/mcp/index.ts] export default defineMcpHandler({ version: '2.0.0', browserRedirect: '/docs', // If tools/resources/prompts not specified, uses global definitions }) ``` This allows you to customize: - `version` - Override the server version - `browserRedirect` - Override the browser redirect URL - `name` - Override the server name (optional) - `tools`, `resources`, `prompts` - Use specific definitions instead of global ones - `middleware` - Add request interception for auth, logging, etc. ([learn more](https://mcp-toolkit.nuxt.dev/advanced/middleware)) ::callout{color="info" icon="i-lucide-info"} The `route` property is **ignored** for the default handler. To change the route, use `mcp.route` in your `nuxt.config.ts` . :: #### Example: Custom Version and Redirect ```typescript [server/mcp/index.ts] export default defineMcpHandler({ name: 'My Documentation MCP', version: '1.2.0', browserRedirect: '/getting-started', }) ``` #### Example: Limiting Exposed Tools ```typescript [server/mcp/index.ts] import { myTool, anotherTool } from './tools/my-tools' export default defineMcpHandler({ // Only expose specific tools instead of all tools in server/mcp/tools/ tools: [myTool, anotherTool], }) ``` ## Custom Handlers Create custom handlers using `defineMcpHandler`: ```typescript [server/mcp/migration.ts] import { z } from 'zod' import { defineMcpTool, defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' const migrationTool = defineMcpTool({ name: 'migrate-v3-to-v4', title: 'Migrate v3 to v4', description: 'Migrate code from version 3 to version 4', inputSchema: { code: z.string().describe('The code to migrate'), }, handler: async ({ code }) => { return code.replace(/v3/g, 'v4') }, }) export default defineMcpHandler({ name: 'migration', version: '0.1.0', route: '/mcp/migration', tools: [migrationTool], browserRedirect: '/', }) ``` # Structure & options ## Handler Structure A handler definition consists of: ::code-group ```typescript [Required Fields] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'handler-name', // Unique identifier }) ``` ```typescript [Optional Fields] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'handler-name', version: '1.0.0', // Handler version description: 'Admin tools', // serverInfo description shown by clients instructions: 'Always …', // System-prompt guidance for LLMs icons: [ ... ], // Server icons shown in client UIs route: '/mcp/custom', // Custom route browserRedirect: '/', // Browser redirect URL middleware: async (event) => { ... }, // Request interception tools: [ ... ], // Array of tools resources: [ ... ], // Array of resources prompts: [ ... ], // Array of prompts experimental_codeMode: true, // Enable code mode (experimental) }) ``` :: ## Handler Options ### `name` (required) Unique identifier for the handler. The `name` determines where the handler will be mounted. By default, the handler will be accessible at `/mcp/:name`. ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'migration', // Handler mounted at /mcp/migration }) ``` ### `version` (optional) Version of the handler. Defaults to the module's configured version. ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'migration', version: '2.0.0', }) ``` ### `description` (optional) Human-readable description sent as part of `serverInfo` during MCP initialization. Clients use it to identify the handler in UIs (server lists, install prompts, tooltips). Falls back to `mcp.description` from `nuxt.config.ts`. ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'admin', description: 'Admin tools — destructive operations gated by Bearer auth.', }) ``` ### `instructions` (optional) Operational guidance for AI agents — typically injected by clients into the model's system prompt. Use this to describe workflows, constraints, or relationships between tools (use `description` to identify the handler). Falls back to `mcp.instructions` from `nuxt.config.ts`. ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'admin', instructions: 'Always call list-users before delete-user. Confirm with the operator before any destructive action.', }) ``` ### `icons` (optional) Icons displayed by clients in their UIs. Each entry needs `src` and `mimeType`, with optional `sizes` and `theme` (`'light' | 'dark'`). Falls back to `mcp.icons` from `nuxt.config.ts`. ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'admin', icons: [ { src: 'https://example.com/admin.png', mimeType: 'image/png', sizes: ['64x64'] }, ], }) ``` ::callout{color="info" icon="i-lucide-info"} `description` , `instructions` , and `icons` are part of the [MCP lifecycle spec](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization){rel=""nofollow""} . Set them at the module level for shared metadata, override per-handler when a specific endpoint needs different identity. :: ### `route` (optional) Custom route for the handler. Defaults to `/mcp/:name`. ::callout{color="info" icon="i-lucide-info"} This option is only used for **custom handlers** . For the default handler override ( `index.ts` ), use `mcp.route` in `nuxt.config.ts` instead. :: ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'migration', route: '/api/mcp/migration', // Custom route }) ``` ### `browserRedirect` (optional) URL to redirect browsers when they access the handler endpoint. Defaults to the module's configured `browserRedirect`. ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'migration', browserRedirect: '/docs/migration', }) ``` ### `middleware` (optional) Function to intercept requests before/after they are processed. Useful for authentication, logging, and setting context. ```typescript [server/mcp/custom.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'custom', middleware: async (event) => { event.context.userId = 'user-123' }, }) ``` ::callout{color="primary" icon="i-lucide-book-open"} See the [Middleware guide](https://mcp-toolkit.nuxt.dev/advanced/middleware) for detailed documentation and examples. :: ### `experimental_codeMode` (optional) Enable [Code Mode](https://mcp-toolkit.nuxt.dev/advanced/code-mode) to let LLMs orchestrate multiple tool calls in a single JavaScript execution. Pass `true` for defaults or an options object: ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'custom', experimental_codeMode: { progressive: true, memoryLimit: 128, }, }) ``` Code Mode requires `secure-exec` and Node.js `>=18.16.0`. ::callout{color="primary" icon="i-lucide-book-open"} See the [Code Mode guide](https://mcp-toolkit.nuxt.dev/advanced/code-mode) for full documentation, security details, and configuration options. :: ### `tools` (optional) Accepts three shapes — pick whichever matches your use case: ::code-group ```typescript [Auto (default)] // server/mcp/handlers/admin/index.ts import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' // `tools` omitted → every tool under handlers/admin/tools/ // is auto-registered (folder convention). export default defineMcpHandler({ middleware: requireAdmin, }) ``` ```typescript [Static array] import { defineMcpTool, defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' const tool1 = defineMcpTool({ ... }) const tool2 = defineMcpTool({ ... }) export default defineMcpHandler({ name: 'custom', tools: [tool1, tool2], }) ``` ```typescript [Dynamic function] import { defineMcpHandler, getMcpTools } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ // Every tool tagged 'searchable', regardless of folder. tools: event => getMcpTools({ event, tags: ['searchable'] }), }) ``` :: ::callout{color="primary" icon="i-lucide-book-open"} See [multi-handler organization](https://mcp-toolkit.nuxt.dev/handlers/organization) for the folder convention, and [`getMcpTools`](https://mcp-toolkit.nuxt.dev/advanced/listing-definitions) for ad-hoc filtering. Per-tool `enabled()` guards apply automatically — see [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) . :: ### `resources` (optional) Same shapes as [`tools`](https://mcp-toolkit.nuxt.dev/#tools-optional): omit for folder-convention auto-attribution, pass an array, or pass `event => getMcpResources({ event, ... })`. ```typescript import { defineMcpResource, defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' const resource1 = defineMcpResource({ ... }) const resource2 = defineMcpResource({ ... }) export default defineMcpHandler({ name: 'custom', resources: [resource1, resource2], }) ``` ### `prompts` (optional) Same shapes as [`tools`](https://mcp-toolkit.nuxt.dev/#tools-optional): omit for folder-convention auto-attribution, pass an array, or pass `event => getMcpPrompts({ event, ... })`. ```typescript import { defineMcpPrompt, defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' const prompt1 = defineMcpPrompt({ ... }) const prompt2 = defineMcpPrompt({ ... }) export default defineMcpHandler({ name: 'custom', prompts: [prompt1, prompt2], }) ``` # Examples & routing ## Complete Example Here's a complete example of a custom handler: ```typescript [server/mcp/api-handler.ts] import { z } from 'zod' import { defineMcpTool, defineMcpResource, defineMcpPrompt, defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' const getUserTool = defineMcpTool({ name: 'get-user', description: 'Get user information', inputSchema: { userId: z.string(), }, handler: async ({ userId }) => { const user = await db.users.find(userId) return user }, }) const createUserTool = defineMcpTool({ name: 'create-user', description: 'Create a new user', inputSchema: { name: z.string(), email: z.string().email(), }, handler: async ({ name, email }) => { const user = await db.users.create({ name, email }) return `User created: ${user.id}` }, }) // Define resources for this handler const userResource = defineMcpResource({ name: 'user', uri: 'api://users/{id}', handler: async (uri, variables) => { const id = variables.id as string const user = await db.users.find(id) return { contents: [{ uri: uri.toString(), mimeType: 'application/json', text: JSON.stringify(user), }], } }, }) // Define prompts for this handler const userPrompt = defineMcpPrompt({ name: 'user-help', description: 'Get help with user operations', handler: async () => { return { messages: [{ role: 'user', content: { type: 'text', text: 'How can I manage users?', }, }], } }, }) // Export the handler export default defineMcpHandler({ name: 'api', version: '1.0.0', route: '/mcp/api', tools: [getUserTool, createUserTool], resources: [userResource], prompts: [userPrompt], browserRedirect: '/docs/api', }) ``` ## Multiple Handlers You can create multiple handlers in your application: ```text server/ └── mcp/ ├── migration.ts # Migration handler ├── api-handler.ts # API handler ├── admin-handler.ts # Admin handler ├── tools/ │ └── echo.ts # Default handler tools ├── resources/ │ └── readme.ts # Default handler resources └── prompts/ └── greeting.ts # Default handler prompts ``` Each handler file should export a default handler definition: ```typescript [server/mcp/migration.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'migration', tools: [ ... ], }) ``` ```typescript [server/mcp/api-handler.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'api', tools: [ ... ], }) ``` ## Handler Routes The handler's `name` determines where it will be mounted. By default, handlers are accessible at `/mcp/:name` where `:name` is the handler's name: - Handler with `name: 'migration'` → mounted at `/mcp/migration` - Handler with `name: 'api'` → mounted at `/mcp/api` - Handler with `name: 'admin'` → mounted at `/mcp/admin` You can also specify a custom route to override the default: ```typescript import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'api', route: '/api/mcp/v1', // Custom route instead of /mcp/api }) ``` ## Default vs Custom Handlers | Feature | Default Handler | Default Handler Override (`index.ts`) | Custom Handler | | --------- | ---------------------------- | ------------------------------------- | ------------------------------ | | Route | `/mcp` (via config) | `/mcp` (via config) | `/mcp/:name` (or custom route) | | Tools | From `server/mcp/tools/` | Custom or global | Defined in handler | | Resources | From `server/mcp/resources/` | Custom or global | Defined in handler | | Prompts | From `server/mcp/prompts/` | Custom or global | Defined in handler | | Name | From config | Custom or config | Handler name (required) | | Version | From config | Custom or config | Handler version | # Sharing & practices ## Use Cases ### 1. Feature Separation Separate different features into different handlers: ```typescript [server/mcp/user-management.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'users', tools: [getUserTool, createUserTool, updateUserTool], }) ``` ```typescript [server/mcp/content-management.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'content', tools: [createPostTool, updatePostTool, deletePostTool], }) ``` ### 2. Versioned APIs Create versioned handlers: ```typescript [server/mcp/api-v1.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'api-v1', version: '1.0.0', route: '/api/v1/mcp', tools: [ ... ], }) ``` ```typescript [server/mcp/api-v2.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'api-v2', version: '2.0.0', route: '/api/v2/mcp', tools: [ ... ], }) ``` ### 3. Domain-Specific Handlers Organize by domain: ```typescript [server/mcp/ecommerce.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'ecommerce', tools: [addToCartTool, checkoutTool, getProductsTool], }) ``` ```typescript [server/mcp/analytics.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'analytics', tools: [getStatsTool, generateReportTool], }) ``` ## Sharing Tools Between Handlers ::callout{color="primary" icon="i-lucide-arrow-right"} Prefer the [multi-handler organization](https://mcp-toolkit.nuxt.dev/handlers/organization) features (folder convention plus the `getMcp*` function form) for any new handler. The patterns below remain fully supported for explicit, hand-built handler configs. :: You can share tool definitions between handlers by exporting them from a separate file: ```typescript [server/mcp/shared-tools.ts] import { z } from 'zod' import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' export const sharedTool = defineMcpTool({ name: 'shared-tool', description: 'A shared tool', inputSchema: { input: z.string(), }, handler: async ({ input }) => `Shared: ${input}`, }) ``` ```typescript [server/mcp/handler1.ts] import { sharedTool } from './shared-tools' import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'handler1', tools: [sharedTool], }) ``` ```typescript [server/mcp/handler2.ts] import { sharedTool } from './shared-tools' import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({ name: 'handler2', tools: [sharedTool], }) ``` ## File Organization Two conventions can coexist in `server/mcp/`: ```text server/ └── mcp/ ├── index.ts # Default handler override (optional) ├── migration.ts # Top-level handler (defaults to all tools) ├── tools/ # Default handler tools (orphans) ├── resources/ # Default handler resources (orphans) ├── prompts/ # Default handler prompts (orphans) └── handlers/ # ✨ Named handler folders (recommended) ├── admin/ │ ├── index.ts # Required: defineMcpHandler({ ... }) │ ├── tools/ # Auto-attached to /mcp/admin │ └── prompts/ └── apps/ ├── index.ts └── tools/ # Auto-attached to /mcp/apps ``` ::callout{color="primary" icon="i-lucide-lightbulb"} The `index.ts` at the root of `server/mcp/` overrides the default handler configuration. Inside a `handlers//` directory, `index.ts` is required (even as a one-liner: `export default defineMcpHandler({})` ) — it's what registers the `/mcp/` route. :: ## Best Practices 1. **Use descriptive names**: Make handler names clear and specific 2. **Group related functionality**: Put related tools/resources together 3. **Version your handlers**: Use semantic versioning for handler versions 4. **Document your handlers**: Add comments explaining what each handler does 5. **Keep handlers focused**: Each handler should have a clear, single purpose ## Next Steps - [Multi-handler organization](https://mcp-toolkit.nuxt.dev/handlers/organization) - Folder convention + `getMcp*` function form - [Code Mode](https://mcp-toolkit.nuxt.dev/advanced/code-mode) - Orchestrate tools with LLM-generated JavaScript - [Middleware](https://mcp-toolkit.nuxt.dev/advanced/middleware) - Add authentication and logging - [Dynamic Definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) - Conditionally register definitions - [Configuration](https://mcp-toolkit.nuxt.dev/getting-started/configuration) - Configure the default handler - [Tools](https://mcp-toolkit.nuxt.dev/tools/overview) - Create tools for your handlers - [Examples](https://mcp-toolkit.nuxt.dev/examples/common-patterns) - See more handler examples # 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//`. Every file under `tools/`, `resources/`, or `prompts/` is auto-attached to that handler via `_meta.handler`. ```bash 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`. ::callout{color="primary" icon="i-lucide-lightbulb"} `index.ts` is required even if it's a one-liner: `export default defineMcpHandler({})` . It's what registers the `/mcp/` 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: ```typescript [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](https://mcp-toolkit.nuxt.dev/advanced/listing-definitions) for the full reference. ## Default handler strategy The default `/mcp` route obeys `mcp.defaultHandlerStrategy` in `nuxt.config.ts`: ::field-group :::field{name="`'orphans'`" type="default"} Only definitions **not attached** to any named handler are exposed. Each definition shows up in exactly one place. ::: :::field{name="`'all'`"} Every discovered definition is exposed (the pre-multi-handler behaviour). Useful when you want a "kitchen sink" route on top of specialized ones. ::: :: ```typescript [nuxt.config.ts] export default defineNuxtConfig({ mcp: { defaultHandlerStrategy: 'orphans', // (default) }, }) ``` ::callout{color="primary" icon="i-lucide-shield-check"} **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 file | Default behaviour for `tools | resources | prompts: undefined` | | ------------------------------------------------------ | -------------------------------------------------------------- | | `server/mcp/index.ts` (default route) | Obeys `defaultHandlerStrategy` | | `server/mcp/handlers//index.ts` (folder handler) | Definitions attributed to `` | | `server/mcp/.ts` (top-level handler) | Every discovered definition (back-compat) | | Any (array) | Used as-is | | Any (function `(event) => T[]`) | Called per request | ::callout{color="info" icon="i-lucide-info"} 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//`. 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 - [Listing definitions](https://mcp-toolkit.nuxt.dev/advanced/listing-definitions) — programmatic access to summaries and raw defs (`listMcp*` / `getMcp*`). - [Sharing & practices](https://mcp-toolkit.nuxt.dev/handlers/sharing-practices) — handler organization patterns and best practices. - [Dynamic definitions](https://mcp-toolkit.nuxt.dev/advanced/dynamic-definitions) — per-request `enabled()` guards on individual definitions. # Apps ## What are MCP Apps? MCP Apps are **interactive HTML widgets** returned by an MCP tool and rendered inline by compatible hosts. Instead of streaming back text, your tool ships a small UI that the user can read, scroll, filter, and click — connected back to your server through a typed message bridge. They follow the [MCP UI proposal (SEP-1865)](https://modelcontextprotocol.io){rel=""nofollow""}: a tool returns a `text/html;profile=mcp-app` resource referenced by `ui://`, the host loads it inside a sandboxed `iframe`, and the iframe talks back over `postMessage`. `@nuxtjs/mcp-toolkit` makes that authoring experience feel like writing a regular Nuxt page: - One **Vue SFC** per app in `app/mcp/`. - A `defineMcpApp` macro inside ` ``` That's it. The toolkit: 1. Detects `defineMcpApp` and **registers an MCP tool** named `color-picker` (from the filename). 2. Generates a **UI resource** at `ui://mcp-app/color-picker` exposing `text/html;profile=mcp-app`. 3. Bundles the SFC + assets into a single HTML file with [`vite-plugin-singlefile`](https://github.com/richardtallent/vite-plugin-singlefile){rel=""nofollow""}. 4. Wires the `handler`'s `structuredContent` into the iframe so the UI hydrates **without a second round-trip**. ## File Convention MCP Apps live in **`app/mcp/`** by default (not `server/mcp/`). Change the app-side directory with `mcp.appsDir` in `nuxt.config.ts`. They sit on the client side of Nuxt because they author Vue components — but the `handler` you declare runs server-side, just like a tool. ```bash app/ └── mcp/ ├── color-picker.vue # → tool: color-picker, resource: ui://mcp-app/color-picker └── admin/ └── audit-log.vue # → tool: audit-log ``` ::callout{color="info" icon="i-lucide-info"} Co-locate helpers next to the SFC (e.g. `format.ts` ) — the bundler inlines them. Keep data generation in `server/api/` and call it via `$fetch` from the handler. :: ### Auto-Generated Name & Title Like tools and resources, `name` and `title` are inferred from the filename: | File | Name | Title | | --------------------- | -------------- | -------------- | | `color-picker.vue` | `color-picker` | `Color Picker` | | `weather-card.vue` | `weather-card` | `Weather Card` | | `admin/audit-log.vue` | `audit-log` | `Audit Log` | Override either by passing `name` / `title` to `defineMcpApp`. ### Routing Apps to a Specific Handler By default, every app is attached to the implicit `apps` handler and only surfaces on `/mcp/apps`. Two ways to route an app to a different named handler: **1. Sub-folder convention** — the first sub-directory under `app/mcp/` becomes the handler attribution: ```bash app/ └── mcp/ ├── color-picker.vue # → /mcp/apps (default) ├── finder/ │ └── stay-finder.vue # → /mcp/finder └── checkout/ └── stay-checkout.vue # → /mcp/checkout ``` Pair each handler folder with `server/mcp/handlers//index.ts`: ```ts [server/mcp/handlers/finder/index.ts] import { defineMcpHandler } from '@nuxtjs/mcp-toolkit/server' export default defineMcpHandler({}) ``` **2. Explicit `attachTo` override** — overrides the sub-folder default if both are present: ```vue [app/mcp/stay-finder.vue] ``` The generated tool and resource carry `_meta.handler = 'finder'`, top-level `group = 'stays'`, and `tags = ['searchable']`. Filter on them with `getMcpTools({ handler: 'finder' })`, `getMcpTools({ tags: ['searchable'] })`, etc. ## `defineMcpApp` A macro — like `definePageMeta` — extracted at build time and **stripped from the browser bundle**. The fields it accepts: ```ts defineMcpApp({ name?: string // Override auto-derived name title?: string // Override auto-derived title description?: string // Shown to the LLM to help it pick this app inputSchema?: ZodRawShape // Validates tool input on the server handler?: (args, extra) => Result // Runs server-side; defaults to (args) => ({ structuredContent: args }) csp?: McpAppCsp | false // Tighten or disable iframe CSP attachTo?: string // Named MCP handler this app routes to (default: 'apps' or sub-folder) group?: string // Top-level group label (default: same as attachTo) tags?: string[] // Top-level tags forwarded to the generated tool _meta?: Record // Extra _meta fields surfaced to the host }) ``` ::callout{color="info" icon="i-lucide-info"} `attachTo` , `group` , and `tags` must be **literals** ( `'finder'` , `['a', 'b']` ) — the toolkit reads them statically at build time to route the generated tool and resource. A dynamic expression ( `attachTo: someVar` ) fails the build with a clear error. :: ### Server Handler The `handler` runs in your Nitro server, not in the iframe. It receives validated input and returns `structuredContent` that the UI hydrates from. **Treat it like a tool handler** — call APIs, query a database, hit `$fetch`: ```ts defineMcpApp({ description: 'Pick a colour and preview a 5-tone palette.', inputSchema: { base: z.string().describe('Hex colour to anchor the palette, e.g. #2563eb'), }, handler: async ({ base }) => { const swatches = await $fetch('/api/palette', { query: { base } }) return { structuredContent: { base, swatches } } }, }) ``` ::callout{color="primary" icon="i-lucide-zap"} Returning `structuredContent` from the handler **inlines the data into the HTML** as a ` ``` Type-only references are stripped from the browser bundle by esbuild — nothing has to resolve inside the iframe at runtime. # useMcpApp() bridge ## `useMcpApp()` The single client-side composable, **auto-imported into every MCP App SFC**. It returns everything the iframe needs to talk to the host: ```ts const { initialData, // Ref — snapshot of the handler payload at mount, never updated data, // Ref — hydrated from structuredContent, refreshed by callTool loading, // Ref — true until first payload arrives error, // Ref — bridge / transport / payload errors pending, // Ref — true while a callTool() is in flight hostContext, // Ref — theme, displayMode, locale, … callTool, // (name, params?) => Promise — re-invoke any MCP tool sendPrompt, // (prompt: string) => void — push a message into the chat openLink, // (url: string) => void — ask the host to open a URL } = useMcpApp() ``` Pass your payload type as the generic to get full inference downstream. ### `initialData` & `data` `initialData` is a **frozen snapshot** of the handler's `structuredContent` inlined at build time (or `window.openai.toolOutput` on ChatGPT). It never changes — use it for bootstrap values such as IDs or query params that later `callTool` results might overwrite in `data`. `data` is **already populated on first render** when the handler returns `structuredContent`, then refreshed by `callTool` or host `tool-result` pushes. `loading` starts as `true` and becomes `false` after the first payload arrives. Use `pending` for in-flight `callTool()` refreshes: ```vue ``` ```vue ``` ### `hostContext` The host hands the iframe a context object during the `ui/initialize` handshake. Use it to **adapt to dark mode, fullscreen, or a fixed iframe size**: ```ts interface HostContext { theme?: 'light' | 'dark' displayMode?: 'inline' | 'fullscreen' | 'pip' containerDimensions?: { width?: number, height?: number, maxWidth?: number, maxHeight?: number } locale?: string timeZone?: string platform?: 'web' | 'desktop' | 'mobile' } ``` ```vue ``` ::callout{color="info" icon="i-lucide-info"} `hostContext` is `null` on the very first paint and populates after the handshake (typically <50 ms). Always use a fallback in your template. :: ### `sendPrompt(prompt)` — Follow-Ups Push a message into the chat as if the user had typed it. The LLM then routes it like any other request — including invoking another MCP App: ```vue ``` The host receives the prompt as if the user had typed it. The LLM may reply, call another tool, or open a different MCP App in response — **app-to-app workflows** fall out of this primitive. ::callout{color="warning" icon="i-lucide-triangle-alert"} Follow-ups are best-effort. Hosts that implement `ui/message` forward the prompt cleanly. ChatGPT acknowledges the request but doesn't always re-render the next tool inline (an upstream limitation). :: ### `callTool(name, params)` — In-Place Refresh Re-invoke any MCP tool from the iframe. The result replaces `data` automatically: ```vue ``` Use this for filters, pagination, refresh buttons — anything that changes the query without a full chat round-trip. ### `openLink(url)` Sandbox iframes can't open windows. `openLink` asks the host to do it for you (e.g. open a booking confirmation in a new browser tab): ```vue ``` ::callout{color="info" icon="i-lucide-info"} Add the target host to `csp.connectDomains` if you also need to `fetch()` it from the iframe. :: # CSP & build pipeline ## CSP & Resource Allow-Lists The toolkit injects a **conservative Content Security Policy** into every app HTML. By default the iframe can: - Run only its own inline script. - Render images and styles only from the same response. - Talk over `postMessage` to its parent host. If your UI needs external assets or APIs, allow them explicitly: ```vue [app/mcp/color-picker.vue] ``` The CSP is mirrored into `_meta.ui.csp` (and `openai/widgetCSP` for ChatGPT) so hosts that enforce CSP at the iframe level pick up the same rules. ::callout{color="warning" icon="i-lucide-shield"} Pass `csp: false` only as a last resort — and only if you fully control the assets the iframe loads. The default policy is what makes apps safe to render across hosts. :: | Field | What it allows | | ----------------- | ------------------------------------------------------------- | | `resourceDomains` | ``, `