From scratch
Ren Okabe6 min read9 views

How to Build an AI Agent With the Claude Agent SDK in Python (2026)

A runnable Python quickstart: install the Claude Agent SDK, stream a run with query(), then give the agent your own tools with the @tool decorator and ClaudeSDKClient. Builds a weather agent that chains two tools.

A dark code editor showing a Python agent script, representing a Claude Agent SDK quickstart
A dark code editor showing a Python agent script, representing a Claude Agent SDK quickstart
On this page

Quick Answer (2026)

To build an AI agent with the Claude Agent SDK in Python, pip install claude-agent-sdk (Python 3.10+), then stream a run with query(prompt=...). To give the agent your own tools, define Python functions with the @tool decorator, register them through create_sdk_mcp_server, and drive the loop with ClaudeSDKClient instead of query(). The SDK owns the plan, tool-call, and result loop; you own only the tool bodies and the system prompt. This tutorial builds a runnable weather agent that chains two of your tools, verified against the SDK reference on July 6, 2026.

Python Anthropic This is the Python companion to our TypeScript quickstart. Same SDK, different runtime, a deliberately different example so you can read both without repetition.

Prerequisites

You need Python 3.10 or newer and an Anthropic API key. The Claude Code CLI is bundled inside the package, so there is nothing else to install for the agent runtime itself.

bash
python3 --version          # 3.10+
pip install claude-agent-sdk httpx anyio
export ANTHROPIC_API_KEY=sk-ant-...   # your key from console.anthropic.com

httpx is only for the example tools below; the SDK does not require it.

The five-line smoke test

Before wiring tools, confirm the SDK talks to the model. query() is an async function that returns an async iterator of message objects. You loop over it and pick out the assistant text.

python
import anyio
from claude_agent_sdk import query, AssistantMessage, TextBlock

async def main():
    async for message in query(prompt="In one sentence, what is an AI agent?"):
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if isinstance(block, TextBlock):
                    print(block.text)

anyio.run(main)

Run it. If you see a sentence print, your key and install are good. Note the shape: the SDK does not hand you a single string, it streams typed messages (AssistantMessage, UserMessage, SystemMessage, ResultMessage) whose .content is a list of blocks (TextBlock, ToolUseBlock, ToolResultBlock). You filter for what you care about.

Why query() is not enough for your own tools

query() is a one-shot generator. It is perfect for a prompt that only needs Claude's built-in tools or no tools at all. But your custom Python functions cannot be attached to a bare query() call. For that you need ClaudeSDKClient, which supports a live session plus in-process tools. That is the real unlock, so the rest of this tutorial uses it.

Define your tools as Python functions

A custom tool is just an async function wrapped in the @tool decorator. The decorator takes a name, a one-line description Claude reads to decide when to call it, and a dict describing the arguments. The function receives a single args dict and returns a content payload.

We will build two tools against the free, key-less Open-Meteo API: one that turns a city name into coordinates, and one that reads the current temperature for those coordinates. Splitting them is intentional; it forces the agent to chain tool calls, which is the behavior worth seeing.

python
import httpx
from claude_agent_sdk import tool

@tool("geocode_city", "Look up the latitude and longitude of a city by name", {"city": str})
async def geocode_city(args):
    async with httpx.AsyncClient(timeout=10) as http:
        r = await http.get(
            "https://geocoding-api.open-meteo.com/v1/search",
            params={"name": args["city"], "count": 1},
        )
        r.raise_for_status()
        results = r.json().get("results")
    if not results:
        return {"content": [{"type": "text", "text": f"No match for '{args['city']}'."}]}
    top = results[0]
    text = f"{top['name']}, {top.get('country', '')}: lat {top['latitude']}, lon {top['longitude']}"
    return {"content": [{"type": "text", "text": text}]}

@tool("get_weather", "Get the current temperature for a latitude and longitude", {"latitude": float, "longitude": float})
async def get_weather(args):
    async with httpx.AsyncClient(timeout=10) as http:
        r = await http.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": args["latitude"],
                "longitude": args["longitude"],
                "current": "temperature_2m",
            },
        )
        r.raise_for_status()
        current = r.json()["current"]
    return {"content": [{"type": "text", "text": f"{current['temperature_2m']} degrees C (as of {current['time']})"}]}

The return shape is a dict with a content list of typed blocks, the same structure the model produces. Keep tool bodies boring: fetch, validate, return text. All the reasoning lives in the model, not in your function.

Register the tools and run the agent

create_sdk_mcp_server bundles your functions into an in-process MCP server. It runs inside your Python process, so there is no subprocess and no IPC overhead. You pass it to ClaudeAgentOptions and pre-approve each tool in allowed_tools.

python
from claude_agent_sdk import (
    create_sdk_mcp_server, ClaudeAgentOptions, ClaudeSDKClient,
    AssistantMessage, TextBlock, ResultMessage,
)

weather_server = create_sdk_mcp_server(
    name="weather",
    version="1.0.0",
    tools=[geocode_city, get_weather],
)

options = ClaudeAgentOptions(
    system_prompt="You are a weather assistant. Use the tools to answer. Never guess coordinates.",
    mcp_servers={"weather": weather_server},
    allowed_tools=["mcp__weather__geocode_city", "mcp__weather__get_weather"],
    max_turns=6,
    model="sonnet",
)

async def main():
    async with ClaudeSDKClient(options=options) as client:
        await client.query("What is the current temperature in Lisbon?")
        async for message in client.receive_response():
            if isinstance(message, AssistantMessage):
                for block in message.content:
                    if isinstance(block, TextBlock):
                        print(block.text)
            elif isinstance(message, ResultMessage):
                print(f"\n[cost: ${(message.total_cost_usd or 0):.4f}]")

anyio.run(main)

Run it and watch the loop work: the model calls geocode_city("Lisbon"), reads the coordinates back, feeds them into get_weather, and only then writes a sentence. You wrote two small functions; the SDK handled the planning, the two tool calls, and the result assembly.

Notice model="sonnet". Passing the alias rather than a pinned model id means this code will not rot the next time a new model ships. The max_turns=6 cap keeps a confused run from looping forever.

The one gotcha that will bite you

Tool names in allowed_tools follow the pattern mcp____. The middle segment is the KEY you used in the mcp_servers dict, not the name= you passed to create_sdk_mcp_server. We set both to weather here so it lines up, but if your dict key were mcp_servers={"wx": weather_server} the allow entries would be mcp__wx__geocode_city. Mismatch it and the tool silently never gets approved, so Claude answers without ever calling it. When an agent ignores a tool you know exists, check this prefix first.

Reading cost and knowing when to stop

The final ResultMessage carries total_cost_usd, the real dollar cost of the run. Print it during development so a chatty agent does not surprise you. Anthropic publishes current per-token rates on its pricing page (2026); the SDK does the arithmetic for you and reports the total, which is more honest than estimating from token counts.

Python or TypeScript, and SDK or hand-rolled

The Python and TypeScript SDKs expose the same shape: query() for one-shot runs, a client plus in-process MCP server for custom tools. Pick the runtime your app already lives in. If you are still deciding whether to adopt the SDK at all instead of writing the tool-call loop yourself, we break that tradeoff down honestly in SDK vs writing the loop yourself. For a non-Anthropic point of comparison, the OpenAI Agents SDK for Python solves the same problem with a different tool and handoff model.

Limitations and open questions

  1. Auth is environment-bound. The bundled CLI reads ANTHROPIC_API_KEY from the environment. In a container or CI you must inject it; there is no argument to pass a key inline in the snippets above.
  2. In-process tools share your process. No subprocess isolation means a tool that blocks the event loop or leaks memory takes your app down with it. Keep tool bodies fast and non-blocking, hence the async httpx calls.
  3. allowed_tools is approval, not availability. Listing a tool auto-approves it; it does not restrict Claude to only your tools. Built-in tools still exist unless you constrain them with disallowed_tools.
  4. Coordinate trust. The system prompt tells the model never to guess coordinates, but nothing enforces it. For production, validate the latitude and longitude range inside get_weather rather than trusting the model to always geocode first.
  5. Cost visibility is per-run, not per-tool. total_cost_usd is the whole loop. Attributing spend to a specific tool or turn still needs your own instrumentation.

Sources

Ren Okabe

Written by

Ren Okabe

Ren builds agent infrastructure and writes copy-paste tutorials for engineers shipping LLM tool-use systems.

Frequently asked questions

What Python version does the Claude Agent SDK need?

Python 3.10 or newer. Install it with pip install claude-agent-sdk. The Claude Code CLI is bundled inside the package, so there is no separate CLI install.

What is the difference between query() and ClaudeSDKClient?

query() is a one-shot async iterator, ideal when you only need built-in tools or no tools. ClaudeSDKClient supports a live session plus your own in-process tools and hooks. Custom @tool functions require ClaudeSDKClient.

How do I give a Claude agent my own tools in Python?

Wrap async Python functions with the @tool decorator, bundle them with create_sdk_mcp_server, pass that server in ClaudeAgentOptions.mcp_servers, and pre-approve each tool in allowed_tools. Then drive the run with ClaudeSDKClient.

Why is my custom tool never called?

Most often the allowed_tools name is wrong. The pattern is mcp____, where the middle segment is the key you used in the mcp_servers dict, not the name passed to create_sdk_mcp_server. A mismatch means the tool is never approved and Claude answers without it.

Which model should I pin in the code?

Use an alias such as model="sonnet" rather than a pinned model id. The alias resolves to the current model, so your tutorial code does not go stale when a new model ships.

How do I see what a run cost?

The final ResultMessage carries total_cost_usd, the real dollar cost of the whole loop. Print it during development. Anthropic publishes current per-token rates on its pricing page for 2026.