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.
{
"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.
{
"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_profile → open_page → snapshot → 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:
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:
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 request —
snapshotwithmode: "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.
| Tool | What it does |
|---|---|
lease_profile | Reserve a free profile, start its browser, bind a tab → the handle. |
release_profile | End the reservation and stop the browser. |
open_page | Navigate to a public http/https address → the full map. |
snapshot | What is on the page now; a diff, unless you ask for full. |
click | Click an element — optionally a point inside it, for a picture rather than a control. |
press_and_hold | The whole “press and hold the button” gesture, in one call. |
type_text | Click a field and type into it at this profile's own cadence. |
fill_form | Several fields in one call. |
press_key | Enter, Tab, Space or a single character. |
scroll | A real wheel gesture with momentum, not a jump. |
wait_for | Wait for visible text, or for a reference to resolve. |
screenshot | Capture the page or one element, for when the text map cannot describe it. |
read_value | Read 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:
| Profile | Tools |
|---|---|
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.
[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.startreturns lifecycle facts and nocdp_ws. An agent that wants to drive a page useslease_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_valuereads text, an allowlisted attribute, a count, or a table. There is noevaland nohtml, 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_reportreads 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_pageaccepts publichttpandhttpsonly. 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?
| Field | Values | Meaning |
|---|---|---|
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/mcpis mounted. - Direct CDP — the other way to drive a page, when you write the automation yourself.