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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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:

import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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:

import { defineMcpTool, textResult } from '@nuxtjs/mcp-toolkit/server' // optional

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

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).

server/mcp/tools/delete-user.ts
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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

AnnotationTypeDefaultDescription
readOnlyHintbooleanfalseIf true, the tool only reads data without modifying any state (safe to retry).
destructiveHintbooleantrueIf true, the tool may perform destructive operations like deleting data. Only meaningful when readOnlyHint is false.
idempotentHintbooleanfalseIf true, calling the tool multiple times with the same arguments has no additional effect beyond the first call. Only meaningful when readOnlyHint is false.
openWorldHintbooleantrueIf 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,
}
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.

server/mcp/tools/create-todo.ts
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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

Handle errors gracefully in your handlers:

server/mcp/tools/safe-divide.ts
import { z } from 'zod'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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'
import { defineMcpTool } from '@nuxtjs/mcp-toolkit/server' // optional

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