Give an agent address tools with the Anthropic API

About 15 minutes. Updated 11 September 2026.

Two ways to let Claude validate and geocode Australian addresses from your own code: point the Messages API at the Locio MCP server and write no tool loop at all, or define the tools yourself over the REST API with the SDK's tool runner.

There are two ways to give Claude the address API from your own code, and they suit different situations. The MCP connector is the shortest: the Messages API connects to the Locio server itself and you write no tool loop. Defining the tools yourself is the one to pick when the agent's other tools are already in your code, or when you want to see and shape every call.

Both use a secret key from your Keys tab, read from the environment, and both are metered exactly like the REST calls they make: resolve is one unit, similar is three.

01The MCP connector: no loop to write

Two parameters go together: mcp_servers names the server and carries the key as its authorization token, and tools must include an mcp_toolset that refers to it by name. The beta header makes the connector available. Anthropic makes the MCP connection server side and runs the tool calls; you get the final answer.

connector.pypython
# pip install anthropic
import os
import anthropic

client = anthropic.Anthropic()   # reads ANTHROPIC_API_KEY

response = client.beta.messages.create(
    model="claude-opus-5",
    max_tokens=16000,
    betas=["mcp-client-2025-11-20", "server-side-fallback-2026-07-01"],
    fallbacks="default",          # a safety refusal is re-served by a fallback model
    mcp_servers=[{
        "type": "url",
        "url": "https://api.locio.com.au/mcp",
        "name": "locio",
        "authorization_token": os.environ["LOCIO_KEY"],   # a secret key
    }],
    tools=[{"type": "mcp_toolset", "mcp_server_name": "locio"}],
    messages=[{
        "role": "user",
        "content": "Is '1 gorge rd sydenhum nsw' a real address? If not, what is the nearest one? Give the G-NAF id.",
    }],
)

for block in response.content:
    if block.type == "text":
        print(block.text)

The samples enable server side refusal fallbacks, so a request a safety classifier declines is re-served by a fallback model in the same call rather than stopping. Leave the two lines out if you would rather handle a refusal stop reason yourself.

02Your own tools over the REST API

A tool is a typed function with a docstring; the SDK turns the signature into the schema and the docstring into the description Claude reads. Write the description the way the reference is written: what the call answers, what to store, and what it costs, because that is what stops an agent calling the three unit tool on every address.

tools.pypython
# tools.py
import os
import requests
from anthropic import beta_tool

API = "https://api.locio.com.au"
HEADERS = {"Authorization": f"Bearer {os.environ['LOCIO_KEY']}"}


@beta_tool
def resolve_address(q: str) -> str:
    """Validate, geocode and parse one Australian address in one call.

    Args:
        q: The whole address as written, e.g. "1 george st sydenham nsw 2044".
    Returns matched (true or false) and, when matched, the address with its
    gnaf_pid, lat, lng and components. Store the gnaf_pid, not the text.
    Costs one unit.
    """
    res = requests.get(f"{API}/v1/addresses/resolve", params={"q": q}, headers=HEADERS, timeout=10)
    return res.text


@beta_tool
def similar_addresses(q: str, limit: int = 5) -> str:
    """The nearest real addresses to one that did not resolve, for a typo.

    Args:
        q: The address as written.
        limit: How many to return, 1 to 10.
    Costs three units, so call it only after resolve_address said matched: false.
    """
    res = requests.get(f"{API}/v1/addresses/similar", params={"q": q, "limit": limit}, headers=HEADERS, timeout=10)
    return res.text

The tool runner drives the loop: it sends the tools, executes whichever Claude calls, feeds the results back and stops when Claude has no more calls to make.

agent.pypython
# agent.py
import anthropic
from tools import resolve_address, similar_addresses

client = anthropic.Anthropic()

runner = client.beta.messages.tool_runner(
    model="claude-opus-5",
    max_tokens=16000,
    betas=["server-side-fallback-2026-07-01"],
    fallbacks="default",
    tools=[resolve_address, similar_addresses],
    messages=[{
        "role": "user",
        "content": "Check these and give me a G-NAF id for each, or the nearest real address: "
                   "'145 sydney rd coburg', '1 gorge rd sydenhum nsw', '12 nowhere st'",
    }],
)

# The runner calls the tools and feeds the results back until Claude is done.
for message in runner:
    for block in message.content:
        if block.type == "text":
            print(block.text)

Which to pick

  • The connector when addresses are the only tools, or when you want the fewest moving parts. Nothing to host, nothing to loop.
  • Your own tools when the agent also reads your database or writes files, when you want to log or gate every call, or when you would rather not send a key to a third party at all.

Either way the answer carries a gnaf_pid. Store that with your record and read the address back later with /v1/addresses/{pid}, in code or from an agent cleaning a whole file.

Related