Add live web search to a Claude app with the Anthropic SDK

Superhighway guides

Anthropic's SDK lets you give Claude external tools using client.messages.create(tools=[...]). This guide shows how to add Superhighway's web search as a tool — no agent framework, just the anthropic library. The Anthropic format differs from OpenAI's: tools carry an input_schema (not parameters), responses come back with stop_reason: "tool_use" and content blocks of type "tool_use", and tool results go back as a user message with tool_result content blocks. Claude decides when a question needs fresh data, calls your web_search tool, and writes its answer from the live results.

1. Install

pip install anthropic requests

2. Define the tool and run a basic search

Declare a web_search tool in the Anthropic schema, send a message with it available, and handle the case where Claude returns stop_reason == "tool_use". You run the call against Superhighway's /search endpoint, hand the JSON back inside a tool_result block, and ask Claude again for its grounded answer.

import os, json, requests
import anthropic

client = anthropic.Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
SUPERHIGHWAY_KEY = os.environ["SUPERHIGHWAY_API_KEY"]

tools = [
    {
        "name": "web_search",
        "description": "Search the live web for up-to-date information. Use for current events, recent releases, prices, or anything not in your training data.",
        "input_schema": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "The search query"
                },
                "count": {
                    "type": "integer",
                    "description": "Number of results (default 5, max 10)"
                }
            },
            "required": ["query"]
        }
    }
]

def web_search(query: str, count: int = 5) -> dict:
    r = requests.get(
        "https://superhighway.walls.sh/search",
        params={"q": query, "count": count},
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"},
        timeout=15,
    )
    r.raise_for_status()
    return r.json()

response = client.messages.create(
    model="claude-opus-4-5",
    max_tokens=1024,
    tools=tools,
    messages=[
        {"role": "user", "content": "What are the top AI agent frameworks gaining traction in 2025?"}
    ]
)

# Handle tool use
if response.stop_reason == "tool_use":
    tool_use_block = next(b for b in response.content if b.type == "tool_use")

    result = web_search(**tool_use_block.input)

    response = client.messages.create(
        model="claude-opus-4-5",
        max_tokens=1024,
        tools=tools,
        messages=[
            {"role": "user", "content": "What are the top AI agent frameworks gaining traction in 2025?"},
            {"role": "assistant", "content": response.content},
            {
                "role": "user",
                "content": [
                    {
                        "type": "tool_result",
                        "tool_use_id": tool_use_block.id,
                        "content": json.dumps(result),
                    }
                ]
            }
        ]
    )

print(response.content[0].text)

Claude returns stop_reason == "tool_use" when it wants live data. You run the search, then send the conversation back with the assistant's response.content (which holds the tool_use block) and a user message whose tool_result is linked to it by tool_use_id. The next call returns the grounded reply.

3. Agentic loop (handle multiple tool calls)

A single round-trip works for one search, but Claude may want to search several times before it has enough to answer. Wrap the exchange in a while loop that runs until stop_reason is no longer "tool_use":

def run_with_search(user_message: str) -> str:
    messages = [{"role": "user", "content": user_message}]

    while True:
        response = client.messages.create(
            model="claude-opus-4-5",
            max_tokens=2048,
            tools=tools,
            messages=messages,
        )

        if response.stop_reason != "tool_use":
            return response.content[0].text

        # Collect all tool calls in this response
        messages.append({"role": "assistant", "content": response.content})

        tool_results = []
        for block in response.content:
            if block.type == "tool_use":
                result = web_search(**block.input)
                tool_results.append({
                    "type": "tool_result",
                    "tool_use_id": block.id,
                    "content": json.dumps(result),
                })

        messages.append({"role": "user", "content": tool_results})

print(run_with_search("Summarize the latest news about AI regulations in Europe."))

Looping over response.content handles the case where Claude requests several tools in one turn — append the assistant message once, then one tool_result per tool_use block (all in a single user message) before looping back to the API.

4. Add news and scrape tools

The same pattern scales to the rest of the Superhighway suite. Add news_search and scrape_page to the tools list and route each call by name:

tools = [
    {
        "name": "web_search",
        "description": "Search the live web for information.",
        "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
    },
    {
        "name": "news_search",
        "description": "Search for recent news articles on a topic.",
        "input_schema": {"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}
    },
    {
        "name": "scrape_page",
        "description": "Fetch the full content of a URL as clean text.",
        "input_schema": {"type": "object", "properties": {"url": {"type": "string"}}, "required": ["url"]}
    },
]

def dispatch(name: str, args: dict) -> dict:
    endpoint = {"web_search": "search", "news_search": "news", "scrape_page": "scrape"}.get(name)
    param_key = "url" if name == "scrape_page" else "q"
    param_val = args.get("url") if name == "scrape_page" else args.get("query")
    r = requests.get(
        f"https://superhighway.walls.sh/{endpoint}",
        params={param_key: param_val},
        headers={"Authorization": f"Bearer {SUPERHIGHWAY_KEY}"},
        timeout=30,
    )
    r.raise_for_status()
    return r.json()

In the agentic loop from section 3, replace web_search(**block.input) with dispatch(block.name, block.input) — every tool_use block carries the tool's name, so one handler covers all three.

5. Anthropic SDK vs MCP vs x402

ApproachBest forWhat you need
Anthropic SDK tool_use (this guide)Direct Claude apps, custom logicanthropic + API key
Claude Code MCPAdd web search to Claude Codenpx
x402 pay-per-callAutonomous agents, no API key managementBase wallet + USDC

Get your API key at /pricing (free tier: 1,000 calls/month). For adding web search to Claude Code directly, see the MCP setup guide.