How do I give Claude a Cartroute search tool?
Two ways: connect Claude Code or Claude Desktop to the Cartroute MCP server with no code, or, in your own application, define a search_products tool with the Anthropic Python SDK and let the SDK's tool runner call the Cartroute API whenever Claude decides to search. The example below is complete and runs as-is with two environment variables.
What do I need?
pip install anthropic requests export ANTHROPIC_API_KEY="sk-ant-..." export CARTROUTE_API_KEY="cr_live_..."
How do I define the tool with the tool runner?
The tool runner turns a decorated Python function into a tool, sends it to Claude, runs the function whenever Claude calls it, and loops until Claude has a final answer.
import json
import os
import anthropic
import requests
from anthropic import beta_tool
CARTROUTE = "https://cart-route.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}
client = anthropic.Anthropic()
@beta_tool
def search_products(query: str, max_price: float | None = None, in_stock: bool = True,
include_membership: bool = True) -> str:
"""Search 12 US retailers for a product and return every offer.
Each offer has price_usd, shipping_usd, the best usable coupon and
effective_price_usd (what the shopper pays before tax). Rank on
effective_price_usd. price_insight.verdict says whether today's price is a
90-day low.
Args:
query: What to find, in plain words, e.g. "sony noise cancelling headphones".
max_price: Optional maximum effective price in USD.
in_stock: Only return offers that are in stock.
include_membership: Include Costco and Sam's Club, which need a paid membership.
"""
params = {"q": query, "limit": 3, "in_stock": str(in_stock).lower(),
"include_membership": str(include_membership).lower()}
if max_price is not None:
params["max_price"] = max_price
resp = requests.get(f"{CARTROUTE}/search", params=params, headers=HEADERS, timeout=20)
# Return errors to Claude as text so it can explain them (e.g. out of credits).
return json.dumps(resp.json())
runner = client.beta.messages.tool_runner(
model="claude-opus-5",
max_tokens=16000,
tools=[search_products],
messages=[{
"role": "user",
"content": "Where should I buy Sony WH-1000XM6 headphones? I don't have a Costco membership.",
}],
)
for message in runner:
for block in message.content:
if block.type == "text":
print(block.text)
Claude reads the docstring as the tool description, so the facts that matter for ranking (effective price, membership) are stated there.
How do I write the loop myself, with refusal fallbacks?
If you need full control, or want server-side refusal fallbacks on the request, use a manual loop. fallbacks: "default" lets the API re-run a request that Claude Opus 5's safety classifiers decline on Anthropic's recommended fallback model, inside the same call.
import json
import os
import anthropic
import requests
CARTROUTE = "https://cart-route.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CARTROUTE_API_KEY']}"}
client = anthropic.Anthropic()
tools = [{
"name": "search_products",
"description": (
"Search 12 US retailers for a product. Returns every offer with "
"effective_price_usd (price + shipping - best usable coupon). Rank on it."
),
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "What to find, in plain words."},
"max_price": {"type": "number", "description": "Maximum effective price in USD."},
"include_membership": {"type": "boolean", "description": "Include Costco and Sam's Club."},
},
"required": ["query"],
},
}]
def run_tool(name, args):
if name != "search_products":
return json.dumps({"error": f"unknown tool {name}"}), True
params = {"q": args["query"], "limit": 3,
"include_membership": str(args.get("include_membership", True)).lower()}
if "max_price" in args:
params["max_price"] = args["max_price"]
resp = requests.get(f"{CARTROUTE}/search", params=params, headers=HEADERS, timeout=20)
return json.dumps(resp.json()), not resp.ok
messages = [{"role": "user", "content": "Find me a 65 inch OLED TV under $2,000."}]
while True:
response = client.beta.messages.create(
model="claude-opus-5",
max_tokens=16000,
betas=["server-side-fallback-2026-07-01"],
fallbacks="default",
tools=tools,
messages=messages,
)
if response.stop_reason == "refusal":
print("The request was declined.")
break
if response.stop_reason != "tool_use":
break
messages.append({"role": "assistant", "content": response.content})
results = []
for block in response.content:
if block.type == "tool_use":
output, is_error = run_tool(block.name, block.input)
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": output, "is_error": is_error})
# All results for one turn go back in a single user message.
messages.append({"role": "user", "content": results})
print(next((b.text for b in response.content if b.type == "text"), ""))
What should the tool return to Claude?
The Cartroute JSON as-is. It is compact, every money field is labelled _usd, and price_insight.reason is written to be quoted. On errors, return the problem body with is_error: true; its detail is written for the model to relay, for example that the account is out of credits.
How do I keep costs down?
- Ask for
limit: 3; the first results are the best matches. - Keep the tool definitions and system prompt identical between requests so Anthropic's prompt caching can reuse them.
- Each tool call is one Cartroute credit; the
/accountendpoint is free if the agent should check its budget first.