Core Concepts

Tools

Create MCP tools with Zod validation and type safety.

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.

Basic Tool Definition

Here's a simple tool that echoes back a message:

server/mcp/tools/echo.ts
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}`,
      }],
    }
  },
})

Auto-Generated Name and Title

You can omit name and title - they will be automatically generated from the filename:

server/mcp/tools/list-documentation.ts
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.

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) => { // Handler function
    return { content: [...] }
  },
})

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:

server/mcp/tools/echo.ts
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:

server/mcp/tools/calculator.ts
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
  },
})

Common Zod Types

Zod TypeExampleDescription
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:

server/mcp/tools/bmi.ts
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.

Handler Function

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
  }
}

Content Types

Tools can return different content types:

return {
  content: [{
    type: 'text',
    text: 'Hello, world!',
  }],
}

Result Helpers

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}`),
})
HelperDescriptionParameters
textResult(text)Simple text responsetext: string
jsonResult(data, pretty?)JSON response (auto-stringify)data: unknown, pretty?: boolean (default: true)
errorResult(message)Error response with isError: truemessage: string
imageResult(data, mimeType)Base64 image responsedata: string, mimeType: string

Tool Annotations

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.

server/mcp/tools/delete-user.ts
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 title annotation is optional. If not provided, it defaults to the title property from the tool definition, which itself is automatically generated from the filename if not specified.

Common Annotations

AnnotationTypeDescription
prioritynumberA value between 0 and 1 indicating the relevance/priority of the tool. Default is 0.5.
audiencestring[]Who the tool is intended for. Can contain 'user', 'assistant', or both.
readOnlyHintbooleanWhether the tool reads data without modifying it (safe to retry/speculate).
destructiveHintbooleanWhether the tool performs destructive updates (should be used with caution).
idempotentHintbooleanWhether calling the tool multiple times with the same arguments produces the same result.
openWorldHintbooleanWhether the tool interacts with external entities (APIs, databases, etc.).
These hints are advisory, helping LLMs make better decisions about tool usage without enforcing specific behaviors.

Error Handling

Handle errors gracefully in your handlers:

server/mcp/tools/safe-divide.ts
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}`,
      }],
    }
  },
})

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:

server/mcp/tools/cached-data.ts
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) }],
    }
  },
})

Full Cache Options

For more control, use an object with all Nitro cache options:

server/mcp/tools/cached-pages.ts
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 }) => {
    // ...
  },
})

Cache Options Reference

OptionTypeRequiredDescription
maxAgestring | numberYesCache duration (e.g., '1h', 3600000)
getKey(args) => stringNoCustom cache key generator
staleMaxAgenumberNoDuration for stale-while-revalidate
swrbooleanNoEnable stale-while-revalidate
namestringNoCache name (auto-generated from tool name)
groupstringNoCache group (default: 'mcp')
See the Nitro Cache documentation for all available options.

Advanced Examples

Tool with Error Handling

Here's an example showing proper error handling:

server/mcp/tools/safe-operation.ts
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,
      }
    }
  },
})

File Organization

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.

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
  },
}

Next Steps