# 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]
{{ s.name }}
```
## 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
[](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
[](https://your-app.com/mcp/deeplink?ide=vscode)
```
### Both IDEs
```md
[](https://your-app.com/mcp/deeplink)
[](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
[](https://your-app.com/mcp/deeplink)
```
**Custom colors:**
```md
[](https://your-app.com/mcp/deeplink)
```
**Without icon:**
```md
[](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 `
Mixing colours…
```
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
{{ data.swatches.length }} swatches from {{ data.base }}
```
### `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` | ``, `