Tool runner and MCP connector

Claude API · Advanced · 13 min

Both remove boilerplate: no hand-written loop, and no glue code to call tools that already exist as MCP servers.

Writing the tool loop by hand is good for learning, but repetitive. The Python SDK has a tool runner (beta) that does it for you.

You write ordinary Python functions with type hints and a docstring, and put @beta_tool above each one. That @ line is a decorator: it wraps the function and builds the JSON Schema from the hints and docstring. Then client.beta.messages.tool_runner(...) calls Claude, runs requested tools, sends results back and repeats until Claude stops asking. Iterate it to see each message, or call .until_done() for the final one. max_iterations caps the number of rounds. Exceptions in your tools are returned to Claude as errors automatically. Write the loop yourself when you need a human to approve calls or custom logging.

MCP (Model Context Protocol) is an open standard for packaging tools as a server that any AI app can connect to. Many services publish MCP servers. The MCP connector (beta) lets the Messages API connect to a remote MCP server itself:

  • mcp_servers=[{"type": "url", "url": "https://...", "name": "..."}] declares the server, with an optional authorization_token.
  • A {"type": "mcp_toolset", "mcp_server_name": "..."} entry in tools exposes its tools, optionally only an allow-list.
  • Call client.beta.messages.create with the beta flag from the docs, for example betas=["mcp-client-2025-11-20"].

Limits: only MCP tools (not prompts or resources), only public HTTPS servers (no local servers), and only connect servers you trust, because their tool results enter Claude's context.

Example

import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()

@beta_tool
def stock_level(sku: str) -> str:
    """Return units in stock for a product.

    Args:
        sku: The product code, e.g. A-100
    """
    return {"A-100": "12", "B-200": "0"}.get(sku, "unknown")

runner = client.beta.messages.tool_runner(
    model="claude-opus-5-5",
    max_tokens=1024,
    max_iterations=5,
    tools=[stock_level],
    messages=[{"role": "user", "content": "Is A-100 or B-200 in stock?"}],
)
final = runner.until_done()
print(next(b.text for b in final.content if b.type == "text"))

Remember

  • @beta_tool + client.beta.messages.tool_runner automate the tool loop
  • MCP connector: mcp_servers + an mcp_toolset tool entry, beta flag required
  • Write your own loop when you need approvals; connect only trusted MCP servers

Watch out for

  • Pointing the MCP connector at a local stdio server; it only reaches public HTTPS servers.

Check yourself

  1. When is a hand-written tool loop better than the tool runner?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: platform.claude.com/docs/en/agents-and-tools/tool-use/tool-runner