Tools are the primary way your MCP server exposes functionality to clients. They represent actions that an AI agent can invoke to perform tasks.
Example
@server.tool(
name="get_weather",
title="Weather Information Provider",
description="Get current weather information for a location",
annotations=ToolAnnotations(
readOnlyHint=True,
openWorldHint=True,
),
)
async def get_weather(
location: Annotated[str, Field(description="City name or zip code")],
units: Annotated[str, Field(description="Temperature units", default="celsius")],
context: Context,
) -> str:
"""Get current weather information for a location."""
await context.info(f"Fetching weather for {location}")
return f"Weather in {location}: 22 {units}"
Here’s the JSON-RPC response that the example above produces when a client calls tools/list:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{
"name": "get_weather",
"title": "Weather Information Provider",
"description": "Get current weather information for a location",
"annotations": {
"readOnlyHint": true,
"openWorldHint": true
},
"inputSchema": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City name or zip code"
},
"units": {
"type": "string",
"description": "Temperature units",
"default": "celsius"
}
},
"required": ["location"]
}
}
]
}
}
Here’s what maps to what:
| Python | MCP Schema | Notes |
|---|
name="get_weather" | "name" | Falls back to function name if omitted |
title="Weather Information Provider" | "title" | Human-readable display name |
description="Get current weather..." | "description" | Falls back to docstring if omitted |
annotations=ToolAnnotations(...) | "annotations" | Behavioral hints for clients |
location: str | "type": "string" | Python type hint → JSON Schema type |
Field(description="City name...") | property "description" | Argument-level description |
default="celsius" | "default": "celsius" | Makes the parameter optional |
No default on location | "required": ["location"] | Parameters without defaults are required |
context: Context | (not in schema) | Automatically excluded from the schema |
Always use Annotated[type, Field(description="...")] for your tool parameters. Without it, LLMs calling your tool won’t know what each argument means.
Minimal Definition
At minimum, a tool only needs a function with type hints:
@server.tool()
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
The function name becomes the tool name, the docstring becomes the description, and type hints define the input schema.
The @server.tool() decorator accepts these options:
@server.tool(
name="custom_name", # Override the function name
title="Custom Title", # Human-readable title
description="Custom desc", # Override the docstring
annotations=ToolAnnotations( # Behavioral hints for clients
destructiveHint=True,
readOnlyHint=False,
),
structured_output=True, # Return structured JSON output
)
def my_tool(param: str) -> str:
return param
Tool annotations provide hints to clients about the tool’s behavior:
| Annotation | Description |
|---|
destructiveHint | Tool may modify or delete data |
readOnlyHint | Tool only reads data, no side effects |
idempotentHint | Calling multiple times has same effect as once |
openWorldHint | Tool interacts with external systems |
Tools can be async for non-blocking I/O operations:
import httpx
@server.tool()
async def fetch_url(
url: Annotated[str, Field(description="The URL to fetch content from")],
) -> str:
"""Fetch content from a URL."""
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.text
Using Context
Access the MCP context for logging and progress reporting. Add a Context parameter — it’s automatically excluded from the tool’s input schema:
from mcp_use.server import Context
@server.tool()
async def long_task(
items: Annotated[list[str], Field(description="Items to process")],
context: Context,
) -> str:
"""Process items with progress reporting."""
results = []
for i, item in enumerate(items):
await context.report_progress(i, len(items))
results.append(f"Processed: {item}")
return "\n".join(results)
Use Pydantic models for complex inputs:
from pydantic import BaseModel
class SearchQuery(BaseModel):
query: str = Field(description="The search query string")
max_results: int = Field(default=10, description="Maximum number of results to return")
include_metadata: bool = Field(default=False, description="Whether to include metadata")
@server.tool()
def search(params: SearchQuery) -> list[str]:
"""Search with complex parameters."""
return ["result1", "result2"]
Return Types
Tools can return various types:
# String
@server.tool()
def text_tool() -> str:
return "Hello"
# Dict (serialized to JSON)
@server.tool()
def json_tool() -> dict:
return {"key": "value", "count": 42}
# List
@server.tool()
def list_tool() -> list[str]:
return ["a", "b", "c"]
Error Handling
Raise exceptions to indicate errors. The error message will be returned to the client:
@server.tool()
def divide(
a: Annotated[float, Field(description="Dividend")],
b: Annotated[float, Field(description="Divisor")],
) -> float:
"""Divide two numbers."""
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b