Add live web search to a Claude app with the Anthropic SDK
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
| Approach | Best for | What you need |
|---|---|---|
| Anthropic SDK tool_use (this guide) | Direct Claude apps, custom logic | anthropic + API key |
| Claude Code MCP | Add web search to Claude Code | npx |
| x402 pay-per-call | Autonomous agents, no API key management | Base 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.