All posts

November 3, 20267 min readaibrowsertutorial

How to Give Claude (or Any AI Agent) a Real Web Browser

Three ways to let Claude drive a browser — an MCP server, Claude's own browser toolset, or a hosted agent run — with working code and what breaks in practice.

By Scrappy Team

The short answer: Claude cannot open a web page by itself. Something has to run a browser, carry out the actions Claude asks for, and send back what the page shows. You can do that three ways: connect an MCP server that has browser tools, run Claude's own browser toolset in your agent loop and execute its calls against a browser, or hand the whole task to a hosted service that runs the loop for you. All three need a browser that sites do not block, which is the harder half of the problem.

This post covers the three options in working code, all against Scrappy's cloud browser, and what we learned building the part that executes Claude's calls.

First, which browser?

The obvious browser is the one on your own machine, and for trying things out it is fine. Claude in Chrome drives your own Chrome, and a local Playwright MCP server drives a browser on your laptop. Both are free and fast to set up.

They stop being enough when the agent has to run without you: overnight, on a server, or for a teammate. Run on a server, Chrome is usually headless and on a datacenter IP, and both are easy for a site to recognise. That is the subject of Headless Chrome is why you're blocked. For agent work, the browser should be:

  • real, headful Chrome, not headless
  • on a residential connection, not a datacenter one
  • able to keep a login between runs, from the same IP
  • watchable, with a way for a person to take over when a site asks for something only a person can give

That is what a Scrappy session is. A new account gets 100 free credits; a session costs 5 credits to start and 1 per minute.

Option 1: an MCP server (any client, any model)

This is the least work. In Claude Code:

claude mcp add --transport http scrappy https://scrappy.gg/api/mcp \
  --header "Authorization: Bearer sk_..."

The key needs the browser:read and browser:write scopes. Claude then has 62 browser_* tools: create a session, navigate, read the page as an accessibility tree, click, type, upload, download, record, take a screenshot, save a login and release the session. Any client that speaks MCP over Streamable HTTP connects the same way, and the model can be whatever your client runs. The configuration for clients that take JSON is in examples/mcp.

Choose this when an agent you already have needs a browser. Choosing an MCP server for browser automation covers what to look for in one.

Option 2: Claude's browser toolset in your own loop

Claude 5 models can use a client toolset for the browser (browser_toolset_20260801). Claude emits calls such as navigate, left_click, read_page and screenshot. Your code executes them and returns results in an exact format. Executing them is the hard part, so we built an endpoint that does it. Your loop forwards the assistant's content and returns what comes back:

import Anthropic from "@anthropic-ai/sdk";

const anthropic = new Anthropic();
const API = "https://scrappy.gg/api/browser";
const H = { Authorization: `Bearer ${process.env.SCRAPPY_API_KEY}`, "Content-Type": "application/json" };

const { session } = await fetch(`${API}/sessions`, { method: "POST", headers: H, body: "{}" }).then((r) => r.json());

const tools = [{ type: "browser_toolset_20260801" }];
const messages: any[] = [{ role: "user", content: "Open example.com/docs and tell me how to get started." }];
try {
  for (;;) {
    const res = await anthropic.messages.create({ model: "claude-sonnet-5-5", max_tokens: 4096, tools, messages } as any);
    messages.push({ role: "assistant", content: res.content });
    if (res.stop_reason !== "tool_use") break;
    const out = await fetch(`${API}/sessions/${session.id}/claude-tools`, {
      method: "POST", headers: H, body: JSON.stringify({ content: res.content }),
    }).then((r) => r.json());
    messages.push({ role: "user", content: out.data.tool_results });
  }
} finally {
  await fetch(`${API}/sessions/${session.id}?capture=false`, { method: "DELETE", headers: H });
}

The complete runnable version is 04-claude-browser-tool.ts.

Choose this when you own the agent and want control: your prompt, your other tools beside the browser, and the conversation in your hands.

What executing Claude's calls actually involves

The format is strict, and a real browser does things a spec does not mention. A few of the things we had to get right:

  • Every call gets an answer, even when something failed. If a turn sends five calls and the second fails, the remaining three still need results. Their text has to say they were not run, because the API rejects a conversation that leaves a call unanswered. A turn also has a 90-second budget and a cap of 50 calls, and anything past either limit gets an error result, never an HTTP error.
  • Native dialogs block the tab. A confirm() stops the page until someone answers it, and to an agent that looks exactly like a frozen tab: every call times out. The executor answers dialogs by a policy you set (dismiss by default) and says so in the result. The dialog that looks like a frozen tab is the story of how we learned that.
  • Popups arrive late. A tab opened by window.open appears after the click that caused it has returned. So "which tabs are new" is measured against what Claude was last told, not against one call's before and after.
  • Screenshots are scaled. Claude's click coordinates are in the scaled image's frame, and the executor scales them back before clicking.

Keeping a person in charge

Two options matter once the agent acts on real accounts:

  • approval holds chosen actions (all clicks and typing, all navigation, or specific members) until a person approves each one. The response carries the pending call. You show it to someone, then post the same turn again with their decision.
  • allowedHosts keeps the agent on the sites you name. It is enforced after every call, not only on navigate, so a link that opens another site in a new tab is closed.
{ "content": [...], "options": { "approval": { "members": ["input"] }, "allowedHosts": ["portal.example.com"] } }

Option 3: a hosted agent run (no loop at all)

If you would rather not run a loop, hand over the task:

curl -X POST https://scrappy.gg/api/browser/agent/runs \
  -H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
  -d '{"sessionId":"<session id>","task":"Find the pricing page and record the plan names","startUrl":"https://example.com"}'

Claude drives the session you created on the live site until the task is done. Navigation is locked to the start URL's host unless you pass allowedHosts. The run ends by recording a playbook: preconditions, the steps as performed (targets named as a person would see them), pitfalls, and how to check the result. The next attempt, whether by a script or by an agent, starts from what worked. Runs are billed per 1,000 tokens at the AI chat rate, plus the session's own time. They are never retried automatically, because their clicks already happened on a real site.

Choose this when the task matters more than the agent: you want the result and a record of how it was done.

When the site asks for a person

However good the model, some pages need a human: a texted two-factor code, a press-and-hold challenge, a decision you do not want an agent to make alone. Every session has a live viewer link you can send to the right person. It works on a phone, they act in the same browser the agent is using, and the agent carries on afterwards. Hand a stuck browser agent to a human has the pattern in code.

Which to pick

  • You already have an agent: the MCP server.
  • You are building the agent around Claude: the toolset through /claude-tools.
  • You want a task done and a record of how: an agent run.

All three use the same sessions, saved logins and viewer, so you can switch later without changing anything else. Guides: Claude integration, Claude's browser tool and the MCP server.

Keep reading