Claude Agent SDK: Custom Tools with In-Process MCP

Claude Agent SDK: Custom Tools with In-Process MCP

K
Kodetra Technologies·April 30, 2026·4 min read Intermediate

Summary

Add custom Python tools to Claude agents with one decorator. No server. No HTTP. Just code.

Most agent frameworks make tool-calling sound complex: spin up an MCP server, expose HTTP endpoints, wire up a transport, then connect a client. The Claude Agent SDK for Python flips that. You define a tool with a decorator, register it with one helper, and your agent calls Python functions directly — no separate process, no network hop.

This guide walks through building a working agent with two custom in-process tools. By the end, you will know how to expose any Python function to Claude, validate inputs with type hints, and ship the whole thing in a single file.

Why this matters now

In April 2026 the Claude Agent SDK shipped first-class support for in-process MCP via create_sdk_mcp_server(). That removed the biggest friction in agent development: managing a fleet of small tool servers. For internal tools, batch jobs, and CLIs, you should default to in-process unless you actually need cross-process sharing.

Prerequisites

  • Python 3.10 or newer
  • An Anthropic API key in ANTHROPIC_API_KEY
  • pip install claude-agent-sdk (0.6.0+)
  • Basic familiarity with async/await

Step 1 — Project setup

Create a fresh virtual environment and install the SDK.

python -m venv .venv
source .venv/bin/activate
pip install --upgrade claude-agent-sdk
export ANTHROPIC_API_KEY=sk-ant-...

Step 2 — Define a tool with @tool

Tools are plain async Python functions. The @tool decorator pulls metadata from your name, docstring, and an explicit input schema. Return a dict that matches the MCP content shape.

from claude_agent_sdk import tool

@tool(
    "word_count",
    "Count words and characters in a piece of text.",
    {"text": str},
)
async def word_count(args: dict) -> dict:
    text = args["text"]
    return {
        "content": [{
            "type": "text",
            "text": f"words={len(text.split())} chars={len(text)}",
        }]
    }

Three things to notice. First, the schema is just {"text": str} — the SDK turns that into JSON schema for you. Second, the function is async, which lets it call HTTP APIs without blocking. Third, the return shape is a list of MCP content blocks, not a raw string.

Step 3 — Add a tool with side effects

In-process tools shine when they touch your local filesystem or call internal services. Here is a tool that appends a line to a log file.

import datetime
from pathlib import Path

LOG = Path("agent.log")

@tool(
    "log_event",
    "Append a timestamped line to agent.log.",
    {"event": str, "level": str},
)
async def log_event(args: dict) -> dict:
    ts = datetime.datetime.utcnow().isoformat(timespec="seconds")
    line = f"{ts} [{args.get('level','info').upper()}] {args['event']}\n"
    LOG.write_text((LOG.read_text() if LOG.exists() else "") + line)
    return {"content": [{"type": "text", "text": f"logged: {line.strip()}"}]}

Step 4 — Bundle tools into an SDK MCP server

create_sdk_mcp_server() wraps a list of tools into an MCP server that runs inside your process. No socket, no port, no startup script.

from claude_agent_sdk import create_sdk_mcp_server

tools_server = create_sdk_mcp_server(
    name="my-tools",
    version="0.1.0",
    tools=[word_count, log_event],
)

Step 5 — Run the agent loop

Give the agent your server in mcp_servers, list the tool names you want it allowed to call, and stream a query. The SDK handles the agent loop, message back-and-forth, and tool dispatch.

import anyio
from claude_agent_sdk import ClaudeAgentOptions, query

async def main():
    options = ClaudeAgentOptions(
        mcp_servers={"local": tools_server},
        allowed_tools=["mcp__local__word_count", "mcp__local__log_event"],
        system_prompt="You are a precise assistant. Use tools instead of guessing.",
    )
    prompt = (
        "Count the words in 'Custom tools should be boring and obvious.' "
        "Then log the result with level=info."
    )
    async for msg in query(prompt=prompt, options=options):
        print(msg)

anyio.run(main)

Example output

AssistantMessage(role=assistant, content=[ToolUseBlock(name=mcp__local__word_count, input={'text': 'Custom tools should be boring and obvious.'})])
ToolResultMessage(content=[{'type': 'text', 'text': 'words=7 chars=42'}])
AssistantMessage(role=assistant, content=[ToolUseBlock(name=mcp__local__log_event, input={'event': 'word_count words=7 chars=42', 'level': 'info'})])
ToolResultMessage(content=[{'type': 'text', 'text': 'logged: 2026-04-30T03:11:02 [INFO] word_count words=7 chars=42'}])
AssistantMessage(role=assistant, content=[TextBlock(text='The phrase has 7 words (42 characters). Logged.')])

Common pitfalls

  • Forgetting allowed_tools. The SDK ignores tools the agent has not been explicitly allowed to call. The naming convention is mcp__<server-name>__<tool-name>.
  • Returning a string instead of MCP content. Always return {"content": [{"type": "text", "text": ...}]}; raw strings will raise a validation error.
  • Blocking I/O inside an async tool. Wrap CPU-heavy or sync work with anyio.to_thread.run_sync or you will stall the loop.
  • Mutating shared state without locks. In-process means tools share memory with the rest of your app; treat counters, caches, and files like you would in a web handler.

Quick reference

What you wantUse this
Define a tool@tool(name, description, schema)
Bundle toolscreate_sdk_mcp_server(name, version, tools)
Run an agentquery(prompt, options) + async for
Allow a toolallowed_tools=['mcp__server__tool']
Stream outputIterate the async generator from query

Next steps

  • Add a tool that calls an internal HTTP API with httpx.AsyncClient.
  • Pass permission_mode='acceptAll' for a CI-friendly run, or 'plan' to dry-run.
  • Combine in-process tools with a remote MCP server by adding a second entry to mcp_servers.
  • Wrap the whole script in a CLI with typer so teammates can use it without touching the code.

Custom tools used to be the boring middle of agent development. With in-process MCP, they are a one-liner. Build the smallest tool you can today, run it, and watch your agent stop hallucinating answers it can simply look up.

Comments

Subscribe to join the conversation...

Be the first to comment