Module 04: Tools and the Model Context Protocol (MCP)
Goal: Deeply understand tool design for AI systems and the Model Context Protocol — enough to build production integrations and answer interview questions confidently.
Table of Contents
- Tool Use Fundamentals
- Tool Design Principles
- Model Context Protocol (MCP)
- Building an MCP Server in Python
- MCP Server Patterns
- Connecting MCP to Claude Code
- When to Use What
- Interview Flashcards
1. Tool Use Fundamentals
What Is Tool Use?
Tool use (also called “function calling”) allows a language model to signal that it wants to invoke an external function rather than generating a plain text response. The model does not execute the function itself — it emits a structured request, your application code runs the function, and the result is fed back to the model.
The flow looks like this:
User message
↓
Claude sees tools in system context
↓
Claude emits a tool_use block (name + input)
↓
Your code calls the actual function
↓
Your code sends a tool_result block back to Claude
↓
Claude generates its final response using the result
This cycle can repeat multiple times in a single conversation if Claude needs to call multiple tools or chain tool calls.
The Full Anatomy of a Tool Definition
Every tool you pass to the Anthropic API has exactly this shape:
{
"name": "get_weather",
"description": "Get the current weather conditions for a specific city. Returns temperature in Celsius, humidity percentage, wind speed in km/h, and a short condition label (e.g. 'Partly cloudy'). Use this when the user asks about current weather, not forecasts.",
"input_schema": {
"type": "object",
"properties": {
"city": {
"type": "string",
"description": "The name of the city, e.g. 'London' or 'New York'"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Temperature unit. Defaults to celsius if not specified."
}
},
"required": ["city"]
}
}Key fields:
| Field | Required | Purpose |
|---|---|---|
name | Yes | Unique identifier Claude uses to refer to this tool |
description | Yes | Natural language explanation — this is model context, not documentation |
input_schema | Yes | JSON Schema object describing the tool’s parameters |
input_schema.type | Yes | Always "object" at the top level |
input_schema.properties | Yes | Each parameter and its schema |
input_schema.required | No | Array of parameter names that must always be provided |
Why Description Quality Matters
The description is not a comment — it is part of the model’s context window. Claude reads it while deciding:
- Whether to use this tool at all
- When to use it vs another tool
- What values to pass to each parameter
A vague description leads to wrong tool selection, incorrect parameter values, or the model hallucinating the tool’s behavior. A good description answers four questions:
- What does it do? — The action performed
- When should you use it? — Conditions/triggers
- What does it return? — Shape and meaning of the output
- What are the edge cases? — Limitations, errors, special inputs
Compare:
// Bad — gives Claude almost no signal
"description": "Gets weather"
// Good — gives Claude everything it needs
"description": "Get the current weather conditions for a specific city. Returns temperature in Celsius, humidity percentage, wind speed in km/h, and a short condition label. Use this for current conditions only — not forecasts. Returns an error field if the city is not found."The second description tells Claude what the return value looks like (so it can plan its next step), when to use it versus a forecast tool, and what to expect on failure.
JSON Schema Essentials for AI Tools
JSON Schema is the language you use to describe tool parameters. Here are the types you’ll use most:
string
{
"type": "string",
"description": "The user's email address"
}number and integer
{
"type": "integer",
"description": "Page number, starting at 1",
"minimum": 1
}boolean
{
"type": "boolean",
"description": "If true, include archived items in results"
}array
{
"type": "array",
"items": { "type": "string" },
"description": "List of tag names to filter by"
}object (nested structure)
{
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zip": { "type": "string" }
},
"required": ["street", "city"],
"description": "Mailing address"
}enum (fixed set of values)
{
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"description": "Priority level for the ticket"
}Enum is especially useful because it constrains Claude’s output to valid values — no more "HIGH" vs "high" inconsistencies.
anyOf (union types)
{
"anyOf": [
{ "type": "string" },
{ "type": "null" }
],
"description": "Optional search query. Pass null to return all results."
}Handling Optional vs Required Parameters
Parameters in required must always be provided by Claude. Parameters not in required are optional.
Best practices:
- Put only truly mandatory parameters in
required - For optional parameters, document the default behavior in the description
- Avoid using
nullas a sentinel — prefer omitting the field entirely (which JSON Schema supports naturally) - If a parameter has a sensible default, say so:
"Defaults to 'en' if not specified."
{
"name": "search_documents",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The search query string"
},
"max_results": {
"type": "integer",
"description": "Maximum number of results to return. Defaults to 10. Max 100.",
"minimum": 1,
"maximum": 100
},
"include_archived": {
"type": "boolean",
"description": "Whether to include archived documents. Defaults to false."
}
},
"required": ["query"]
}
}Tool Result Format
When your code runs the tool and gets a result, you send it back using a tool_result content block:
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01abc123", # Must match the tool_use block's id
"content": "The weather in London is 15°C, partly cloudy."
# OR pass a list of content blocks:
# "content": [{"type": "text", "text": "..."}]
}
]
}Key points:
- The
tool_use_idmust exactly match theidfrom Claude’stool_useblock contentcan be a plain string or a list of content blocks- For errors, set
"is_error": trueand put the error message incontent - You can return multiple tool results in the same message if Claude called multiple tools
2. Tool Design Principles
Verb-Noun Naming
Tool names should be verb_noun in snake_case. The verb describes the action; the noun describes the subject.
Good:
search_documents
create_ticket
get_user_profile
list_repositories
delete_file
summarize_meeting
Bad:
tool1
doStuff
handler
process
myFunction
getData ← too vague, get what?
The name appears in Claude’s reasoning. A good name makes the tool’s purpose obvious without reading the description.
Minimal Surface Area
One tool does one thing well. Resist the temptation to add flags and modes that change a tool’s fundamental behavior.
# Bad: one tool with a "mode" parameter that changes what it does
{
"name": "manage_ticket",
"description": "Creates, updates, or deletes a ticket based on the 'action' parameter",
"input_schema": {
"properties": {
"action": {"type": "string", "enum": ["create", "update", "delete"]},
...
}
}
}
# Good: three separate focused tools
create_ticket(title, description, priority)
update_ticket(ticket_id, fields_to_update)
close_ticket(ticket_id, resolution_note)Benefits of minimal surface area:
- Claude can more precisely choose the right tool
- Easier to test and debug each tool independently
- Cleaner error handling (each tool has its own error modes)
- Safer: Claude can’t accidentally delete when it meant to update
Idempotency Hierarchy
Design tools in a hierarchy of safety:
Reads (GET) — safest; can call multiple times, same result
Writes (PUT) — moderate risk; idempotent updates are safer than appends
Creates (POST) — creates new state; not idempotent; use carefully
Deletes (DELETE) — highest risk; irreversible; require confirmation patterns
Make write operations idempotent where possible:
# Not idempotent — creates duplicate if called twice
def create_tag(name: str) -> dict: ...
# Idempotent — safe to call multiple times
def ensure_tag_exists(name: str) -> dict:
"""Creates tag if it doesn't exist. Returns existing tag if found."""
...For destructive tools (delete, overwrite), consider:
- Requiring a confirmation parameter:
{"confirmed": true} - Moving to trash instead of permanent deletion
- Providing a dry-run mode
Rich Descriptions
A complete tool description answers:
- What it does — the action
- When to use it — conditions, triggers, use cases
- What it returns — the output structure and meaning
- Edge cases — what happens with bad input, empty results, rate limits
Template:
"[Verb] [noun] [brief what]. [When to use]. Returns [output description].
[Edge cases or constraints]."
Example:
"Search documents in the knowledge base using full-text search. Use this
when the user asks a question that might be answered by existing documentation.
Returns a list of matching documents with their titles, snippets, and relevance
scores. Returns an empty list if no matches found. The query must be at least
2 characters long."
Typed Outputs
Return structured data, not raw strings. This lets Claude plan follow-up actions based on the result’s structure.
# Bad: Claude can't reliably parse this
def get_user(user_id: str) -> str:
return "John Smith, john@example.com, admin role, active"
# Good: Claude can navigate the structure
def get_user(user_id: str) -> dict:
return {
"id": user_id,
"name": "John Smith",
"email": "john@example.com",
"role": "admin",
"status": "active"
}When you serialize the result to send back as a tool_result, use json.dumps() — Claude handles JSON in tool results natively.
Error Handling in Tools
Never let tool functions raise uncaught exceptions. Catch errors and return them as structured data in the tool result. Use is_error: true to signal Claude that something went wrong.
def call_tool(tool_name: str, tool_input: dict) -> dict:
try:
result = execute_tool(tool_name, tool_input)
return {
"type": "tool_result",
"tool_use_id": tool_use_id,
"content": json.dumps(result)
}
except ToolNotFoundError as e:
return {
"type": "tool_result",
"tool_use_id": tool_use_id,
"is_error": True,
"content": f"Tool not found: {e}"
}
except ValueError as e:
return {
"type": "tool_result",
"tool_use_id": tool_use_id,
"is_error": True,
"content": f"Invalid input: {e}"
}When Claude receives an error result, it can:
- Try a different approach
- Use different parameters
- Explain the failure to the user
- Ask for clarification
Anti-Patterns with Examples
| Anti-Pattern | Why It’s Bad | Fix |
|---|---|---|
"description": "Does stuff with files" | No signal for tool selection | Be specific about what, when, returns, edge cases |
| God tool with mode parameter | Ambiguous, hard to select correctly | Split into focused tools |
| Returning raw HTML/XML strings | Hard for LLM to extract structured info | Parse and return a dict |
| Raising exceptions from tools | Crashes the tool loop; Claude can’t recover | Catch and return is_error: true |
required: [every_param] | Forces Claude to provide optional params | Only require truly mandatory params |
| No enum for constrained strings | Claude may use invalid values | Use enum for fixed-set params |
| Side-effectful reads | Unexpected state change on read | Separate read and write operations |
| Overly broad tools | Poor tool selection, more errors | Narrow scope, improve description |
3. Model Context Protocol (MCP)
What MCP Is
The Model Context Protocol (MCP) is an open standard that defines how AI models connect to external tools, data sources, and services. It was created by Anthropic and released as an open protocol in late 2024.
MCP is a client-server protocol. The “server” exposes capabilities (tools, data, prompts). The “client” (typically an AI assistant or IDE) connects to the server and uses those capabilities on behalf of users.
Think of it like HTTP for AI tool integrations:
- HTTP standardizes how web clients talk to web servers
- MCP standardizes how AI assistants talk to tool servers
Why MCP Was Created
Before MCP, every integration was bespoke:
- To connect Claude to a database → write custom code
- To connect Claude to Slack → write different custom code
- To connect VS Code + Claude to GitHub → yet more custom code
- Same tools couldn’t be reused across Claude Desktop, Claude Code, and custom apps
MCP solves this by providing a single protocol that any AI client can speak and any tool server can implement. The result: write an MCP server once, and it works everywhere that supports MCP (Claude Desktop, Claude Code, Continue.dev, etc.).
The Three MCP Primitives
MCP defines exactly three types of things a server can expose:
1. Resources
Resources are read-only data sources. They are identified by a URI and return content when read.
Think of resources like files or API endpoints you can GET from:
- A file on disk:
file:///home/user/notes.txt - A database query result:
db://mydb/users/active - A live feed:
weather://london/current
Resources can be static (content doesn’t change) or dynamic (content is computed on access).
# Resource definition in MCP Python SDK
@server.list_resources()
async def list_resources():
return [
Resource(
uri="memo://notes",
name="Project Notes",
description="Current project notes and todos",
mimeType="text/plain"
)
]
@server.read_resource()
async def read_resource(uri: AnyUrl):
if str(uri) == "memo://notes":
return "## Project Notes\n- Fix the login bug\n- Deploy by Friday"Key properties of resources:
- They do not have side effects (read-only by definition)
- They are identified by URI, not by function call
- They can be listed (discoverable)
- Content can be text or binary
2. Tools
Tools are callable functions with possible side effects. They are the MCP equivalent of API endpoints you can POST to.
@server.call_tool()
async def call_tool(name: str, arguments: dict):
if name == "create_ticket":
title = arguments["title"]
# ... create the ticket
return [TextContent(type="text", text=f"Created ticket: {ticket_id}")]Tools in MCP have the same structure as tools in the Anthropic API (name, description, input schema) — but they’re defined once on the server and automatically exposed to any connecting client.
Key properties of tools:
- They can have side effects (create, update, delete)
- They take structured inputs (JSON Schema)
- They return content (text, images, or structured data)
3. Prompts
Prompts are reusable prompt templates that can be parameterized.
@server.list_prompts()
async def list_prompts():
return [
Prompt(
name="code_review",
description="Review code for quality and security issues",
arguments=[
PromptArgument(name="code", description="The code to review", required=True),
PromptArgument(name="language", description="Programming language", required=False)
]
)
]Prompts are less commonly used than tools and resources, but they’re powerful for standardizing how users interact with specialized workflows.
MCP Transport Layers
MCP can run over different transport mechanisms:
| Transport | Use Case | Description |
|---|---|---|
| stdio | Local tools | Server runs as a subprocess; communication over stdin/stdout. Used by Claude Desktop and Claude Code. |
| SSE (Server-Sent Events) | Remote tools | Server runs as an HTTP service. Client connects and receives events. Used for hosted MCP servers. |
| HTTP+SSE | Hybrid | POST for requests, SSE stream for responses. |
For local development and Claude Code/Desktop integrations, stdio is the standard. The host application (Claude Code) spawns the MCP server as a child process and communicates via stdin/stdout.
MCP Server vs MCP Client
MCP Server:
- Exposes capabilities (tools, resources, prompts)
- Written by tool developers (you write this)
- Runs as a process (local) or HTTP service (remote)
- Has no knowledge of the AI model — only speaks MCP protocol
- Examples: a filesystem server, a GitHub server, a database query server
MCP Client:
- Connects to MCP servers and uses their capabilities
- Usually part of an AI host application
- Discovers available tools/resources and presents them to the LLM
- Manages the connection lifecycle
- Examples: Claude Desktop, Claude Code, Continue.dev, your custom app using the MCP SDK
The separation is key: you write servers; the AI assistant acts as the client. This means your server works with any MCP-compatible client.
How Claude Code Uses MCP
Claude Code (the CLI) acts as an MCP client. When you register an MCP server in ~/.claude/settings.json, Claude Code:
- Spawns the server as a subprocess (for stdio servers)
- Discovers the server’s tools using the MCP
tools/listcall - Makes those tools available to Claude with names prefixed:
mcp__<server_name>__<tool_name>
For example, if you register a server named "minimal" that exposes a tool "add_numbers", Claude Code makes it available as mcp__minimal__add_numbers.
This naming convention (mcp__<server>__<tool>) is how you see MCP tools in Claude Code sessions.
MCP vs Raw API Tool Use
| Aspect | Raw API Tool Use | MCP Server |
|---|---|---|
| Where defined | In your API call | In a separate server process |
| Reusability | Per-application | Any MCP-compatible client |
| Discovery | You choose which tools to pass | Client auto-discovers |
| Transport | In-process | IPC (stdio) or network |
| State | Stateless per call | Server can maintain state |
| Best for | App-specific, tightly coupled | Reusable, cross-project tools |
4. Building an MCP Server in Python
Installation
pip install mcp
# or with uv:
uv add mcpThe mcp package provides the Python SDK for building MCP servers and clients.
Minimal Server Structure
import asyncio
from mcp.server import Server
from mcp.server.models import InitializationOptions
import mcp.server.stdio
import mcp.types as types
# Create the server instance
server = Server("my-server-name")
# Define tools
@server.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="my_tool",
description="What this tool does and when to use it.",
inputSchema={
"type": "object",
"properties": {
"param1": {"type": "string", "description": "..."}
},
"required": ["param1"]
}
)
]
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "my_tool":
result = do_something(arguments["param1"])
return [types.TextContent(type="text", text=result)]
raise ValueError(f"Unknown tool: {name}")
# Run the server
async def main():
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(
read_stream,
write_stream,
InitializationOptions(
server_name="my-server-name",
server_version="0.1.0",
capabilities=server.get_capabilities(
notification_options=None,
experimental_capabilities={}
)
)
)
if __name__ == "__main__":
asyncio.run(main())Defining Tools
Tools are defined using two decorators:
@server.list_tools()— returns the tool definitions (name, description, schema)@server.call_tool()— handles tool invocations
The call_tool handler receives the tool name and a dict of arguments. It must return a list of content objects (TextContent, ImageContent, or EmbeddedResource).
@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
if name == "add_numbers":
a = arguments["a"]
b = arguments["b"]
return [types.TextContent(type="text", text=str(a + b))]
raise ValueError(f"Unknown tool: {name}")Defining Resources
Resources use two decorators:
@server.list_resources()— returns available resource URIs and metadata@server.read_resource()— returns the content of a specific resource
from mcp.types import Resource, TextContent
from pydantic import AnyUrl
@server.list_resources()
async def list_resources() -> list[Resource]:
return [
Resource(
uri=AnyUrl("memo://notes"),
name="Memo Notes",
description="Current notes stored in this server",
mimeType="text/plain"
)
]
@server.read_resource()
async def read_resource(uri: AnyUrl) -> str:
if str(uri) == "memo://notes":
return "## Notes\n- Item 1\n- Item 2"
raise ValueError(f"Unknown resource: {uri}")Running the Server
stdio mode (for Claude Desktop/Code):
async with mcp.server.stdio.stdio_server() as (read_stream, write_stream):
await server.run(read_stream, write_stream, init_options)The server communicates via stdin/stdout. The host application (Claude Code) manages the process lifecycle.
SSE mode (for remote/networked access):
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
sse = SseServerTransport("/messages")
# ... mount on a Starlette/FastAPI appConfiguration in ~/.claude/settings.json
{
"mcpServers": {
"my-server": {
"command": "python",
"args": ["/absolute/path/to/server.py"],
"env": {
"API_KEY": "optional-env-vars"
}
}
}
}Use absolute paths. Relative paths often fail because the working directory when Claude Code spawns the process may not be what you expect.
5. MCP Server Patterns
Pattern 1: Filesystem Server
Expose local files as resources and provide tools for reading/searching files.
# Tools:
# read_file(path) -> file contents
# search_files(directory, pattern) -> list of matching file paths
# list_directory(path) -> directory listing
# Resources:
# file:///home/user/project/README.md -> file contentsThis is the most common pattern for giving an AI assistant access to a codebase. The official @modelcontextprotocol/server-filesystem is a reference implementation.
Security note: scope the server to a specific root directory. Never expose / or ~.
Pattern 2: Database Query Server (Read-Only SQL)
Expose database query capabilities while enforcing read-only access.
# Tools:
# execute_query(sql) -> rows as list of dicts
# list_tables() -> table names and schemas
# describe_table(table_name) -> column definitions
# Resources:
# db://schema -> full database schemaKey safety concern: use a read-only database user or connection. Never expose write access unless you have strong input validation and audit logging.
Pattern 3: API Wrapper Server
Wrap a REST API as MCP tools so Claude can interact with external services.
# Example: wrapping a task management API
# Tools:
# list_tasks(project_id, status?) -> list of tasks
# create_task(project_id, title, description?, priority?) -> created task
# update_task(task_id, fields) -> updated task
# add_comment(task_id, comment) -> comment idEach REST endpoint becomes an MCP tool. The server handles authentication (API keys from env vars), request construction, and error normalization.
Pattern 4: Memory/Note Server
A persistent storage server for notes, context, or memory that persists across conversations.
# Tools:
# save_note(key, content) -> success
# get_note(key) -> content
# list_notes() -> list of keys
# delete_note(key) -> success
# Resources:
# notes://all -> all notes as structured text
# notes://recent -> recently added notesThis pattern is used by Claude’s built-in memory tool. You can build custom versions that store notes in files, SQLite, or a vector database for semantic search.
6. Connecting MCP to Claude Code
Adding an MCP Server
Edit ~/.claude/settings.json:
{
"mcpServers": {
"notes": {
"command": "python",
"args": ["/Users/you/mcp-servers/notes/server.py"]
},
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": {
"GITHUB_TOKEN": "your-token-here"
}
}
}
}The key in mcpServers is the server name used for tool naming (mcp__notes__save_note, etc.).
The mcpServers Section Format
{
"mcpServers": {
"<server-name>": {
"command": "<executable>", // Required: the command to run
"args": ["<arg1>", "<arg2>"], // Optional: command-line arguments
"env": { // Optional: environment variables
"KEY": "value"
},
"cwd": "/path/to/working/dir" // Optional: working directory
}
}
}Claude Code spawns the process when it starts and keeps it alive for the session.
Debugging MCP Connections
Using /doctor: Run /doctor in Claude Code to see the health status of all configured MCP servers. It will show which servers connected successfully, which failed, and why.
Common issues:
| Issue | Likely Cause | Fix |
|---|---|---|
| Server not found | Wrong command path | Use which python to get full path |
| Server crashes on start | Import error or missing deps | Run python server.py manually to see the error |
| Tools not appearing | Server connected but tool registration failed | Check list_tools handler |
| Env vars not available | Forgot to set env in config | Add to env section in settings.json |
Manual testing: You can test your server outside of Claude Code by running it directly. For stdio servers, you’d need an MCP client, but often just running python server.py and seeing no import errors is a good first check.
Security Model
MCP servers run as your user process with your permissions. This means:
- A malicious MCP server has access to everything your user account has access to
- Only install MCP servers from trusted sources
- Review the server code before adding it to your config
- Use environment variables for secrets — never hardcode API keys in server code
- For servers with network access, be aware of what data they can exfiltrate
- Consider running sensitive servers in a container with limited permissions
The trust model is similar to installing a VS Code extension: you’re giving code someone else wrote access to run with your credentials.
7. When to Use What
Decision Framework
Is the tool reusable across multiple projects/clients?
├── Yes → Build an MCP server
└── No → Use raw API tool use
Does the tool need to persist state between calls?
├── Yes → MCP server (can maintain in-memory or file state)
└── No → Either works
Is this a one-off integration in a specific app?
├── Yes → Raw API tool use (simpler, no extra process)
└── No → MCP server (more overhead, but reusable)
Does the user need to use this from Claude Desktop AND your app?
├── Yes → MCP server (write once, use anywhere)
└── No → Either works
Is this a production API that other teams might integrate?
├── Yes → MCP server with proper auth/versioning
└── No → Either works
Raw API Tool Use: Best When
- You’re building a specific application that owns the tool definitions
- The tools are tightly coupled to your app’s logic
- You need maximum control over the tool execution environment
- You want the simplest possible setup (no extra process)
- The tools change frequently with your application
MCP Server: Best When
- You want to reuse tools across Claude Desktop, Claude Code, and your app
- You’re exposing developer tools (filesystem, database, APIs) to AI assistants
- Multiple people on a team need the same integrations
- You’re building a service that other developers will integrate
- The tools need to run as a persistent process (connection pooling, caching)
Neither (Direct External API Calls): Best When
- The LLM doesn’t need to decide when/how to call the API — your code does
- You don’t need Claude to orchestrate multi-step tool workflows
- Simple data enrichment pipeline: fetch → transform → summarize
8. Interview Flashcards
Q: What is MCP and why was it created?
A: The Model Context Protocol (MCP) is an open standard (created by Anthropic, 2024) for connecting AI models to external tools, data sources, and services. It was created because before MCP, every AI tool integration was custom-built — you’d write different glue code for every combination of AI assistant and external service. MCP standardizes the protocol so a tool server written once works with any MCP-compatible client (Claude Desktop, Claude Code, Continue.dev, etc.). Think of it as HTTP for AI tool integrations.
Q: What are the three MCP primitives?
A:
- Resources — Read-only data sources identified by URI. Like a GET endpoint. Can be static or dynamic. Examples: a file, a database query result, a live API feed.
- Tools — Callable functions with possible side effects. Like a POST endpoint. Take structured inputs (JSON Schema), return content. Examples: create_ticket, search_database, send_email.
- Prompts — Reusable, parameterized prompt templates. Used to standardize how users trigger specific AI workflows. Examples: a code_review prompt, a summarize_document prompt.
Q: How do you design a good tool description?
A: A good tool description answers four questions: (1) What does it do? (2) When should you use it vs other tools? (3) What does it return? (4) What are the edge cases or constraints? The description is literally part of the model’s context window — it’s what Claude reads when deciding whether and how to use the tool. A vague description leads to wrong tool selection and incorrect parameter values. A good description reads like: “Search documents in the knowledge base using full-text search. Use when the user asks a question that might be answered by existing docs. Returns a list of matches with titles, snippets, and relevance scores. Returns empty list if no matches. Query must be at least 2 characters.”
Q: What is the difference between a Resource and a Tool in MCP?
A: The key difference is side effects. Resources are read-only data sources — they return content but don’t change state. Tools are callable functions that can have side effects (create, update, delete). Resources are accessed by URI (like a URL) and can be listed and browsed. Tools are invoked by name with structured arguments. Use a resource when exposing data that should be readable but not writable (files, database queries). Use a tool when the action might change something.
Q: How do you handle errors in tool results?
A: Return errors in the tool result using is_error: true, rather than raising exceptions. Raising an exception would crash the tool loop. By returning a structured error, you let Claude decide how to handle it — it might try different parameters, try a different tool, or explain the failure to the user. In the Anthropic API: {"type": "tool_result", "tool_use_id": "...", "is_error": true, "content": "Error: city 'Atlantis' not found"}. Always catch exceptions in your tool executor and convert them to error results.
Q: What transport does MCP use for local vs remote servers?
A: For local servers (Claude Desktop, Claude Code), MCP uses stdio transport. The host application spawns the server as a subprocess and communicates via stdin/stdout. This is simple, secure (no network), and is the standard for developer tooling. For remote servers, MCP uses SSE (Server-Sent Events) or HTTP+SSE, where the server is an HTTP service. Remote transport is used for hosted, shared MCP servers accessible over a network.
Q: When would you use MCP over raw tool use in the Anthropic API?
A: Use MCP when you want reusability across clients. If you’re building tools that should work in Claude Desktop, Claude Code, and your custom app — write them once as an MCP server. Use raw API tool use when tools are tightly coupled to a specific application, when you want the simplest possible setup (no extra process), or when you need maximum control over tool execution. MCP has more overhead (a separate process, a protocol) but pays off when the same tools need to be used in multiple contexts.
Q: How do you make a tool idempotent?
A: An idempotent tool produces the same result whether called once or many times with the same inputs. Strategies:
- Check before create: Before creating a resource, check if it exists and return the existing one.
ensure_tag_exists(name)instead ofcreate_tag(name). - Use upsert semantics: “Create or update” rather than “always create”.
- Use deterministic IDs: If the ID is derived from the content (hash-based), the same content always produces the same ID.
- Separate reads and writes: Make read tools inherently idempotent (they don’t change state). Keep write tools focused and add idempotency at the write level.
Idempotency matters because tools can be retried on failure, and Claude sometimes calls the same tool multiple times.
Appendix: Quick Reference
Tool Definition Template
{
"name": "verb_noun",
"description": "Does X. Use when Y. Returns Z. Edge case W.",
"input_schema": {
"type": "object",
"properties": {
"required_param": {
"type": "string",
"description": "What this parameter means"
},
"optional_enum_param": {
"type": "string",
"enum": ["option_a", "option_b"],
"description": "One of option_a or option_b. Defaults to option_a."
}
},
"required": ["required_param"]
}
}Tool Result Template
# Success
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result_dict)
}
# Error
{
"type": "tool_result",
"tool_use_id": tool_use.id,
"is_error": True,
"content": "Error message explaining what went wrong"
}MCP Server Settings Template
{
"mcpServers": {
"server-name": {
"command": "/usr/bin/python3",
"args": ["/absolute/path/to/server.py"],
"env": {
"ENV_VAR": "value"
}
}
}
}MCP Tool Naming Convention
mcp__<server-name>__<tool-name>
Examples:
mcp__filesystem__read_file
mcp__github__create_issue
mcp__minimal__add_numbers