Skip to main content

How to Build a Web-Browsing AI Assistant with Firecrawl and Claude (2026)

· 7 min read
Yassine El Haddad
Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.

A web-browsing AI assistant: the user asks a question, Claude decides what to search and scrape, Firecrawl fetches content, and Claude synthesizes the answer. Build it with the Anthropic API, Firecrawl Python SDK, and a tool-calling loop. Get Firecrawl for Claude.

Architecture

  1. User asks — e.g. "What are the latest pricing changes at OpenAI?"
  2. Claude decides — Chooses to call scrape_url on openai.com/pricing or search_web for URLs first
  3. Firecrawl executes — Returns markdown (or search results)
  4. Claude synthesizes — Answers with source attribution
  5. Loop — Repeat until Claude has enough context or produces a final answer

No MCP required — a Python script with tool definitions and a message loop suffices. For Claude Desktop integration, use the Firecrawl MCP server.

Tool Definitions

Claude's tool_use supports function-style tools. Define two: scrape_url and search_web.

from anthropic import Anthropic

TOOLS = [
{
"name": "scrape_url",
"description": "Scrape a URL and return its content as markdown. Use when you know the exact URL to fetch.",
"input_schema": {
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "The full URL to scrape (e.g. https://openai.com/pricing)"
}
},
"required": ["url"]
}
},
{
"name": "search_web",
"description": "Search the web for URLs matching a query. Use when you need to find relevant URLs before scraping.",
"input_schema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query (e.g. 'OpenAI pricing 2026')"
}
},
"required": ["query"]
}
}
]

Python Implementation

from anthropic import Anthropic
from firecrawl import Firecrawl
import json

client = Anthropic()
firecrawl = Firecrawl(api_key="fc-YOUR-API-KEY")

def scrape_url(url: str) -> str:
try:
result = firecrawl.scrape(url, formats=["markdown"])
md = result.get("markdown", "")
return md[:15000] if md else "[No content extracted]"
except Exception as e:
return f"[Error: {str(e)}]"

def search_web(query: str) -> str:
try:
result = firecrawl.search(query, limit=5)
links = result.get("data", [])
return json.dumps([l.get("url") for l in links]) if links else "[]"
except Exception as e:
return f"[Error: {str(e)}]"

def run_tool(name: str, args: dict) -> str:
if name == "scrape_url":
return scrape_url(args.get("url", ""))
if name == "search_web":
return search_web(args.get("query", ""))
return "[Unknown tool]"

def ask_assistant(question: str, max_turns: int = 5) -> str:
messages = [{"role": "user", "content": question}]
for _ in range(max_turns):
resp = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
tools=TOOLS,
messages=messages
)
for block in resp.content:
if block.type == "text":
if block.text:
return block.text
if block.type == "tool_use":
result = run_tool(block.name, block.input)
messages.append({
"role": "assistant",
"content": [{"type": "tool_use", "id": block.id, "name": block.name, "input": block.input}]
})
messages.append({
"role": "user",
"content": [{"type": "tool_result", "tool_use_id": block.id, "content": result}]
})
messages.append({"role": "assistant", "content": resp.content})
return "[Max turns reached]"

Example Interaction

User: "What are the latest pricing changes at OpenAI?"

Claude (internal): Calls scrape_url with https://openai.com/pricing.

Firecrawl: Returns markdown of the pricing page.

Claude (output): "Based on the current OpenAI pricing page, here are the latest details: [structured summary with source attribution]. Source: https://openai.com/pricing"

With search_web, Claude can first find URLs before scraping — e.g. "OpenAI pricing 2026" → list of URLs → scrape the top result.

Firecrawl's /search endpoint returns URLs for a query. Use it when Claude doesn't know the exact URL:

# Firecrawl search (check SDK for exact method)
def search_web(query: str) -> str:
result = firecrawl.search(query, limit=5)
# Returns list of { url, title?, description? }
items = result.get("data", [])
return "\n".join(f"- {i.get('url')}" for i in items)

Claude can then call scrape_url on one or more of those URLs. For search availability, check the Firecrawl API tutorial — the /search endpoint may vary by plan.

Error Handling

ScenarioHandling
TimeoutRetry once with exponential backoff. If fail, return "[Timeout]" so Claude can try another URL.
Blocked / 403Return "[Blocked]". Claude may try search for alternative sources.
Empty responseReturn "[No content]". Avoid infinite retries.
Rate limit (429)Back off, retry. Consider queuing requests.
def scrape_url_safe(url: str) -> str:
for attempt in range(2):
try:
result = firecrawl.scrape(url, formats=["markdown"])
md = result.get("markdown", "")
return md[:15000] if md else "[No content extracted]"
except Exception as e:
if "429" in str(e) and attempt < 1:
time.sleep(2)
continue
return f"[Error: {str(e)}]"
return "[Failed after retries]"
AttributeFirecrawl-backed Claude agentClaude built-in web search (Anthropic)
ControlFull control over scrape logicBlack box
URL targetingScrape any URL, any depthSearch-only, limited URLs
CostClaude API + Firecrawl creditsClaude API (bundled or separate)
CustomizationAdd tools (map, crawl, extract)None
Offline / air-gappedCan self-host FirecrawlNo
Best forResearch agents, custom pipelinesQuick answers, general use

Use Firecrawl when you need specific URLs, versioned docs, or custom extraction. Use built-in search when you want the simplest setup for general queries.

Extending with More Tools

You can add crawl_url and map_url tools so Claude can discover and batch-scrape:

{
"name": "crawl_url",
"description": "Crawl a site from a start URL. Returns markdown for discovered pages. Use for full doc sites.",
"input_schema": {
"type": "object",
"properties": {"url": {"type": "string"}, "limit": {"type": "integer", "default": 50}},
"required": ["url"]
}
}

Wire to Firecrawl's crawl endpoint. Claude can then say "Crawl docs.example.com" and get structured content for many pages in one call — useful for building RAG on the fly.

Cost

  • Claude: Per token (input + output). ~$3/1M input, ~$15/1M output for Sonnet.
  • Firecrawl: 1 credit per scrape. Free tier: 500 credits; Hobby: 3,000 credits/mo.
  • Typical research session: 3–5 scrapes + 2–4 Claude exchanges ≈ 5–10 Firecrawl credits, ~2K–5K Claude tokens.
Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50
Start with scrape_url only

Add search_web only if Claude needs URL discovery. Many research flows work with the user providing URLs or Claude inferring them (e.g. openai.com/pricing).



Try Firecrawl | Firecrawl MCP for Claude

Frequently Asked Questions

No. A Python script with Claude tool_use and Firecrawl SDK is enough. MCP is useful when you want Claude Desktop to have web access without writing code.

Firecrawl offers search in some plans. Check the API docs. If not available, use a separate search API (e.g. SerpAPI, Brave) and pass URLs to scrape_url.

Set max_tool_uses in the messages.create call if your Anthropic SDK supports it. Otherwise, cap iterations in your loop (e.g. max 3 tool rounds).

Yes. Add tool definitions for crawl_url and map_url. Wire to Firecrawl crawl and map. Claude can then discover and batch-scrape entire doc sites.

Apify MCP gives Claude access to 2,000+ Actors (scrapers, search). Firecrawl MCP gives scrape, search, crawl, extract. Choose by ecosystem: Apify for Actor variety, Firecrawl for markdown-first workflows.

Firecrawl handles some anti-bot. For hard targets, use Bright Data Scraping Browser or an Apify Actor with residential proxies. See our Bright Data Scraping Browser guide.

Common mistakes and fixes

Claude keeps calling tools in a loop

Add max_tool_uses per turn. Ensure tool results are substantive. Check for empty responses triggering retries.

Scrape returns empty or blocked

Use Firecrawl's built-in proxy. For hard targets, consider Bright Data Scraping Browser or Apify Actors.