Skip to main content
Technology & EngineeringMcp Server298 lines

MCP Tools

Defining and implementing tools in MCP servers — the primary way AI assistants take actions through MCP. Covers tool definitions with JSON Schema inputs, writing tool handlers, returning structured results, error handling with isError, tool annotations for UI hints, and patterns for robust tool implementations.

Quick Summary28 lines
You are an AI assistant helping developers implement tools in MCP servers. Tools are the most important MCP primitive — they let AI models take actions like querying databases, calling APIs, modifying files, and running computations. Your role is to guide correct, robust tool implementations.

## Key Points

- What the tool does in concrete terms.
- What input it expects and in what format.
- What it returns.
- Constraints and limitations.
- When to use this tool versus a similar one.
- `readOnlyHint` — true if the tool does not modify state (safe to auto-approve).
- `destructiveHint` — true if the tool can irreversibly destroy data (prompt for confirmation).
- `idempotentHint` — true if calling the tool multiple times with the same input has the same effect as calling it once.
- `openWorldHint` — true if the tool interacts with external systems beyond the server's control.
- **Vague tool names** — `do_stuff`, `helper`, `run`. Use specific verbs: `query_database`, `read_file`, `create_issue`.
- **Missing descriptions** — the model cannot use a tool effectively without knowing what it does.
- **Overly broad tools** — a single tool that does 10 different things based on a `mode` parameter. Split into focused tools.

## Quick Example

```
Bad:  "description": "Runs a query"
Good: "description": "Execute a read-only SQL query against the connected PostgreSQL database. Returns results as JSON rows. Use this for SELECT queries only."

Bad:  "description": "File operations"
Good: "description": "Read the contents of a file at the given path. Returns the file content as UTF-8 text. Fails if the file does not exist or is not within the allowed directory."
```
skilldb get mcp-server-skills/MCP ToolsFull skill: 298 lines
Paste into your CLAUDE.md or agent config

MCP Tools

You are an AI assistant helping developers implement tools in MCP servers. Tools are the most important MCP primitive — they let AI models take actions like querying databases, calling APIs, modifying files, and running computations. Your role is to guide correct, robust tool implementations.

Philosophy

Tools are model-controlled: the AI model decides when and how to call them based on the user's intent. This means tool names and descriptions are critical — they are the interface the model reads to decide what to invoke. A tool with a vague description will be used incorrectly. A tool with a clear name, precise description, and well-structured input schema will be used reliably. Design tools for the model, not just for humans.

Techniques

Defining Tools

Tools are declared via the tools/list method. Each tool has a name, description, and input schema:

{
  "tools": [
    {
      "name": "query_database",
      "description": "Execute a read-only SQL query against the connected PostgreSQL database. Returns results as JSON rows. Use this for SELECT queries only — it will reject INSERT, UPDATE, or DELETE statements.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "description": "The SQL SELECT query to execute"
          },
          "limit": {
            "type": "integer",
            "description": "Maximum number of rows to return. Defaults to 100.",
            "default": 100
          }
        },
        "required": ["query"]
      }
    }
  ]
}

Writing Tool Descriptions

The description is the most important part of a tool definition. The AI model reads it to decide whether and how to use the tool.

Bad:  "description": "Runs a query"
Good: "description": "Execute a read-only SQL query against the connected PostgreSQL database. Returns results as JSON rows. Use this for SELECT queries only."

Bad:  "description": "File operations"
Good: "description": "Read the contents of a file at the given path. Returns the file content as UTF-8 text. Fails if the file does not exist or is not within the allowed directory."

Include in descriptions:

  • What the tool does in concrete terms.
  • What input it expects and in what format.
  • What it returns.
  • Constraints and limitations.
  • When to use this tool versus a similar one.

Input Schemas with JSON Schema

Input schemas use JSON Schema draft 2020-12. Be specific with types and constraints:

{
  "type": "object",
  "properties": {
    "path": {
      "type": "string",
      "description": "Absolute file path to read",
      "pattern": "^/"
    },
    "encoding": {
      "type": "string",
      "enum": ["utf-8", "base64"],
      "default": "utf-8",
      "description": "How to encode the file content in the response"
    },
    "line_range": {
      "type": "object",
      "properties": {
        "start": { "type": "integer", "minimum": 1 },
        "end": { "type": "integer", "minimum": 1 }
      },
      "description": "Optional line range to read. Omit to read the entire file."
    }
  },
  "required": ["path"],
  "additionalProperties": false
}

Implementing Tool Handlers (TypeScript)

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const server = new McpServer({
  name: "file-server",
  version: "1.0.0",
});

server.tool(
  "read_file",
  "Read the contents of a file at the given absolute path. Returns UTF-8 text.",
  {
    path: z.string().describe("Absolute file path to read"),
    encoding: z.enum(["utf-8", "base64"]).default("utf-8")
      .describe("Encoding for the response content"),
  },
  async ({ path, encoding }) => {
    // Validate the path is within allowed directories
    if (!isAllowedPath(path)) {
      return {
        isError: true,
        content: [{ type: "text", text: `Access denied: ${path} is outside allowed directories` }],
      };
    }

    try {
      const data = await fs.readFile(path, encoding === "utf-8" ? "utf-8" : undefined);
      const text = encoding === "base64" ? data.toString("base64") : data;
      return {
        content: [{ type: "text", text }],
      };
    } catch (err) {
      return {
        isError: true,
        content: [{ type: "text", text: `Failed to read file: ${err.message}` }],
      };
    }
  }
);

Implementing Tool Handlers (Python)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("file-server")

@mcp.tool()
async def read_file(path: str, encoding: str = "utf-8") -> str:
    """Read the contents of a file at the given absolute path.

    Args:
        path: Absolute file path to read
        encoding: Either 'utf-8' or 'base64'. Defaults to utf-8.

    Returns:
        The file contents as text.
    """
    if not is_allowed_path(path):
        raise ValueError(f"Access denied: {path} is outside allowed directories")

    with open(path, "r" if encoding == "utf-8" else "rb") as f:
        content = f.read()

    if encoding == "base64":
        import base64
        return base64.b64encode(content).decode("ascii")
    return content

Tool Result Content Types

Tools return content as an array of typed blocks:

{
  "content": [
    { "type": "text", "text": "Query returned 5 rows" },
    {
      "type": "image",
      "data": "iVBORw0KGgo...",
      "mimeType": "image/png"
    },
    {
      "type": "resource",
      "resource": {
        "uri": "file:///path/to/result.csv",
        "text": "col1,col2\nval1,val2",
        "mimeType": "text/csv"
      }
    }
  ]
}

Use text for most results. Use image for charts, screenshots, or visual data. Use resource to embed a resource reference in the result.

Error Handling: isError

Tool execution errors must NOT be returned as JSON-RPC errors. Use isError: true in the result:

// Correct — tool-level error
return {
  isError: true,
  content: [{ type: "text", text: "Table 'users' does not exist" }],
};

// Wrong — this is a protocol-level error, not for tool failures
throw new McpError(ErrorCode.InternalError, "Table 'users' does not exist");

The distinction matters: JSON-RPC errors indicate protocol problems (malformed request, method not found). Tool errors indicate the tool ran but could not complete its task. The model sees isError results and can decide to retry or try a different approach.

Tool Annotations

Annotations provide metadata hints to clients about a tool's behavior:

{
  "name": "delete_file",
  "description": "Permanently delete a file at the given path.",
  "inputSchema": { ... },
  "annotations": {
    "title": "Delete File",
    "readOnlyHint": false,
    "destructiveHint": true,
    "idempotentHint": true,
    "openWorldHint": false
  }
}
  • readOnlyHint — true if the tool does not modify state (safe to auto-approve).
  • destructiveHint — true if the tool can irreversibly destroy data (prompt for confirmation).
  • idempotentHint — true if calling the tool multiple times with the same input has the same effect as calling it once.
  • openWorldHint — true if the tool interacts with external systems beyond the server's control.

These are hints, not guarantees. Clients may use them for UI decisions like auto-approving read-only tools.

Validating Inputs Before Execution

Always validate inputs even though the schema provides some validation:

server.tool(
  "run_query",
  "Execute a read-only SQL query",
  { query: z.string() },
  async ({ query }) => {
    const normalized = query.trim().toUpperCase();
    if (!normalized.startsWith("SELECT")) {
      return {
        isError: true,
        content: [{ type: "text", text: "Only SELECT queries are allowed" }],
      };
    }

    // Additional safety: reject dangerous patterns
    const forbidden = ["DROP", "DELETE", "INSERT", "UPDATE", "ALTER", "TRUNCATE"];
    for (const keyword of forbidden) {
      if (normalized.includes(keyword)) {
        return {
          isError: true,
          content: [{ type: "text", text: `Query contains forbidden keyword: ${keyword}` }],
        };
      }
    }

    const result = await db.query(query);
    return {
      content: [{ type: "text", text: JSON.stringify(result.rows, null, 2) }],
    };
  }
);

Anti-Patterns

  • Vague tool namesdo_stuff, helper, run. Use specific verbs: query_database, read_file, create_issue.
  • Missing descriptions — the model cannot use a tool effectively without knowing what it does.
  • Overly broad tools — a single tool that does 10 different things based on a mode parameter. Split into focused tools.
  • Throwing exceptions for expected failures — use isError: true for tool-level errors. Reserve exceptions for bugs.
  • Ignoring input validation — never trust that inputs match the schema. Validate file paths, SQL queries, URLs, and all user-influenced data.
  • Returning raw error stack traces — return human-readable error messages. Stack traces leak implementation details and confuse the model.
  • Not setting additionalProperties: false — without this, the model might send extra fields that silently get ignored, masking prompt issues.

Best Practices

  • Keep tool count manageable. Ten well-described tools are better than fifty overlapping ones.
  • Use consistent naming conventions across all tools in a server (snake_case is conventional).
  • Test every tool with the MCP Inspector before connecting to a real client.
  • Return structured data (JSON) rather than prose when the result will be processed further.
  • Set sensible defaults for optional parameters to minimize required input.
  • Document edge cases in the tool description — what happens with empty input, missing resources, permission errors.

Install this skill directly: skilldb add mcp-server-skills

Get CLI access →

Related Skills

MCP Auth and Security

Securing MCP servers with authentication, authorization, and defensive practices. Covers OAuth 2.1 integration for remote servers, API key management through environment variables, input validation and sanitization, rate limiting, sandboxing tool execution, path traversal prevention, and the principle of least privilege for tool design.

Mcp Server327L

MCP Deployment

Deploying MCP servers across different environments and transports. Covers local deployment via stdio, remote deployment with SSE and streamable HTTP, Docker containerization, cloud deployment on AWS/GCP/Vercel, npx and uvx distribution for zero-install usage, configuration management, and production hardening.

Mcp Server353L

MCP Fundamentals

Core architecture of the Model Context Protocol (MCP) — the open protocol from Anthropic that connects AI assistants to external tools and data sources. Covers JSON-RPC transport, capabilities negotiation, server lifecycle, the client-server interaction model, and how tools, resources, and prompts fit together.

Mcp Server226L

MCP Patterns

Common architectural patterns for MCP servers — database servers, API wrappers, file system servers, multi-tool orchestration, caching strategies, error recovery, and composition patterns. Practical blueprints for building production-quality MCP servers that handle real-world complexity.

Mcp Server431L

MCP Prompts

Defining prompt templates in MCP servers that AI clients can discover and invoke. Covers prompt definitions with arguments, dynamic prompt generation, multi-turn prompt structures, embedding resources in prompts, prompt discovery, and patterns for building reusable prompt libraries.

Mcp Server287L

MCP Python Server

Building MCP servers in Python using the official mcp SDK and the FastMCP high-level pattern. Covers project setup with uv, defining tools with type hints, async handlers, resources, prompts, stdio and SSE transports, context objects, and deployment strategies including uvx distribution.

Mcp Server390L