Skip to content
sobolewski.proAI today, every eveningSubscribe

Once a day. Unsubscribe with one click. Privacy policy

How to build an AI agent in Python: Claude Agent SDK step by step

How to build an AI agent that reads and fixes code on its own: install the Claude Agent SDK, write your first agent, add a custom tool and MCP. Step by step.

9 min read

This article in other languages:العربيةDeutschEspañolFrançaisItaliano日本語Polski简体中文

In this article
  1. What is an AI agent from a developer’s point of view?
  2. Step 1: how to install the Claude Agent SDK?
  3. Step 2: how to set the API key?
  4. Step 3: how to write your first AI agent?
  5. Step 4: how to control what the agent can do on its own?
  6. Step 5: how to give the agent a custom tool?
  7. Step 6: how to connect the agent to an MCP server?
  8. What’s next after your first agent?
  9. What to watch out for?
  10. FAQ

How do you build an AI agent without writing the whole loop from scratch? The shortest route I know is the Claude Agent SDK, a library for Python and TypeScript that gives you the same tools, agent loop and context management that Claude Code runs on. You write a dozen or so lines of code, and the agent reads files, fixes bugs and runs commands on its own.

In this tutorial we build a first agent in Python, set its permissions, and add a custom tool and an MCP server. All commands and code snippets come from the official Agent SDK quickstart and the documentation pages I link along the way.

What is an AI agent from a developer’s point of view?

According to the documentation, an agent is an application that carries out a task by planning its own next steps and calling tools: it reads files, runs commands or edits code. I covered the broader picture, without code, in What is an AI agent.

For a developer, the key part is the loop: the model thinks, calls a tool, looks at the result and decides what to do next. The Agent SDK takes over that loop, tool execution, context management and retries, and all we do is read the message stream.

The documentation distinguishes four ways of working with Claude:

  1. Agent SDK, a library that runs the Claude Code agent inside your application (in a process you manage yourself),
  2. Claude Code CLI, the terminal for everyday, interactive work (see my Claude Code tutorial),
  3. Client SDK, direct access to the Claude API, where you write the tool loop yourself (or hand it to the “tool runner”, in beta),
  4. Managed Agents, an agent hosted by Anthropic and configured through the API.

This tutorial is about the first option.

Step 1: how to install the Claude Agent SDK?

You need Python 3.10+ (or Node.js 18.14+ for TypeScript) and a Claude Console account. First, create a project folder:

mkdir my-agent
cd my-agent

By default the agent has access to files in this directory and its subdirectories. The documentation recommends the uv package manager:

uv init
uv add claude-agent-sdk

With plain pip (macOS and Linux) it looks like this:

python3 -m venv .venv
source .venv/bin/activate
pip install claude-agent-sdk

Interestingly, you usually don’t need a separate Claude Code install: both SDKs bundle a native Claude Code binary. The exception is platforms without a prebuilt package, such as Windows ARM64 (then you install Claude Code manually and the SDK finds it on your PATH).

Step 2: how to set the API key?

Create a key in the Claude Console and set it as an environment variable in the terminal where you’ll run the agent:

export ANTHROPIC_API_KEY=your-api-key

The SDK doesn’t load .env files automatically, so if you keep the key in .env, you have to load it yourself (for example with the dotenv package). Instead of an Anthropic key you can also use Amazon Bedrock, Claude Platform on AWS, Google Cloud or Microsoft Foundry by setting the right variable, e.g. CLAUDE_CODE_USE_BEDROCK=1.

One caveat from the documentation: without prior approval, Anthropic doesn’t allow products built on the Agent SDK to offer claude.ai login or to use subscription limits. The agent runs on an API key and you pay for tokens (I wrote about tracking them in Claude API costs).

Step 3: how to write your first AI agent?

The quickstart example is an agent that finds and fixes bugs in code. First, create a utils.py file with two deliberate bugs:

def calculate_average(numbers):
    total = 0
    for num in numbers:
        total += num
    return total / len(numbers)


def get_user_name(user):
    return user["name"].upper()

calculate_average([]) will crash with a division by zero, and get_user_name(None) with a TypeError. Now the agent itself, the agent.py file:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, AssistantMessage, ResultMessage


async def main():
    # Agentic loop: streams messages as Claude works
    async for message in query(
        prompt="Review utils.py for bugs that would cause crashes. Fix any issues you find.",
        options=ClaudeAgentOptions(
            allowed_tools=["Read", "Edit", "Glob"],  # Auto-approve these tools
            permission_mode="acceptEdits",  # Auto-approve file edits
        ),
    ):
        # Print human-readable output
        if isinstance(message, AssistantMessage):
            for block in message.content:
                if hasattr(block, "text"):
                    print(block.text)  # Claude's reasoning
                elif hasattr(block, "name"):
                    print(f"Tool: {block.name}")  # Tool being called
        elif isinstance(message, ResultMessage):
            print(f"Done: {message.subtype}")  # Final result


asyncio.run(main())

The code has three parts:

  1. query creates the agent loop and returns an async iterator, so you read messages as they arrive with async for,
  2. prompt is the task. Claude decides on its own which tools to use,
  3. options is the configuration: here allowed_tools pre-approves Read, Edit and Glob, and permission_mode="acceptEdits" automatically accepts file changes.

Run it:

uv run agent.py

(or python agent.py in an active venv). The agent prints its reasoning and every tool it calls, and finally Done: success. Afterwards, utils.py handles an empty list and a missing user. The agent read the file, found the edge cases and fixed the code by itself, and we didn’t write a single tool-handling function.

If you see Not logged in or Invalid API key, the ANTHROPIC_API_KEY variable is almost certainly not set in that terminal.

Step 4: how to control what the agent can do on its own?

Tools decide what the agent is capable of. The documentation lists three typical sets:

  1. Read, Glob, Grep: analysis only, no file changes,
  2. Read, Edit, Glob: analysis and code editing,
  3. Read, Edit, Bash, Glob, Grep: full automation with terminal commands.

To let the agent, say, search the web or run tests, add a tool and change the prompt:

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Edit", "Glob", "Bash"], permission_mode="acceptEdits"
)

With Bash enabled, the quickstart suggests the task “Write unit tests for utils.py, run them, and fix any failures”. You can also give the agent a role through system_prompt, for example an experienced Python developer who sticks to PEP 8.

The second lever is the permission mode (permission_mode):

  1. "default" passes every unapproved call to your canUseTool function and denies it if there is none,
  2. "acceptEdits" accepts file edits and simple commands like mkdir or mv in the working directory,
  3. "plan" makes the agent explore the code and prepare a plan first,
  4. "dontAsk" never asks: only what you pre-approved runs, everything else is denied (good for agents with no human at the keyboard),
  5. "auto" runs actions through a classifier that reviews each one and blocks or allows it,
  6. "bypassPermissions" runs everything without asking and, according to the documentation, belongs only in isolated environments such as CI or containers.

The golden rule from the documentation: "acceptEdits" on a developer machine, "bypassPermissions" only where the agent can’t damage anything important. More on why this matters in my guide to AI agent security.

It’s also worth setting limits right away: max_turns caps the number of turns, and max_budget_usd stops the loop once the estimated cost is exceeded. Without them, the agent runs until it decides it’s done, which for open-ended tasks like “improve this project” can take a while ;)

Step 5: how to give the agent a custom tool?

Built-in tools are just the start. You define a custom tool with the @tool decorator (name, description, input schema and function), then wrap it in an MCP server that runs in the same process. The documentation’s example fetches the temperature from the Open-Meteo API, so first add the httpx library:

uv add httpx

The tool definition:

from typing import Annotated, Any
import httpx
from claude_agent_sdk import tool, create_sdk_mcp_server


# Define a tool: name, description, input schema, handler
@tool(
    "get_temperature",
    "Get the current temperature at a location",
    {
        "latitude": Annotated[float, "Latitude coordinate"],
        "longitude": Annotated[float, "Longitude coordinate"],
    },
)
async def get_temperature(args: dict[str, Any]) -> dict[str, Any]:
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": args["latitude"],
                "longitude": args["longitude"],
                "current": "temperature_2m",
                "temperature_unit": "fahrenheit",
            },
        )
        data = response.json()

    # Return a content array - Claude sees this as the tool result
    return {
        "content": [
            {
                "type": "text",
                "text": f"Temperature: {data['current']['temperature_2m']}°F",
            }
        ]
    }


# Wrap the tool in an in-process MCP server
weather_server = create_sdk_mcp_server(
    name="weather",
    version="1.0.0",
    tools=[get_temperature],
)

In the same file, pass the server to the agent. The tool’s full name has the format mcp__{server}__{tool}, and that’s what goes into allowed_tools so it runs without asking for permission:

import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage


async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"weather": weather_server},
        allowed_tools=["mcp__weather__get_temperature"],
    )

    async for message in query(
        prompt="What's the temperature in San Francisco?",
        options=options,
    ):
        # ResultMessage is the final message after all tool calls complete
        if isinstance(message, ResultMessage) and message.subtype == "success":
            print(message.result)


asyncio.run(main())

The agent calls your tool and answers based on the result. Instead of the weather, you can plug in anything: your database, an internal API or a script.

Step 6: how to connect the agent to an MCP server?

Ready-made MCP servers (I covered the protocol itself in What is MCP) go into the mcp_servers option. The documentation’s example connects the agent to the remote Claude Code docs server over HTTP:

options = ClaudeAgentOptions(
    mcp_servers={
        "claude-code-docs": {
            "type": "http",
            "url": "https://code.claude.com/docs/mcp",
        }
    },
    allowed_tools=["mcp__claude-code-docs__*"],
)

The asterisk in allowed_tools approves every tool from that server. With the prompt “Use the docs MCP server to explain what hooks are in Claude Code”, the agent searches the documentation and returns an answer.

What’s next after your first agent?

The Agent SDK also has the rest of Claude Code’s features:

  1. hooks: your own code that runs before or after a tool call,
  2. subagents for separate subtasks,
  3. sessions: the ID from ResultMessage.session_id lets you resume a conversation with full context or fork it,
  4. skills, commands and memory loaded from the .claude/ directory, just like in Claude Code.

Ready-made sample apps (including an email assistant and a research agent) live in the claude-agent-sdk-demos repository.

What to watch out for?

First, costs. Context grows with every turn, and a large file or a chatty command can eat thousands of tokens at once. Static parts (the system prompt, tool definitions) are cached automatically, but set max_budget_usd from day one anyway (the cap can be exceeded by the cost of one response, so leave some headroom).

Second, permissions. An agent with Bash and bypassPermissions on an ordinary laptop is asking for trouble, so keep full automation for containers.

Third, branding. According to the documentation, a product built on the SDK can be called, for example, “Claude Agent” or “ Powered by Claude”, but not “Claude Code”, and it can’t pose as an Anthropic product.

FAQ

How to build an AI agent without writing the loop from scratch?

The simplest way is a library that already has the loop, such as the Claude Agent SDK. Install claude-agent-sdk, set ANTHROPIC_API_KEY and call query() with a prompt and a list of allowed tools. The SDK does the rest: calling tools and managing context.

Is the Claude Agent SDK free?

The library itself costs nothing, but the agent uses the Claude API, so you pay for the tokens you use. Without Anthropic’s approval you can’t connect it to a claude.ai subscription.

How is the Agent SDK different from the Claude API?

With the regular Client SDK (Claude API), you write the tool loop yourself or use the tool runner in beta. The Agent SDK comes with Claude Code’s built-in tools (reading and editing files, commands, web search), permissions, sessions and hooks.

Can you write an agent in TypeScript?

Yes. The package is @anthropic-ai/claude-agent-sdk, it needs Node.js 18.14+, and in the documentation’s examples the options have the same names as in Python, just in camelCase (allowedTools, permissionMode).

  • AI
  • AI agents
  • Claude
  • programming
  • tutorial

AI today, every evening

The day's biggest AI news and new posts from the blog, every evening. Short, with sources.

Once a day. Unsubscribe with one click. Privacy policy