Skip to content
Documentation

Docs · Interfaces

MCP server

How an AI agent connects to Scalebrowser and operates a real browser profile — seeing the page, clicking, typing, waiting.

The daemon is an MCP server. An agent points its client at it and gets a small set of tools that reserve a profile, open a page and act on it. No remote-control code, no CDP, no JavaScript: the agent reads a text map of the page and addresses elements by reference, and every input it sends goes through the humanization layer on the way to the browser.

This layer does not protect you from a mis-click. It refuses a stale reference, an element that something else covers, and an address outside the public web. That is the whole list. It does not know that “Delete account” is irreversible, and there is no confirmation step, no dry run and no undo. The server states this in the instructions it hands every client on connect, and it is worth repeating here: treat every tool call as final.

Connecting

Two transports, the same tools. Which one you want depends on whether your client speaks HTTP to a server that is already running, or launches its server as a subprocess.

Over HTTP

The daemon serves MCP at /v1/mcp, inside the same bearer-token auth as the rest of the API. It speaks Streamable HTTP — one path, using POST, GET and DELETE — and negotiates the protocol revision with your client, so an older client still works.

json
{
  "mcpServers": {
    "scalebrowser": {
      "type": "http",
      "url": "http://127.0.0.1:8787/v1/mcp",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}

The transport also checks the Host header, which stops a web page on some other origin from driving your loopback daemon through the browser. Loopback and the daemon's own bind address are accepted out of the box; a daemon reachable under a public name needs that name added to [mcp] allowed_hosts in the configuration.

Over stdio

Clients that launch their MCP server as a subprocess use --mcp-stdio. Point it at the same config or data directory the daemon uses; it resolves the token itself, so no credential appears in the client configuration.

json
{
  "mcpServers": {
    "scalebrowser": {
      "command": "scalebrowser-daemon",
      "args": ["--mcp-stdio", "--config", "/etc/scalebrowser/scalebrowser.toml"]
    }
  }
}

What that process does depends on what it finds. If a daemon is already serving that data directory, it becomes a thin relay to it — owning no browser, so your client can restart as often as it likes without taking running profiles down. If nothing is running there, it serves MCP itself. It says which on startup, on standard error: standard output belongs to the protocol, and nothing else is ever written to it.

The relay is the normal case, and it is the safer one. Two daemons on one data directory is exactly what the directory lock exists to prevent, and a stdio process that owned the browsers would kill every running profile each time the client reconnected.

The loop

lease_profileopen_pagesnapshot → act → release_profile.

lease_profile reserves a free profile, starts its browser, binds a tab and returns a handle. Every other tool takes that handle, and no tool takes a profile id — which is what keeps one agent out of another agent's browser. A lease expires on its own (15 minutes by default, and the server clamps whatever a caller asks for), so a crashed agent cannot hold a profile forever. release_profile gives it back early.

open_page navigates and returns the page map. A map is flat lines, not nested JSON:

text
URL: https://x.com/home  |  epoch 3  |  snapshot #7
title: Home
[e_412] textbox "What is happening?"
[e_418] button "Post" (disabled)
[e_431] link "Notifications"
|scroll down: 1.8 screens|

e_418 is what a tool call addresses. The scroll line says how much page is left, so an agent knows whether what it is looking for is below the fold. Then it acts: click, type_text, press_key, scroll, wait_for.

State comes back as a diff

After the first map, every action answers with only what changed — which is what makes a long session affordable in tokens:

text
snapshot #8 (diff from #7, epoch 3)
+ [e_502] alert "Post published"
~ [e_418] button "Post" → disabled
- [e_412] textbox "What is happening?"

A full map comes back instead in four situations, and it always says which:

  • Navigation — the page changed, so every earlier reference is void.
  • A large reflow — more than half the map turned over, and a diff that size is longer than the map itself.
  • Periodically — every few steps by default, so a long session cannot drift away from what is really on screen.
  • On requestsnapshot with mode: "full".

References belong to one document generation, which the epoch counts. After a navigation, a reference from before it is refused as stale_ref rather than resolved against whichever element inherited its position — and the refusal carries a fresh full map, so the agent can carry straight on.

The tool set

Tools are grouped into four profiles, and the daemon decides which are served. That decision comes from its configuration only: no tool argument can widen the catalog, and a tool outside the served set does not exist as far as a client is concerned — it is absent from the listing and refused on call.

core is the default, on its own. Thirteen tools, which keeps it inside the ≤15 a model still selects from accurately; that ceiling is held by a test rather than by discipline.

ToolWhat it does
lease_profileReserve a free profile, start its browser, bind a tab → the handle.
release_profileEnd the reservation and stop the browser.
open_pageNavigate to a public http/https address → the full map.
snapshotWhat is on the page now; a diff, unless you ask for full.
clickClick an element — optionally a point inside it, for a picture rather than a control.
press_and_holdThe whole “press and hold the button” gesture, in one call.
type_textClick a field and type into it at this profile's own cadence.
fill_formSeveral fields in one call.
press_keyEnter, Tab, Space or a single character.
scrollA real wheel gesture with momentum, not a jump.
wait_forWait for visible text, or for a reference to resolve.
screenshotCapture the page or one element, for when the text map cannot describe it.
read_valueRead one thing from an element: text, an allowlisted attribute, a count, or a table as rows.

The other three profiles are opt-in, named in [mcp] profiles:

ProfileTools
extended (4) interact (hover, drag, upload), navigate (history and tabs — this is where a popup a click opened becomes visible), page_report (console messages and a request summary), download_file
management (8) profile.list, profile.create, profile.start, profile.stop, proxy.assign, input.humanize, session.export, session.import
workflows (3) record_workflow, compile_workflow, run_workflow

No profile carries a delete tool, in any configuration. Deleting a profile destroys its browser directory and every login inside it, and that is a confirmation a person gives.

management is the one most people add: an agent with core alone cannot even list a profile, so it can do nothing until a human has prepared one by hand. The Windows desktop client therefore ships ["core", "management"] — 21 tools, knowingly past the ≤15 budget, in exchange for an agent that can set itself up.

toml
[mcp]
profiles = ["core", "management"]
read_only = false
lease_ttl_secs = 900        # default lease lifetime
lease_ttl_max_secs = 3600   # ceiling on what a caller may ask for
snapshot_full_every = 5     # force a full map every N steps; 0 disables

The two important settings also exist as flags — --mcp-profiles core,management and --mcp-read-only — and as the environment variables SCALEBROWSER_MCP_PROFILES and SCALEBROWSER_MCP_READ_ONLY.

Read-only mode

--mcp-read-only removes every tool that changes anything — in the browser, in the store or on disk — from the listing, and refuses it if it is called anyway. The observing tools stay: snapshot, wait_for, screenshot, read_value, scroll.

Both halves matter. Hiding a tool from the listing is not a protection on its own, because a client that cached an earlier listing — or one that simply guesses the name — can still call it. Listing and call gate are the same function here, so they cannot drift apart, and nothing an agent sends can lift the mode: it is read from the daemon's configuration on every single call.

This is the only built-in restraint the layer offers. In normal mode there is none — which is the warning at the top of this page.

What an agent never gets

The boundaries below are structural. They are not filters that could be misconfigured; the path is not there.

  • No CDP endpoint. Over MCP, profile.start returns lifecycle facts and no cdp_ws. An agent that wants to drive a page uses lease_profile, and every input then runs through the humanization layer. There is no way around it from here.
  • No raw HTML and no JavaScript. read_value reads text, an allowlisted attribute, a count, or a table. There is no eval and no html, both of which would hand over hidden form values and tokens. A password field reads back as a mask.
  • No headers, cookies or bodies. page_report reads the page's own resource timeline: origin and path, kind, status, size, timing. That source carries no headers at all, and query strings are dropped.
  • No file paths. An upload takes an artifact id produced by an earlier tool call, never a path. Screenshots and downloads come back as ids for the same reason — they are also far too large to travel in a tool result.
  • No private addresses. open_page accepts public http and https only. Loopback, private ranges and the cloud metadata address are refused, and the check runs twice — once on the address, once on every IP the name resolves to, so a name that looks public and points inward does not get through. An operator with a legitimate internal target allowlists it explicitly under [mcp] url_allowlist.

When a call fails

A failure is a tool result the model can see and reason about, not a protocol error. It carries a code, a sentence, and three fields that answer the only question that matters after a failed action — did it happen?

FieldValuesMeaning
phase precondition · before_effect · effect · observation How far the call got: refused up front, attempted but never sent, sent, or landed with only the reading afterwards failing.
effect none · possible · confirmed · partial What it did to the world.
retryable true · false Whether the agent may repeat the call unprompted.

retryable is derived from effect rather than set by hand, so an action whose outcome is unknown or partial can never be marked retryable — there is no field to get wrong. That is the difference between a connection that dropped after a click and a click that never happened, and therefore the difference between posting once and posting twice. When retryable is false, take a snapshot and look before acting again.

The codes you meet in practice: stale_ref (a reference from a previous page), click_intercepted (something covers the element), url_blocked, lease_expired, handle_invalid, read_only_mode, no_profile_available, effect_unknown and partial_effect.

An unknown argument is an error, not a value that gets dropped. Every published schema forbids extra properties, so a misspelled argument name comes back as a refused call. The alternative was measured and rejected: a silently ignored argument produced an unchanged page, which reads exactly like a page that would not move.

Workflows

The workflows profile lets an agent record a stretch of ordinary work and replay it later. record_workflow brackets the work, compile_workflow turns the recording into a named routine — marking which steps may run in any order, and where a person would stop to read — and run_workflow replays it on a leased profile.

A stored routine deliberately carries no handle, no element reference and no timing. Elements are described by role and accessible name and found again on the page as it is at replay; typed text becomes a named input slot the caller fills; and every pause is drawn fresh from the profile. Two runs of the same routine therefore never share a timeline — a recording that replayed its own timings would be one machine signature repeated on every run.

Next

  • Human input — what the humanization layer actually does to a click and a keystroke.
  • Coherence & proxies — what has to be true before a profile is allowed to start.
  • REST API — the surface underneath, including where /v1/mcp is mounted.
  • Direct CDP — the other way to drive a page, when you write the automation yourself.