{
  "object": "doc",
  "title": "How do I give Claude a Cartroute search tool?",
  "description": "A complete Python example using the Anthropic SDK tool runner.",
  "section": "Agents and integrations",
  "url": "https://cart-route.com/docs/guides/claude",
  "updated": "2026-09-23",
  "prev": "https://cart-route.com/docs/mcp",
  "next": "https://cart-route.com/docs/guides/function-calling",
  "text": "Two ways: connect Claude Code or Claude Desktop to the [Cartroute MCP server](https://cart-route.com/docs/mcp) 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.\n\n## What do I need?\n\n```\npip install anthropic requests\nexport ANTHROPIC_API_KEY=\"sk-ant-...\"\nexport CARTROUTE_API_KEY=\"cr_live_...\"\n```\n\n## How do I define the tool with the tool runner?\n\nThe 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.\n\n```\nimport json\nimport os\n\nimport anthropic\nimport requests\nfrom anthropic import beta_tool\n\nCARTROUTE = \"https://cart-route.com/api/v1\"\nHEADERS = {\"Authorization\": f\"Bearer {os.environ['CARTROUTE_API_KEY']}\"}\n\nclient = anthropic.Anthropic()\n\n@beta_tool\ndef search_products(query: str, max_price: float | None = None, in_stock: bool = True,\n                    include_membership: bool = True) -> str:\n    \"\"\"Search 12 US retailers for a product and return every offer.\n\n    Each offer has price_usd, shipping_usd, the best usable coupon and\n    effective_price_usd (what the shopper pays before tax). Rank on\n    effective_price_usd. price_insight.verdict says whether today's price is a\n    90-day low.\n\n    Args:\n        query: What to find, in plain words, e.g. \"sony noise cancelling headphones\".\n        max_price: Optional maximum effective price in USD.\n        in_stock: Only return offers that are in stock.\n        include_membership: Include Costco and Sam's Club, which need a paid membership.\n    \"\"\"\n    params = {\"q\": query, \"limit\": 3, \"in_stock\": str(in_stock).lower(),\n              \"include_membership\": str(include_membership).lower()}\n    if max_price is not None:\n        params[\"max_price\"] = max_price\n    resp = requests.get(f\"{CARTROUTE}/search\", params=params, headers=HEADERS, timeout=20)\n    # Return errors to Claude as text so it can explain them (e.g. out of credits).\n    return json.dumps(resp.json())\n\nrunner = client.beta.messages.tool_runner(\n    model=\"claude-opus-5\",\n    max_tokens=16000,\n    tools=[search_products],\n    messages=[{\n        \"role\": \"user\",\n        \"content\": \"Where should I buy Sony WH-1000XM6 headphones? I don't have a Costco membership.\",\n    }],\n)\n\nfor message in runner:\n    for block in message.content:\n        if block.type == \"text\":\n            print(block.text)\n```\n\nClaude reads the docstring as the tool description, so the facts that matter for ranking (effective price, membership) are stated there.\n\n## How do I write the loop myself, with refusal fallbacks?\n\nIf 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.\n\n```\nimport json\nimport os\n\nimport anthropic\nimport requests\n\nCARTROUTE = \"https://cart-route.com/api/v1\"\nHEADERS = {\"Authorization\": f\"Bearer {os.environ['CARTROUTE_API_KEY']}\"}\nclient = anthropic.Anthropic()\n\ntools = [{\n    \"name\": \"search_products\",\n    \"description\": (\n        \"Search 12 US retailers for a product. Returns every offer with \"\n        \"effective_price_usd (price + shipping - best usable coupon). Rank on it.\"\n    ),\n    \"input_schema\": {\n        \"type\": \"object\",\n        \"properties\": {\n            \"query\": {\"type\": \"string\", \"description\": \"What to find, in plain words.\"},\n            \"max_price\": {\"type\": \"number\", \"description\": \"Maximum effective price in USD.\"},\n            \"include_membership\": {\"type\": \"boolean\", \"description\": \"Include Costco and Sam's Club.\"},\n        },\n        \"required\": [\"query\"],\n    },\n}]\n\ndef run_tool(name, args):\n    if name != \"search_products\":\n        return json.dumps({\"error\": f\"unknown tool {name}\"}), True\n    params = {\"q\": args[\"query\"], \"limit\": 3,\n              \"include_membership\": str(args.get(\"include_membership\", True)).lower()}\n    if \"max_price\" in args:\n        params[\"max_price\"] = args[\"max_price\"]\n    resp = requests.get(f\"{CARTROUTE}/search\", params=params, headers=HEADERS, timeout=20)\n    return json.dumps(resp.json()), not resp.ok\n\nmessages = [{\"role\": \"user\", \"content\": \"Find me a 65 inch OLED TV under $2,000.\"}]\n\nwhile True:\n    response = client.beta.messages.create(\n        model=\"claude-opus-5\",\n        max_tokens=16000,\n        betas=[\"server-side-fallback-2026-07-01\"],\n        fallbacks=\"default\",\n        tools=tools,\n        messages=messages,\n    )\n    if response.stop_reason == \"refusal\":\n        print(\"The request was declined.\")\n        break\n    if response.stop_reason != \"tool_use\":\n        break\n\n    messages.append({\"role\": \"assistant\", \"content\": response.content})\n    results = []\n    for block in response.content:\n        if block.type == \"tool_use\":\n            output, is_error = run_tool(block.name, block.input)\n            results.append({\"type\": \"tool_result\", \"tool_use_id\": block.id,\n                            \"content\": output, \"is_error\": is_error})\n    # All results for one turn go back in a single user message.\n    messages.append({\"role\": \"user\", \"content\": results})\n\nprint(next((b.text for b in response.content if b.type == \"text\"), \"\"))\n```\n\n## What should the tool return to Claude?\n\nThe 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.\n\n## How do I keep costs down?\n\n- Ask for `limit: 3`; the first results are the best matches.\n\n- Keep the tool definitions and system prompt identical between requests so Anthropic's prompt caching can reuse them.\n\n- Each tool call is one Cartroute credit; the [`/account`](https://cart-route.com/docs/api/account) endpoint is free if the agent should check its budget first."
}