MCP server
The MCP tools an AI agent uses to see and operate a page inside a Scalebrowser profile: connecting the server, the tool set, and the page map it works from.
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 over 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 five 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. Eighteen tools, a ceiling held by a test rather than by discipline, because a model picks accurately from a short list and starts guessing from a long one.
| Tool | What it does |
|---|---|
lease_profile | Reserve a free profile, start its browser, bind a tab, and return the handle. |
lease_fleet | The same for several profiles at once, one handle each. |
lease_renew | Push the reservation's deadline out without losing the handle. |
release_profile | End the reservation and stop the browser. |
start_run | Say in one sentence what this stretch of work is for, so the dashboard can show it. |
open_page | Navigate to a public http/https address, and get the full map back. |
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 | A key, including modifier chords. |
scroll | A real wheel gesture with momentum, not a jump. |
wait_for | Wait for text or an element to appear, to go, or for the page to go quiet. |
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. |
read_text | Read the page's prose verbatim, for an article or a thread of comments. |
interruption_respond | Answer what the browser itself is asking: a permission prompt, a device chooser. |
The other four profiles are opt-in, named in [mcp] profiles:
| Profile | Tools |
|---|---|
extended (17) | Operating the page beyond a click: interact (hover, drag, upload, choose from a dropdown), navigate (history and tabs, where a popup a click opened becomes visible), clipboard, save_pdf, download_file, page_report, artifact_list, run_list, interruption_list, the cookie pair (cookie_list, cookie_delete), the passkey pair (passkey_list, passkey_retire), and four measuring tools for verification challenges (measure_frames, measure_gap, measure_rotation, transcribe_audio). |
management (17) | The fleet, addressed by id rather than by handle: profile.list, profile.create, profile.start, profile.stop, profile.update, profile.batch, preset.list, preset.instantiate, group.list, group.create, group.delete, proxy.list, extension.list, session.export, session.import, input.humanize, and credential.list (which platforms a profile can log into: account names, never secrets). |
workflows (3) | record_workflow, compile_workflow, run_workflow |
credentials (4) | credential_fill (log in with the profile's stored account), credential_new (sign up: the daemon draws the password, stores it, types it), credential_store (adopt an account you already have), read_inbox (the confirmation code). The agent names the form fields, the daemon supplies the values, and no response, log or recorded routine ever carries them. |
The naming is a rule, not a habit. A dotted name takes a profile id and manages the fleet; a snake_case name takes a handle and works on the page in front of it. Anything that shows typed text or saved files hangs on the handle, never on an id, because a history addressed by id would show one agent what another one typed.
No profile carries a tool that deletes a profile, in any configuration. Deleting one 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 ships three profiles for that reason:
[mcp]
profiles = ["core", "management", "extended"]
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 disablescredentials is deliberately not among them. An agent doing research has no business inside anyone's account, so switching that on is a decision the operator makes explicitly.
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.
Changing them needs a daemon restart. There is no route that widens the catalog at runtime, on purpose.
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. What stays is everything that only observes: from core that is snapshot, wait_for, screenshot, read_value, read_text, scroll and lease_renew, and the other profiles keep their own list tools the same way.
lease_profile is one of the tools it removes. Reserving a profile starts a browser, so it counts as changing something. An agent under this flag therefore cannot open its own session, which makes the mode a restraint on what a connected agent may do rather than a lighter way to let one in.
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, and 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, because 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.