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.
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: https://mcp-toolkit.nuxt.dev/core-concepts/tools
Basic Tool Definition
Here's a simple tool that echoes back a message:
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:
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:
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
},
})
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) => { ... },
})
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:
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:
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:
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:
handler: async ({ name }) => `Hello ${name}`
// → { content: [{ type: 'text', text: 'Hello World' }] }
handler: async ({ a, b }) => a + b
// → { content: [{ type: 'text', text: '10' }] }
handler: async ({ id }) => {
const user = await getUser(id)
return user
}
// → { content: [{ type: 'text', text: '{ "id": ... }' }] }
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:
return {
content: [{
type: 'image',
data: base64ImageData,
mimeType: 'image/png',
}],
}
return {
structuredContent: {
bmi: 25.5,
category: 'Normal',
},
}
// text content is auto-generated as fallback for older clients
return {
content: [{
type: 'resource',
resource: {
uri: 'file:///path/to/file',
text: 'File content',
mimeType: 'text/plain',
},
}],
}
Result Helpers
The module provides the imageResult helper for image responses:
import { z } from 'zod'
import { defineMcpTool, imageResult } 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')
},
})
textResult, jsonResult, and errorResult helpers are deprecated. Return values directly from your handler instead, and throw errors for error cases (see Error Handling).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).
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:
// Search, list, lookup, calculate...
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
}
// Creates a new record each time
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false,
}
// Updates are idempotent (same input → same result)
annotations: {
readOnlyHint: false,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
}
// Destructive and idempotent (deleting twice is the same)
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
}
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.
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 }) => {
// ...
},
})
inputExamples are particularly useful for tools with optional parameters, enums, or complex nested inputs where showing concrete values helps models pick the right format.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:
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:
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:
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) or a number in milliseconds:
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:
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 |
name | string | No | Cache name (auto-generated from tool name) |
group | string | No | Cache group (default: 'mcp') |
Advanced Examples
Tool with API Integration
Here's an example showing a typical API-backed tool:
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 when adopted.
Explicit Group and Tags
Set group and tags directly on the definition:
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:
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:
{
"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.
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 is adopted by the MCP SDK.File Organization
Organize your tools in the server/mcp/tools/ directory. Both flat and nested layouts are supported:
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:
// 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:
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.