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