A smartphone displaying a browser window in cobalt wireframe, connected by glowing threads to a field of other phones, captioned "A browser MCP server on real phones"

The Archonum Browser MCP Server: Setup, Tools, and Limits

Archonum runs a hosted browser MCP server at https://app.archonum.com/mcp. It gives an AI agent 19 tools that drive Chrome on a real consumer smartphone, with a per-session exit country, and there is nothing to install: it is a remote endpoint you point a client at, and it now speaks OAuth 2.1 as well as HTTP Basic, so interactive clients connect with a sign-in rather than a pasted token.

19
tools exposed over one Streamable HTTP endpoint, no SDK and no local process
Archonum hosted MCP, checked August 2026
250,000+
real consumer smartphones the browser can run on
Archonum, checked August 2026
175+
countries you can pin a session’s exit to
Archonum, checked August 2026

Chrome runs on the handset that owns the IP. The agent is not driving a headless browser in your cloud that then relays out through somebody’s home connection, so there is no relay in the path and one network hop to the target.

When should an agent use this instead of web_fetch?

Most of the time it should not. web_fetch and web_search are faster and cheaper, and for open, undefended pages they return the same text.

Switch when one of these holds:

  • The site blocks bots. web_fetch comes back with a 403, a challenge page, or suspiciously empty content.
  • The data only exists after interaction: a search form, a login, a cart, a booking flow.
  • You need the page as seen from a specific country, for geo pricing, availability or localization.

The rule of thumb we ship in the server’s own instructions is deliberately narrow: if web_fetch comes back blocked or empty, retry that one URL with read. Agents that reach for a remote browser by default burn credits on pages a plain fetch would have returned.

How do you connect a client to the browser MCP server?

The endpoint speaks Streamable HTTP, the transport defined in the Model Context Protocol specification, revision 2025-06-18, which requires “a single HTTP endpoint path” supporting both POST and GET. Any compliant client can attach.

Authentication comes in two forms, and picking between them is the whole setup decision:

  • OAuth 2.1 for interactive clients: add the endpoint with no credentials at all, and the client discovers the flow and opens a browser window to sign in. Claude, ChatGPT and Codex all handle this.
  • HTTP Basic for headless setups: your archonum username and API token, the same token the proxy and the CDP gateway already accept.

For the Basic route, encode the pair yourself:

printf '%s' 'USERNAME:TOKEN' | base64

Or skip that step. The dashboard’s API-key page has a Setup for agents panel that copies a paste-ready prompt with the credential already encoded, pointing the agent at app.archonum.com/skill.md for the rest.

Credential note

The Basic credential is your API token, base64-encoded rather than encrypted. It is the same token that authorizes proxy bandwidth, so treat the config file holding it exactly as you treat the token itself. OAuth narrows that exposure: access tokens live one hour, refresh tokens thirty days and rotating, and a client can revoke its own grant at /o/revoke_token/. Regenerating the key in the dashboard invalidates the old one everywhere at once, OAuth grants included. MCP session ids are a different matter: they travel in headers, client configs and logs, so they are not secrets and the server never treats one as an access grant on its own.

Claude Code

OAuth: add the server with no header, then run /mcp, pick archonum and choose Authenticate.

claude mcp add --transport http archonum https://app.archonum.com/mcp

Or Basic, for a machine where nobody is around to click through a sign-in:

claude mcp add --transport http archonum https://app.archonum.com/mcp \
  --header "Authorization: Basic <base64 of username:token>"

Claude Desktop

Claude Desktop’s custom connectors authenticate through OAuth, which the endpoint now speaks natively: add a custom connector, give it https://app.archonum.com/mcp, and sign in when prompted. No bridge, no config file.

The mcp-remote bridge is only still needed if you want Basic auth here, because the connector UI has no field for a static header. In that case add it to claude_desktop_config.json as a stdio server:

{
  "mcpServers": {
    "archonum": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://app.archonum.com/mcp",
        "--header",
        "Authorization:${ARCHONUM_AUTH}"
      ],
      "env": { "ARCHONUM_AUTH": "Basic <base64 of username:token>" }
    }
  }
}

The colon carries no trailing space on purpose. Claude Desktop on Windows and Cursor both fail to escape spaces inside args when invoking npx, which mangles the header; putting the value in env sidesteps it. That workaround is documented in the mcp-remote README.

ChatGPT

In the ChatGPT app, open Settings, then MCP servers, then Add server, name it, choose Streamable HTTP, and give it the endpoint URL. ChatGPT is one of the interactive clients that can sign in over OAuth; where you prefer Basic instead, supply the credential in the client’s header field.

Where a client accepts only a URL, the endpoint also honors credentials in the userinfo position, which the HTTP stack converts into the same Basic header:

https://USERNAME:TOKEN@app.archonum.com/mcp

Codex CLI

Codex configures remote servers in ~/.codex/config.toml rather than through codex mcp add, which covers stdio servers. For Basic auth, use env_http_headers so the credential lives in the environment instead of the config file:

[mcp_servers.archonum]
url = "https://app.archonum.com/mcp"
env_http_headers = { "Authorization" = "ARCHONUM_MCP_AUTH" }

Then export ARCHONUM_MCP_AUTH="Basic <base64 of username:token>". The static alternative is http_headers = { "Authorization" = "Basic ..." }, which writes the credential to disk.

Any other client

{
  "mcpServers": {
    "archonum": {
      "type": "http",
      "url": "https://app.archonum.com/mcp",
      "headers": { "Authorization": "Basic <base64 of username:token>" }
    }
  }
}

The OAuth details, for anyone wiring a client by hand

The server publishes discovery metadata at /.well-known/oauth-authorization-server (RFC 8414) and /.well-known/oauth-protected-resource (RFC 9728). The flow is authorization code with mandatory PKCE, S256 only; a client without PKCE cannot connect. There is a single scope, mcp. Dynamic client registration (RFC 7591) is open at /o/register/, unauthenticated but limited to 30 registrations per hour per caller, with 429 past that. Redirect URIs may be https on any host, or http only on localhost. Access tokens last one hour and refresh tokens thirty days, rotating, so a client that cannot refresh stops working after an hour.

How do you verify it connected?

Call read on something trivial and confirm rendered text comes back:

{ "url": "https://example.com" }

A successful call returns the page’s text under a status line carrying the resolved URL, HTTP status, a blocked flag and the text length.

Three errors are worth telling apart, because two of them tempt the wrong fix. A 401 means the username or token is genuinely wrong, or the account’s email is unverified; recheck the credential. A 402 means the balance is empty or a free trial lapsed — re-authenticating or regenerating the key will not help, only topping up will. A 404 unknown session means the MCP session id expired after 30 minutes of no requests; re-initialize the connection and carry on.

What are the 19 tools?

Most work runs through three of them: read, snapshot and click. The rest exist for cases those three cannot reach.

The 19 tools exposed by the Archonum hosted MCP server, August 2026.
Group Tools What it is for
One-shot read read Rendered text of a URL, optionally from a given country. The web_fetch substitute; no session needed.
Sessions open_session, close_session, list_sessions Named, independent sessions, each pinned to an exit country.
Navigation navigate, back, wait_for_content, page_info Load a URL in a session, go back one history entry, and report whether a bot wall cleared.
Reading snapshot, get_text, get_html, screenshot Interactive-element outline, page text, rendered HTML, viewport PNG.
Acting click, type, select, press, scroll Drive the page by element ref.
Account get_credits Remaining credit balance in MB; check it before a large job.
Geo diff compare_countries Replay one fixed flow across 2 to 5 countries in parallel and compare.

The interactive loop is navigate, snapshot, act, snapshot again. snapshot returns an outline tagged with refs like [e12] button "Add to cart", and click and type take those refs. It pierces open shadow DOM and same-origin iframes; cross-origin frames appear as markers, because their contents are genuinely unreachable.

Refs are ephemeral. They belong to that session’s most recent snapshot and go stale the moment the DOM changes. Re-snapshot rather than reusing an old ref.

get_html is the newest of the reading tools: the fully rendered, post-JavaScript DOM for structured extraction, scoped with a CSS selector and truncated at maxChars (up to 100,000). Where get_text answers “what does the page say”, get_html answers “give me the markup so I can parse it”.

click and type fail loudly rather than silently doing nothing. If an overlay covers the target, the error names the blocker instead of reporting a successful click on empty space. Actions also report effect=navigation, dom-change or none, so an agent can tell that a widget ignored it and retry with keyboard activation rather than carrying on from a false premise.

click and type fail loudly rather than silently doing nothing.

An agent that cannot tell “I clicked the button” from “I clicked where the button used to be” will confidently report a booking it never made. Loud failure is cheaper than that.

How do sessions, countries and limits work?

Each user gets their own pool of named sessions. Open one per country and drive them independently:

open_session { "name": "ch", "country": "ch" }
open_session { "name": "it", "country": "it" }

Pages and refs are per-session and never mix. Reopening a name with a different country replaces the session outright — the page and its state are gone, exactly as with freshIdentity. The defaults, all server-side configurable:

  • 5 concurrent browser sessions per user; opening a sixth fails until one closes.
  • 3 in-flight read or compare_countries calls per user, since each opens a device outside the session pool.
  • Sessions are reaped after 10 minutes idle, users after 30 minutes; MCP session ids expire after 30 minutes of no requests.
  • Requests are capped at 4 MB (413 past that); page loads time out at 90 seconds; wait_for_content accepts at most 60.
  • read output is capped at 8,000 characters. For more, navigate in a session and use get_text with a larger maxChars (up to 50,000) or a selector.

Billing is inherited. The service connects through the same gateway as everything else, so usage meters as ordinary gateway traffic with no MCP-specific charge; get_credits reports the remaining balance, and screenshots and media-heavy pages cost noticeably more than text reads.

Every request is re-authenticated, including requests that already carry a session id. A session id belonging to another user is refused with a 403 rather than served that user’s browser and credits.

What happens when a page stays blocked?

navigate reports a blocked flag with a challenge classification, naming what it hit: cloudflare-turnstile, cloudflare-js-challenge, akamai, unknown-empty, and a clearable estimate.

Most walls clear on their own, because the request is coming from a real handset with a genuine fingerprint and there is nothing synthetic underneath to contradict it. Call wait_for_content and let it settle.

When it does not settle, the useful signal is stalled=true, returned early after ten seconds of zero DOM, network and text change. That means the identity is hard-blocked and further waiting is wasted. Reopen under the same name with a new device and exit IP:

open_session { "name": "ch", "freshIdentity": true }

Which tool fits which job?

Use read
You need the text of a page and nothing more

One-shot, no session, no cleanup. Pass waitForText on JS-heavy pages so it cannot return a half-rendered shell; settled=false in the result means the wait timed out and the text may be incomplete. Output caps at 8,000 characters — past that, a session with get_text is the escape hatch. This is the tool that replaces a blocked web_fetch, and it should be the majority of your calls.

Use a session
The data only exists after you interact

Logins, search forms, carts, booking flows, anything spanning several steps. open_session, then navigate, snapshot, act, and use get_text to read the state you drove to, or get_html when you want markup to parse rather than prose. Sessions hold one identity across every step, which is the part a rotating proxy cannot give you.

Use compare_countries
You want the same page from several countries at once

A declarative flow replayed across 2 to 5 countries in parallel, each in a fresh session, with an optional CSS selector to extract one value per country. Steps target elements by visible name or selector rather than by ref, since refs cannot survive across separate sessions. For anything needing real interaction per country, open a session per country instead.

The credential is on the API-key page in the dashboard, and OAuth clients need nothing at all beyond the endpoint URL. The agent-facing version of this document lives at app.archonum.com/skill.md, and most clients can read a URL, so handing an agent that link is usually the whole of the setup.