Skip to main content
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}"

Anatomy of a Tool

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:
PythonMCP SchemaNotes
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.

Tool Options

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

Tool annotations provide hints to clients about the tool’s behavior:
AnnotationDescription
destructiveHintTool may modify or delete data
readOnlyHintTool only reads data, no side effects
idempotentHintCalling multiple times has same effect as once
openWorldHintTool interacts with external systems

Async Tools

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)

Complex Input Types

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