Chapter 4: Tools
The Star of the Show
If MCP were a band, Tools would be the lead singer. Resources are the solid bassist everyone respects but nobody screams for, and Prompts are the keyboard player your mom keeps asking about. But Tools—tools are why most people show up.
Tools are executable functions that MCP servers expose and LLMs can invoke. When Claude says “let me check the weather” and actually checks the weather, that’s a tool. When it creates a GitHub issue, queries a database, or runs a shell command—tools, tools, tools.
Tools are model-controlled, meaning the LLM decides when to use them based on context. The model sees the available tools, their descriptions, and their parameter schemas, and makes a judgment call about which tool to use and with what arguments. (With a human in the loop to approve, of course. We’re not animals.)
Anatomy of a Tool
Every tool has a definition with these fields:
{
"name": "create_github_issue",
"title": "Create GitHub Issue",
"description": "Creates a new issue in a GitHub repository",
"inputSchema": {
"type": "object",
"properties": {
"owner": {
"type": "string",
"description": "Repository owner (user or org)"
},
"repo": {
"type": "string",
"description": "Repository name"
},
"title": {
"type": "string",
"description": "Issue title"
},
"body": {
"type": "string",
"description": "Issue body in Markdown"
},
"labels": {
"type": "array",
"items": { "type": "string" },
"description": "Labels to apply to the issue"
}
},
"required": ["owner", "repo", "title"]
},
"annotations": {
"title": "Create GitHub Issue",
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": true
}
}
Let’s break this down:
name (required)
The unique identifier for the tool within this server. Tool names should be:
- Between 1 and 128 characters
- Case-sensitive
- Composed of letters, digits, underscores, hyphens, and dots
- No spaces, no commas, no emoji (sorry)
- Unique within the server
Good names: get_weather, search_documents, db.query, create_issue_v2
Bad names: do_stuff, tool1, my super cool tool!!!
title (optional)
A human-readable display name. Unlike name, this can have spaces, proper capitalization, and be friendly. It’s what shows up in UIs that list available tools.
description (optional but strongly recommended)
This is arguably the most important field, because the LLM reads it. A good description tells the model:
- What the tool does
- When to use it
- What the output looks like
- Any important constraints
The LLM uses this description to decide whether to invoke the tool. A vague description leads to poor tool selection. A detailed description leads to accurate, targeted tool use.
Bad: "Does weather stuff"
Good: "Get current weather information for a location. Returns temperature
in Fahrenheit, conditions (sunny/cloudy/rainy), humidity percentage,
and wind speed in mph. Use this when the user asks about weather,
temperature, or outdoor conditions for a specific place."
inputSchema (required)
A JSON Schema object defining the tool’s parameters. This must be a valid JSON Schema with type: "object". The LLM uses this schema to generate correctly-typed arguments.
For tools with no parameters:
{
"type": "object",
"additionalProperties": false
}
The schema defaults to JSON Schema draft 2020-12 unless a $schema field specifies otherwise.
outputSchema (optional)
A JSON Schema defining the structure of the tool’s output. When provided:
- The server MUST return structured results conforming to this schema
- Clients SHOULD validate results against it
- LLMs can better parse and use the output
{
"outputSchema": {
"type": "object",
"properties": {
"temperature": { "type": "number" },
"conditions": { "type": "string" },
"humidity": { "type": "number" }
},
"required": ["temperature", "conditions", "humidity"]
}
}
annotations (optional)
Metadata hints about the tool’s behavior. We’ll cover these in detail shortly.
Discovering Tools
Clients discover available tools by sending a tools/list request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/list",
"params": {}
}
The server responds with the full list:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"title": "Weather Lookup",
"description": "Get current weather for a location",
"inputSchema": { /* ... */ }
},
{
"name": "search_web",
"title": "Web Search",
"description": "Search the web using DuckDuckGo",
"inputSchema": { /* ... */ }
}
]
}
}
If the list is large, pagination kicks in (see Chapter 3). The host typically calls tools/list right after initialization and caches the result, refreshing when it receives a notifications/tools/list_changed notification.
Calling Tools
To invoke a tool, the client sends a tools/call request:
{
"jsonrpc": "2.0",
"id": 42,
"method": "tools/call",
"params": {
"name": "get_weather",
"arguments": {
"location": "San Francisco, CA"
}
}
}
The server executes the tool and returns the result:
{
"jsonrpc": "2.0",
"id": 42,
"result": {
"content": [
{
"type": "text",
"text": "Current weather in San Francisco, CA:\nTemperature: 62°F\nConditions: Foggy (obviously)\nHumidity: 85%\nWind: 12 mph W"
}
],
"isError": false
}
}
Tool Results: Content Types
Tool results contain a content array that can include multiple items of different types. This is more expressive than just returning a string.
Text Content
The most common type. Plain text that gets fed to the LLM:
{
"type": "text",
"text": "Query returned 42 rows. First row: id=1, name='Alice', age=30"
}
Image Content
Base64-encoded images. Useful for tools that generate charts, screenshots, or visual data:
{
"type": "image",
"data": "iVBORw0KGgoAAAANSUhEUg...",
"mimeType": "image/png"
}
Audio Content
Base64-encoded audio data:
{
"type": "audio",
"data": "UklGRiQAAABXQVZFZm10...",
"mimeType": "audio/wav"
}
Resource Links
A tool can return links to MCP resources, letting the client fetch more data if needed:
{
"type": "resource_link",
"uri": "file:///project/src/main.rs",
"name": "main.rs",
"description": "The file that was modified",
"mimeType": "text/x-rust"
}
Embedded Resources
A tool can embed entire resources inline:
{
"type": "resource",
"resource": {
"uri": "file:///project/output.json",
"mimeType": "application/json",
"text": "{\"status\": \"complete\", \"count\": 42}"
}
}
Structured Content
When a tool has an outputSchema, it can return structured data in addition to the content array:
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "{\"temperature\": 62, \"conditions\": \"Foggy\", \"humidity\": 85}"
}
],
"structuredContent": {
"temperature": 62,
"conditions": "Foggy",
"humidity": 85
}
}
}
The content array provides backward compatibility (older clients that don’t understand structuredContent still get something useful). The structuredContent field gives newer clients strongly-typed data.
Tool Annotations
Annotations are metadata hints that help clients understand a tool’s behavior without executing it. They’re advisory—not enforced, not guaranteed—but enormously useful for building good user interfaces.
The Annotation Fields
| Field | Type | Default | Purpose |
|---|---|---|---|
title | string | — | Human-readable display name |
readOnlyHint | boolean | false | Does this tool only read data? |
destructiveHint | boolean | true | Could this tool destroy or irreversibly modify data? |
idempotentHint | boolean | false | Is calling it twice with the same args safe? |
openWorldHint | boolean | true | Does it interact with external services? |
How Clients Use Annotations
A well-built host uses annotations to make smart UX decisions:
- Read-only tools (
readOnlyHint: true) might be auto-approved without user confirmation - Destructive tools (
destructiveHint: true) should always show a confirmation dialog - Idempotent tools (
idempotentHint: true) can be safely retried on failure - Open-world tools (
openWorldHint: true) might warrant extra scrutiny since they touch external systems
Example Annotations for Common Patterns
A search tool (reads data, touches the internet):
{
"readOnlyHint": true,
"openWorldHint": true
}
A file deletion tool (modifies state, destructive, but idempotent—deleting twice is the same as once):
{
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": true,
"openWorldHint": false
}
A database INSERT (modifies state, not destructive per se, not idempotent—inserting twice creates two rows):
{
"readOnlyHint": false,
"destructiveHint": false,
"idempotentHint": false,
"openWorldHint": false
}
A calculator (pure function, no side effects):
{
"readOnlyHint": true,
"openWorldHint": false
}
The Trust Warning
The spec is explicit: annotations are hints. A server could claim a tool is read-only when it actually formats your hard drive. Clients MUST NOT make security decisions based solely on annotations from untrusted servers.
Think of annotations like food labels. Helpful when the restaurant is trustworthy. Less so when you bought the sushi from a gas station.
Dynamic Tool Lists
Tools aren’t static. A server can add, remove, or modify tools at runtime. When it does, it sends a notification:
{
"jsonrpc": "2.0",
"method": "notifications/tools/list_changed"
}
The client should then re-fetch the tool list with tools/list.
This enables dynamic scenarios:
- A database server that adds tools based on the tables it discovers
- A plugin system where tools are loaded/unloaded at runtime
- A server that adapts its tool set based on the user’s permissions
- Feature flags that enable or disable tools
Error Handling: Two Kinds of Bad
As covered in Chapter 3, MCP distinguishes between protocol errors and tool execution errors. This distinction is worth hammering home because getting it wrong leads to confused LLMs and frustrated users.
When to Use Protocol Errors
Return a JSON-RPC error when:
- The tool name doesn’t exist →
-32601 - The request is malformed →
-32600 - The server crashes →
-32603
{
"jsonrpc": "2.0",
"id": 3,
"error": {
"code": -32601,
"message": "Unknown tool: 'gett_weather'. Did you mean 'get_weather'?"
}
}
When to Use Tool Execution Errors
Return a result with isError: true when:
- The file wasn’t found
- The API returned an error
- The input was valid JSON Schema but semantically wrong (date in the past, etc.)
- A rate limit was hit
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [{
"type": "text",
"text": "Error: Cannot query table 'users' - permission denied. Available tables: 'products', 'orders'."
}],
"isError": true
}
}
The key insight: tool execution errors get fed back to the LLM, which can often self-correct. “Permission denied on ‘users’? Let me try ‘products’ instead.” Protocol errors are typically dead ends.
Tool Name Conflicts
When a host connects to multiple servers, tool names might collide. Two servers could both expose a search tool.
The spec suggests several disambiguation strategies:
- Server name prefix:
web1___searchandweb2___search - Random prefix:
jrwxs___searchand6cq52___search - URI prefix:
web1.example.com:searchandweb2.example.com:search
The host is responsible for disambiguation. It knows which server each tool came from and can present them appropriately to the LLM.
Best Practices for Tool Design
1. Name Tools Like Functions
Tool names are identifiers, not sentences. Use snake_case or camelCase, be specific, and be consistent.
Good: get_current_weather, search_documents, create_issue
Bad: GetTheWeather, search, new
2. Write Descriptions for the LLM
Your description is a prompt. The LLM reads it to decide when and how to use the tool. Be specific about:
- What the tool does
- What it returns
- When to use it vs. alternatives
- Edge cases and limitations
3. Design Schemas Defensively
Use required fields. Add description to every property. Use enums for constrained values. Add minimum/maximum for numbers. The more constrained your schema, the more accurate the LLM’s arguments will be.
4. Make Errors Helpful
When a tool fails, tell the LLM why and what to do about it:
Bad: "Error"
Bad: "Something went wrong"
Good: "File not found: /tmp/data.csv. The /tmp directory contains:
report.csv, output.json, readme.txt"
5. Keep Tools Focused
One tool, one job. Don’t build a do_everything tool with a mode parameter. Build search_documents, create_document, delete_document. This makes it easier for the LLM to select the right tool and for users to understand what’s happening.
6. Think About Idempotency
If your tool can be safely retried (and many can), mark it as idempotent. This helps clients implement retry logic and gives users confidence that accidental double-invocations won’t cause problems.
7. Use Progress Reporting for Slow Tools
If a tool takes more than a second or two, report progress. Users and LLMs don’t like staring at a spinner with no information. A progress notification saying “Processing row 500 of 10,000” is infinitely better than silence.
Summary
Tools are the primary mechanism by which LLMs take action through MCP. They have names, descriptions, input schemas, optional output schemas, and behavioral annotations. They’re discovered via tools/list, invoked via tools/call, and can change at runtime.
The key design insight: tools are described richly enough for LLMs to use them intelligently, but executed on the server side where they have access to real data and systems. The LLM decides what to do; the server decides how to do it.
Next up: the quieter but equally important primitive—resources.