Skip to main content

Tools

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.

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 are asynchronous
  • Stateless: Each invocation is independent

Basic Tool Structure

Every tool has three main components:
server.tool({
  name: 'tool_name',           // Unique identifier
  description: 'What it does',  // Clear description for clients
  inputs: [...],                // Parameter definitions
  cb: async (params) => {...}  // Handler function
})

Defining Tool Inputs

Input Types

Tools support five parameter types:
// String input
{
  name: 'message',
  type: 'string',
  description: 'The message to process',
  required: true
}

// Number input
{
  name: 'count',
  type: 'number',
  description: 'Number of items',
  required: false,
  default: 10
}

// Boolean input
{
  name: 'verbose',
  type: 'boolean',
  description: 'Enable verbose output',
  required: false,
  default: false
}

// Object input
{
  name: 'config',
  type: 'object',
  description: 'Configuration object',
  required: false
}

// Array input
{
  name: 'items',
  type: 'array',
  description: 'List of items to process',
  required: true
}

Optional Parameters and Defaults

server.tool({
  name: 'search',
  description: 'Search for items',
  inputs: [
    {
      name: 'query',
      type: 'string',
      required: true  // Required parameter
    },
    {
      name: 'limit',
      type: 'number',
      required: false,  // Optional
      default: 10       // Default value if not provided
    },
    {
      name: 'sort',
      type: 'string',
      required: false   // Optional with no default
    }
  ],
  cb: async ({ query, limit = 10, sort }) => {
    // limit will be 10 if not provided
    // sort will be undefined if not provided
    return { content: [{ type: 'text', text: 'Results...' }] }
  }
})

Tool Callbacks

Basic Response

The simplest tool response is text:
cb: async (params) => {
  return {
    content: [{
      type: 'text',
      text: 'Response text here'
    }]
  }
}

Multiple Content Items

Tools can return multiple content items:
cb: async ({ data }) => {
  return {
    content: [
      {
        type: 'text',
        text: 'Analysis complete:'
      },
      {
        type: 'text',
        text: `Found ${data.length} items`
      },
      {
        type: 'resource',
        uri: 'results://latest',
        mimeType: 'application/json'
      }
    ]
  }
}

Including Resources

Tools can reference resources:
cb: async ({ filename }) => {
  // Process file and create resource
  const resourceUri = `file://${filename}`

  return {
    content: [
      {
        type: 'text',
        text: `Processed ${filename}`
      },
      {
        type: 'resource',
        uri: resourceUri,
        mimeType: 'text/plain'
      }
    ]
  }
}

Advanced Tool Patterns

Input Validation

Always validate inputs in your tool handlers:
server.tool({
  name: 'divide',
  description: 'Divide two numbers',
  inputs: [
    { name: 'dividend', type: 'number', required: true },
    { name: 'divisor', type: 'number', required: true }
  ],
  cb: async ({ dividend, divisor }) => {
    // Validation
    if (divisor === 0) {
      return {
        content: [{
          type: 'text',
          text: 'Error: Division by zero is not allowed'
        }]
      }
    }

    const result = dividend / divisor
    return {
      content: [{
        type: 'text',
        text: `Result: ${result}`
      }]
    }
  }
})

Async Operations

Tools naturally support async operations:
server.tool({
  name: 'fetch_data',
  description: 'Fetch data from external API',
  inputs: [
    { name: 'endpoint', type: 'string', required: true }
  ],
  cb: async ({ endpoint }) => {
    try {
      const response = await fetch(endpoint)
      const data = await response.json()

      return {
        content: [{
          type: 'text',
          text: JSON.stringify(data, null, 2)
        }]
      }
    } catch (error) {
      return {
        content: [{
          type: 'text',
          text: `Error fetching data: ${error.message}`
        }]
      }
    }
  }
})

Progress Reporting

For long-running operations, provide status updates:
server.tool({
  name: 'process_batch',
  description: 'Process a batch of items',
  inputs: [
    { name: 'items', type: 'array', required: true }
  ],
  cb: async ({ items }) => {
    const results = []
    const messages = []

    for (let i = 0; i < items.length; i++) {
      // Process each item
      const result = await processItem(items[i])
      results.push(result)

      // Create progress message
      messages.push(`Processed ${i + 1}/${items.length}: ${items[i]}`)
    }

    return {
      content: [
        {
          type: 'text',
          text: messages.join('\n')
        },
        {
          type: 'text',
          text: `\nCompleted processing ${items.length} items`
        }
      ]
    }
  }
})

Tool Composition

Tools can work together:
// Data fetching tool
server.tool({
  name: 'fetch_user',
  description: 'Fetch user data',
  inputs: [
    { name: 'userId', type: 'string', required: true }
  ],
  cb: async ({ userId }) => {
    const user = await getUserFromDatabase(userId)
    return {
      content: [{
        type: 'text',
        text: JSON.stringify(user)
      }]
    }
  }
})

// Analysis tool that uses fetched data
server.tool({
  name: 'analyze_user',
  description: 'Analyze user activity',
  inputs: [
    { name: 'userId', type: 'string', required: true }
  ],
  cb: async ({ userId }) => {
    // Could call fetch_user first or expect client to provide data
    const userData = await getUserFromDatabase(userId)
    const analysis = analyzeUserActivity(userData)

    return {
      content: [{
        type: 'text',
        text: `User ${userId} analysis:\n${analysis}`
      }]
    }
  }
})

Tool Annotations

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

OpenAI Apps SDK Integration

For ChatGPT and OpenAI compatible clients:
server.tool({
  name: 'show_chart',
  description: 'Display a chart',
  inputs: [
    { name: 'data', type: 'array', required: true }
  ],
  _meta: {
    'openai/outputTemplate': 'ui://widgets/chart',
    'openai/toolInvocation/invoking': 'Generating chart...',
    'openai/toolInvocation/invoked': 'Chart generated',
    'openai/widgetAccessible': true
  },
  cb: async ({ data }) => {
    return {
      _meta: {
        'openai/outputTemplate': 'ui://widgets/chart'
      },
      content: [{
        type: 'text',
        text: 'Chart displayed'
      }],
      structuredContent: { data }
    }
  }
})

Error Handling Best Practices

Graceful Degradation

server.tool({
  name: 'external_api',
  description: 'Call external API',
  inputs: [
    { name: 'endpoint', type: 'string', required: true }
  ],
  cb: async ({ endpoint }) => {
    try {
      const data = await callExternalAPI(endpoint)
      return {
        content: [{
          type: 'text',
          text: JSON.stringify(data)
        }]
      }
    } catch (error) {
      // Provide useful error information
      return {
        content: [{
          type: 'text',
          text: `Unable to fetch data from ${endpoint}.\n` +
                `Error: ${error.message}\n` +
                `Please check the endpoint and try again.`
        }]
      }
    }
  }
})

Input Sanitization

server.tool({
  name: 'execute_command',
  description: 'Execute a safe command',
  inputs: [
    { name: 'command', type: 'string', required: true }
  ],
  cb: async ({ command }) => {
    // Sanitize input
    const allowedCommands = ['list', 'status', 'info']
    const sanitizedCommand = command.toLowerCase().trim()

    if (!allowedCommands.includes(sanitizedCommand)) {
      return {
        content: [{
          type: 'text',
          text: `Invalid command. Allowed commands: ${allowedCommands.join(', ')}`
        }]
      }
    }

    // Execute safe command
    const result = await executeSafeCommand(sanitizedCommand)
    return {
      content: [{
        type: 'text',
        text: result
      }]
    }
  }
})

Testing Tools

Unit Testing Example

import { describe, it, expect } from 'vitest'
import { createMCPServer } from 'mcp-use/server'

describe('Calculator Tool', () => {
  const server = createMCPServer('test-server')

  server.tool({
    name: 'add',
    description: 'Add two numbers',
    inputs: [
      { name: 'a', type: 'number', required: true },
      { name: 'b', type: 'number', required: true }
    ],
    cb: async ({ a, b }) => ({
      content: [{
        type: 'text',
        text: `${a + b}`
      }]
    })
  })

  it('should add two numbers correctly', async () => {
    const tool = server.getTool('add') // Note: This is pseudocode
    const result = await tool.cb({ a: 5, b: 3 })

    expect(result.content[0].text).toBe('8')
  })
})

Manual Testing with Inspector

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

Performance Optimization

Caching Results

const cache = new Map()

server.tool({
  name: 'expensive_operation',
  description: 'Performs expensive computation',
  inputs: [
    { name: 'input', type: 'string', required: true }
  ],
  cb: async ({ input }) => {
    // Check cache
    const cacheKey = `expensive:${input}`
    if (cache.has(cacheKey)) {
      return {
        content: [{
          type: 'text',
          text: cache.get(cacheKey)
        }]
      }
    }

    // Perform expensive operation
    const result = await performExpensiveOperation(input)

    // Cache result
    cache.set(cacheKey, result)

    return {
      content: [{
        type: 'text',
        text: result
      }]
    }
  }
})

Streaming Large Results

For large datasets, consider pagination:
server.tool({
  name: 'list_items',
  description: 'List items with pagination',
  inputs: [
    { name: 'page', type: 'number', required: false, default: 1 },
    { name: 'pageSize', type: 'number', required: false, default: 20 }
  ],
  cb: async ({ page = 1, pageSize = 20 }) => {
    const offset = (page - 1) * pageSize
    const items = await getItems(offset, pageSize)
    const total = await getTotalCount()
    const totalPages = Math.ceil(total / pageSize)

    return {
      content: [{
        type: 'text',
        text: `Page ${page} of ${totalPages} (${total} total items):\n` +
              items.map(item => `- ${item.name}`).join('\n')
      }]
    }
  }
})

Best Practices

  1. Clear Naming: Use descriptive, action-oriented names
  2. Comprehensive Descriptions: Explain what the tool does and how to use it
  3. Input Validation: Always validate and sanitize inputs
  4. Error Handling: Provide helpful error messages
  5. Idempotency: Make tools idempotent when possible
  6. Documentation: Document complex tools with examples
  7. Testing: Write tests for critical tools
  8. Performance: Consider caching and pagination for expensive operations

Next Steps