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.

Prompt
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:

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:

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:

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

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'

export default defineMcpTool({
  name: 'echo',
  description: 'Echo back a message',
  handler: async () => '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'

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'

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

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

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')
  },
})
The 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).

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

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'

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.

Use createError() from H3 for errors with status codes:

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:

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:

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) or a number in milliseconds:

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:

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

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 API Integration

Here's an example showing a typical API-backed tool:

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 when adopted.

Explicit Group and Tags

Set group and tags directly on the definition:

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:

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.

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

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.

See the Dynamic Definitions guide for detailed documentation on auth-based filtering.

Next Steps

Copyright © 2026