Skip to main content
Tools are the primary way MCP clients interact with your server. They represent functions that can be invoked with parameters and return results. This guide covers everything you need to know about creating powerful and reliable tools.
Response Helpers: Throughout this guide, you’ll see code examples using response helpers like text(), object(), and mix(). These utilities simplify creating tool responses with proper typing and metadata. See Response Helpers for a complete reference.

Understanding Tools

Tools in MCP are:
  • Invocable Functions: Clients can call them with parameters
  • Typed: Parameters and returns have defined types
  • Async: All tool handlers can be asynchronous

Basic Tool Structure

Every tool has three main components:
import { z } from 'zod';

server.tool({
  name: 'tool_name',           // Unique identifier
  description: 'What it does',  // Clear description for clients
  schema: z.object({...}),      // Zod schema for input validation
}, async (params) => {...})    // Handler function

Input Validation with Zod

Tools use Zod schemas for input validation. The server automatically validates inputs before calling your handler, so you can trust that the parameters match your schema.

Basic Input Types

import { z } from 'zod';

server.tool({
  name: 'process_message',
  description: 'Process a message with various options',
  schema: z.object({
    // Required string
    message: z.string().describe('The message to process'),
    
    // Optional number with default
    count: z.number().default(10).describe('Number of items'),
    
    // Optional boolean with default
    verbose: z.boolean().default(false).describe('Enable verbose output'),
    
    // Optional object
    config: z.object({
      setting1: z.string(),
      setting2: z.number()
    }).optional().describe('Configuration object'),
    
    // Array of strings
    items: z.array(z.string()).describe('List of items to process')
  })
}, async ({ message, count, verbose, config, items }) => {
  // message, count, verbose, config, items are fully typed and validated
  // count will be 10 if not provided
  // verbose will be false if not provided
  return text('Results...')
})

Tool Callbacks

Basic Response

The simplest tool response is text. Use the text() helper for clean, readable code:
import { text } from 'mcp-use/server';

async (params) => {
  return text('Response text here');
}

Multiple Content Items

Tools can return multiple content items using the mix() helper:
import { mix, text, resource, object } from 'mcp-use/server';

cb: async ({ data }) => {
  return mix(
    text('Analysis complete:'),
    text(`Found ${data.length} items`),
    resource('results://latest', object({ items: data }))
  );
}

Tool Annotations

Add metadata to tools for better client integration:
server.tool({
  name: 'important_operation',
  description: 'Performs an important operation',
  schema: z.object({ ... }),
  annotations: {
    requiresAuth: true,
    rateLimit: '10/minute',
    deprecated: false
  }
}, async (params) => {
  // Tool implementation
})

Returning Widgets from Tools

Tools can return interactive UI widgets using the widget() helper with the widget config option. This automatically configures the tool to render widgets in the Inspector and ChatGPT.
You must include the widget: { name, ... } config in your tool definition when returning widgets. This sets up all the registration-time metadata needed for proper widget rendering.
import { widget, text } from 'mcp-use/server';
import { z } from 'zod';

server.tool({
  name: 'get-weather',
  description: 'Get current weather for a city',
  schema: z.object({
    city: z.string().describe('City name')
  }),
  // Widget config sets all registration-time metadata
  widget: {
    name: 'weather-display',  // Must match a widget in resources/
    invoking: 'Fetching weather...',
    invoked: 'Weather loaded'
  }
}, async ({ city }) => {
  // Fetch weather data
  const weatherData = await fetchWeather(city);
  
  // Return widget with runtime data only
  return widget({
    props: weatherData,
    output: text(`Weather in ${city}: ${weatherData.temp}°C`),
    message: `Current weather in ${city}`
  });
});
How it works:
  • widget: { name, invoking, invoked, ... } on tool definition - Configures all widget metadata at registration time
  • widget({ props, output, message }) helper - Returns runtime data only:
    • props - Widget-only data passed to useWidget().props (hidden from model)
    • output - Optional response helper (text(), object(), etc.) that the model sees
    • message - Optional text message override
  • The widget must exist in your resources/ folder as a .tsx file or folder
See UI Widgets for complete widget creation and registration documentation.

OpenAI Apps SDK Integration

For ChatGPT and OpenAI compatible clients:
Recommended Approach: The recommended way to create OpenAI Apps SDK widgets is to use the widget helper with returnsWidget option or automatic widget mounting, which automatically registers widgets from the resources/ folder as tools. The manual approach below is shown for reference.
server.tool({
  name: 'show_chart',
  description: 'Display a chart',
  schema: z.object({
    data: z.array(z.any()).describe('The chart data')
  }),
  _meta: {
    'openai/outputTemplate': 'ui://widgets/chart',
    'openai/toolInvocation/invoking': 'Generating chart...',
    'openai/toolInvocation/invoked': 'Chart generated',
    'openai/widgetAccessible': true
  }
}, async ({ data }) => {
  return {
    _meta: {
      'openai/outputTemplate': 'ui://widgets/chart'
    },
    content: [{
      type: 'text',
      text: 'Chart displayed'
    }],
    structuredContent: { data }
  }
})

Error Handling with error()

The error() helper provides a standardized way to return error responses from tools. It sets the isError flag to true, allowing clients to distinguish between successful results and error conditions. This ensures consistent error handling across your MCP server. The error() helper creates a properly formatted error response with:
  • isError: true flag to indicate failure
  • Text content with your error message
  • Proper MIME type metadata
import { object, error } from 'mcp-use/server';

server.tool({
  name: 'external_api',
  description: 'Call external API',
  schema: z.object({
    endpoint: z.string().url().describe('The API endpoint URL')
  })
}, async ({ endpoint }) => {
  try {
    const data = await callExternalAPI(endpoint);
    return object(data);
  } catch (err) {
    // Use error() helper to signal failure to the client
    return error(
      `Unable to fetch data from ${endpoint}.\n` +
      `Error: ${err.message}\n` +
      `Please check the endpoint and try again.`
    );
  }
})

Logging from Tools

Tools can send log messages to clients during execution using ctx.log(). This is useful for reporting progress, debugging tool behavior, and providing real-time feedback during long-running operations.
server.tool({
  name: 'process_files',
  description: 'Process multiple files',
  schema: z.object({
    files: z.array(z.string())
  })
}, async ({ files }, ctx) => {
  await ctx.log('info', 'Starting file processing');
  
  for (const file of files) {
    await ctx.log('debug', `Processing ${file}`);
    // ... process file ...
  }
  
  await ctx.log('info', 'Processing completed');
  return text(`Processed ${files.length} files`);
})
The ctx.log() function accepts a log level ('debug', 'info', 'notice', 'warning', 'error', 'critical', 'alert', 'emergency'), a message string, and an optional logger name. See Server Logging for complete documentation on log levels and best practices.

Notifying Clients of Tool Changes

When dynamically adding or removing tools, notify clients to refresh their tools cache:
// Register a new tool dynamically
server.tool({
  name: 'new_tool',
  description: 'A dynamically added tool',
  schema: z.object({ input: z.string() })
}, async ({ input }) => text(`Processed: ${input}`));

// Notify all connected clients
await server.sendToolsListChanged();
See Notifications for more details.

Testing

Use the built-in inspector for interactive testing:
  1. Start your server with the inspector
  2. Navigate to http://localhost:3000/inspector
  3. Select your tool from the list
  4. Enter parameter values
  5. Execute and view results

Next Steps