Tools are functions that AI assistants can call to perform actions or retrieve information. They accept validated input parameters and return structured results.
Here's a simple tool that echoes back a message:
import { z } from 'zod'
export default defineMcpTool({
name: 'echo',
description: 'Echo back a message',
inputSchema: {
message: z.string().describe('The message to echo back'),
},
handler: async ({ message }) => {
return {
content: [{
type: 'text',
text: `Echo: ${message}`,
}],
}
},
})
You can omit name and title - they will be automatically generated from the filename:
import { z } from 'zod'
export default defineMcpTool({
// name and title are auto-generated from filename:
// name: 'list-documentation'
// title: 'List Documentation'
description: 'List all documentation files',
inputSchema: {},
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.
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) => { // Handler function
return { content: [...] }
},
})
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: { ... }, // Tool annotations
handler: async (args) => { ... },
})
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:
export default defineMcpTool({
name: 'echo',
description: 'Echo back a message',
handler: async () => {
return {
content: [{
type: 'text',
text: 'Echo: test',
}],
}
},
})
For tools with parameters, define them using Zod schemas:
import { z } from 'zod'
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
},
})
| 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 |
Define structured output using outputSchema:
import { z } from 'zod'
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 {
content: [{
type: 'text',
text: `BMI: ${bmi.toFixed(2)} (${category})`,
}],
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.
The handler is an async function that receives validated input and returns results:
handler: async (args, extra) => {
// args: Validated input matching inputSchema
// extra: Request handler extra information
return {
content: [{
type: 'text',
text: 'Result text',
}],
structuredContent: { ... }, // Optional structured output
}
}
Tools can return different content types:
return {
content: [{
type: 'text',
text: 'Hello, world!',
}],
}
return {
content: [{
type: 'image',
data: base64ImageData,
mimeType: 'image/png',
}],
}
return {
content: [{
type: 'resource',
resource: {
uri: 'file:///path/to/file',
text: 'File content',
mimeType: 'text/plain',
},
}],
}
To simplify creating tool responses, the module provides auto-imported helper functions:
// Simple text response
export default defineMcpTool({
description: 'Echo a message',
inputSchema: { message: z.string() },
handler: async ({ message }) => textResult(`Echo: ${message}`),
})
// JSON response (auto-stringified)
export default defineMcpTool({
description: 'Get user data',
inputSchema: { id: z.string() },
handler: async ({ id }) => {
const user = await getUser(id)
return jsonResult(user)
// or jsonResult(user, false) for compact JSON
},
})
// Error response
export default defineMcpTool({
description: 'Find resource',
inputSchema: { id: z.string() },
handler: async ({ id }) => {
const resource = await findResource(id)
if (!resource) return errorResult('Resource not found')
return jsonResult(resource)
},
})
// Image response (base64)
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')
},
})
| Helper | Description | Parameters |
|---|---|---|
textResult(text) | Simple text response | text: string |
jsonResult(data, pretty?) | JSON response (auto-stringify) | data: unknown, pretty?: boolean (default: true) |
errorResult(message) | Error response with isError: true | message: string |
imageResult(data, mimeType) | Base64 image response | data: string, mimeType: string |
You can provide metadata and behavioral hints to clients using the annotations property. These annotations help the AI assistant understand how to use your tool appropriately.
import { z } from 'zod'
export default defineMcpTool({
name: 'delete-user',
description: 'Delete a user account',
inputSchema: {
userId: z.string(),
},
annotations: {
// Human-readable title displayed in UIs
// Note: Can also be defined at the root level or auto-generated
title: 'Delete User',
// Behavioral hints
priority: 0.8, // Value between 0 and 1 (default: 0.5)
audience: ['user', 'assistant'], // Who should see/use this tool
// Tool capabilities
readOnlyHint: false, // Tool modifies state
destructiveHint: true, // Tool performs destructive updates
idempotentHint: false, // Repeated calls may have different effects
openWorldHint: false, // Tool does not interact with external world
},
handler: async ({ userId }) => {
// ...
},
})
The
titleannotation is optional. If not provided, it defaults to thetitleproperty from the tool definition, which itself is automatically generated from the filename if not specified.
| Annotation | Type | Description |
|---|---|---|
priority | number | A value between 0 and 1 indicating the relevance/priority of the tool. Default is 0.5. |
audience | string[] | Who the tool is intended for. Can contain 'user', 'assistant', or both. |
readOnlyHint | boolean | Whether the tool reads data without modifying it (safe to retry/speculate). |
destructiveHint | boolean | Whether the tool performs destructive updates (should be used with caution). |
idempotentHint | boolean | Whether calling the tool multiple times with the same arguments produces the same result. |
openWorldHint | boolean | Whether the tool interacts with external entities (APIs, databases, etc.). |
Handle errors gracefully in your handlers:
import { z } from 'zod'
export default defineMcpTool({
name: 'safe-divide',
inputSchema: {
a: z.number(),
b: z.number(),
},
handler: async ({ a, b }) => {
if (b === 0) {
return {
content: [{
type: 'text',
text: 'Error: Division by zero',
}],
isError: true,
}
}
const result = a / b
return {
content: [{
type: 'text',
text: `Result: ${result}`,
}],
}
},
})
You can cache tool responses using Nitro's caching system. The cache option accepts three formats:
Use a string duration (parsed by ms) or a number in milliseconds:
import { z } from 'zod'
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 }) => {
const data = await fetchExpensiveData(id)
return {
content: [{ type: 'text', text: JSON.stringify(data) }],
}
},
})
For more control, use an object with all Nitro cache options:
import { z } from 'zod'
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 }) => {
// ...
},
})
| 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') |
Here's an example showing proper error handling:
import { z } from 'zod'
export default defineMcpTool({
name: 'safe-operation',
description: 'Perform an operation with error handling',
inputSchema: {
value: z.string().describe('Input value'),
},
handler: async ({ value }) => {
try {
// Your operation here
const result = value.toUpperCase()
return {
content: [{
type: 'text',
text: `Result: ${result}`,
}],
}
}
catch (error) {
return {
content: [{
type: 'text',
text: `Error: ${error instanceof Error ? error.message : String(error)}`,
}],
isError: true,
}
}
},
})
Organize your tools in the server/mcp/tools/ directory:
server/
└── mcp/
└── tools/
├── echo.ts
├── calculator.ts
├── bmi.ts
└── text-processor.ts
Each file should export a default tool definition.
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
},
}