The Model Context Protocol (MCP) is becoming the standard way for AI assistants to interact with external tools. But how do tools actually work under the hood? Let's dig in.

The Basics: What Is a Tool?

In MCP, a tool is a function that an AI can call. It has:

  1. A name — unique identifier (e.g., read_file, search_web)
  2. A description — tells the AI when to use it
  3. An input schema — JSON Schema defining what parameters it accepts
  4. An output — what the tool returns

Here's a minimal tool definition:

{
  name: "get_weather",
  description: "Get current weather for a location",
  inputSchema: {
    type: "object",
    properties: {
      location: {
        type: "string",
        description: "City name or coordinates"
      }
    },
    required: ["location"]
  }
}

How Discovery Works

When an MCP client connects to a server, it calls tools/list to discover available tools. The server responds with an array of tool definitions:

{
  "tools": [
    {
      "name": "read_file",
      "description": "Read contents of a file",
      "inputSchema": { ... }
    },
    {
      "name": "write_file", 
      "description": "Write content to a file",
      "inputSchema": { ... }
    }
  ]
}

The client (usually an AI assistant) uses these definitions to:

  1. Know what tools exist
  2. Understand when each tool is appropriate
  3. Validate arguments before calling

Input Schema: The Contract

The inputSchema uses JSON Schema to define valid inputs. This is crucial because:

  • Type safety — Ensures the AI passes correct types
  • Validation — Server can reject malformed requests
  • Documentation — Schema IS the documentation

Common Patterns

Required vs optional parameters:

{
  "type": "object",
  "properties": {
    "path": { "type": "string" },
    "encoding": { "type": "string", "default": "utf-8" }
  },
  "required": ["path"]
}

Enums for constrained values:

{
  "type": "object",
  "properties": {
    "method": {
      "type": "string",
      "enum": ["GET", "POST", "PUT", "DELETE"]
    }
  }
}

Arrays for multiple items:

{
  "type": "object",
  "properties": {
    "files": {
      "type": "array",
      "items": { "type": "string" }
    }
  }
}

Tool Execution Flow

When the AI decides to use a tool:

  1. Client sends tools/call with tool name and arguments
  2. Server validates arguments against inputSchema
  3. Server executes the tool logic
  4. Server returns content (text, images, or structured data)
// Request
{
  "method": "tools/call",
  "params": {
    "name": "read_file",
    "arguments": {
      "path": "/etc/hosts"
    }
  }
}
 
// Response
{
  "content": [
    {
      "type": "text",
      "text": "127.0.0.1 localhost\n..."
    }
  ]
}

Real-World Example: Filesystem Server

Let's look at how the official MCP filesystem server defines its tools:

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "read_file",
      description: "Read the complete contents of a file",
      inputSchema: {
        type: "object",
        properties: {
          path: { type: "string", description: "Path to the file" }
        },
        required: ["path"]
      }
    },
    {
      name: "write_file",
      description: "Create or overwrite a file",
      inputSchema: {
        type: "object",
        properties: {
          path: { type: "string" },
          content: { type: "string" }
        },
        required: ["path", "content"]
      }
    },
    {
      name: "list_directory",
      description: "List contents of a directory",
      inputSchema: {
        type: "object",
        properties: {
          path: { type: "string" }
        },
        required: ["path"]
      }
    }
  ]
}));

Notice how each tool has a clear, single responsibility. Good tool design follows the same principles as good API design.

Tool Annotations (New in 2025)

The latest MCP spec adds tool annotations — hints that help clients understand tool behavior:

{
  name: "delete_file",
  description: "Permanently delete a file",
  inputSchema: { ... },
  annotations: {
    destructive: true,      // Changes state irreversibly
    idempotent: false,      // Calling twice has different effects
    readOnly: false,        // Modifies external state
    openWorld: false        // Only affects known resources
  }
}

These annotations let clients:

  • Warn users before destructive operations
  • Optimize caching for read-only tools
  • Make better retry decisions

Common Pitfalls

1. Vague Descriptions

// Bad
{ name: "process", description: "Process the data" }
 
// Good  
{ name: "parse_csv", description: "Parse CSV text into structured rows with headers" }

The AI uses descriptions to decide WHEN to use a tool. Be specific.

2. Missing Required Fields

// Bad - what does "query" mean?
{
  inputSchema: {
    properties: { query: { type: "string" } }
  }
}
 
// Good - clear expectations
{
  inputSchema: {
    properties: {
      query: { 
        type: "string",
        description: "SQL query to execute",
        examples: ["SELECT * FROM users LIMIT 10"]
      }
    },
    required: ["query"]
  }
}

3. Overly Complex Schemas

If your input schema needs 20 properties, you probably need multiple tools instead.

4. No Error Handling

Tools should return clear error messages:

{
  content: [
    {
      type: "text",
      text: "Error: File not found at /path/to/file"
    }
  ],
  isError: true
}

Building Your Own Tools

Here's a template for implementing a tool in TypeScript:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
 
const server = new McpServer({
  name: "my-server",
  version: "1.0.0"
});
 
// Define tools
server.tool(
  "my_tool",
  "Description of what this tool does",
  {
    param1: z.string().describe("First parameter"),
    param2: z.number().optional().describe("Optional number")
  },
  async ({ param1, param2 }) => {
    // Tool implementation
    const result = await doSomething(param1, param2);
    
    return {
      content: [
        { type: "text", text: JSON.stringify(result) }
      ]
    };
  }
);

The SDK handles:

  • Schema validation (via Zod)
  • JSON-RPC protocol
  • Error formatting

You focus on the logic.

Key Takeaways

  1. Tools are functions with structured inputs and outputs
  2. JSON Schema defines the contract between client and server
  3. Discovery happens via tools/list at connection time
  4. Good descriptions are critical — they're how AI decides what to use
  5. Annotations provide behavioral hints for smarter clients
  6. Keep tools focused — one responsibility per tool

The MCP tool system is elegantly simple but powerful. Understanding it deeply will help you build better integrations and debug issues faster.


Want to see tools in action? Check out the official MCP servers for real-world examples.

React to this post: