junso-browser
v0.3.0
Published
Standalone CloakBrowser host — runs fingerprinted stealth-Chromium per profile, exposes each over a token-authed CDP gateway + interactive viewport, and ships an optional MCP server so any client gets browser tools with no local browser.
Maintainers
Readme
junso-browser
A standalone CloakBrowser host. It runs one stealth-Chromium instance per profile, each with its own deterministic device fingerprint + proxy + cookies, and exposes each over a token-authed CDP gateway. Drive it with anything that speaks CDP — agent-browser, Playwright, Puppeteer — via --cdp.
Built for Jun: Jun keeps its whole tool surface and just connects to a junso-browser profile's CDP URL, gaining real per-profile device identities (the gap a shared local browser can't close).
Why
A fresh profile gives new cookies + storage; a proxy gives a new IP — but on one machine every profile shares the same device fingerprint (canvas/WebGL/timezone/fonts/TLS), so a site can still link them. CloakBrowser derives a full, consistent fingerprint from a seed (same seed = same device; different seed = different device). junso-browser runs one seeded instance per profile and hands out its CDP endpoint.
How it works
client (agent-browser --cdp wss://host/cdp/<profile>?token=…)
│ WS relay + /json discovery (token-authed gateway)
▼
junso-browser ──spawn──> cloak chrome --fingerprint=<seed> --fingerprint-platform=…
--proxy-server=… --user-data-dir=… --remote-debugging-port=0- No Playwright at runtime — the cloak binary applies the fingerprint from
--fingerprint*flags (--headless=new); junso-browser is a thin process supervisor that spawns it and proxies CDP. - The cloak CDP binds
127.0.0.1with no auth; junso-browser is the only thing that touches it and fronts it with a token gateway.
Install & run
bun i -g junso-browser # installs the host + MCP bins
JUNSO_BROWSER_TOKEN=secret JUNSO_BROWSER_HOST=0.0.0.0 junso-browser # start the host (:8790)The CloakBrowser binary (~200 MB) auto-downloads on first run (opt out: JUNSO_BROWSER_NO_CLOAK_DOWNLOAD=true). Runs on Bun (Bun.serve/Bun.spawn).
The package ships two bins: junso-browser (the host) and junso-browser-mcp (the optional MCP — see below). From source: bun install then bun run start / bun run mcp. Build: bun run build (bundle dist/) or bun run build:binary (single compiled binary).
Config (env)
| Var | Default | |
|---|---|---|
| JUNSO_BROWSER_PORT | 8790 | listen port |
| JUNSO_BROWSER_HOST | 127.0.0.1 | listen addr (token required if non-loopback) |
| JUNSO_BROWSER_TOKEN | — | shared secret for API + CDP |
| JUNSO_BROWSER_DATA_DIR | ~/.junso-browser | profiles.json + user-data dirs |
| JUNSO_BROWSER_MAX_INSTANCES | 3 | concurrent cloak instances (LRU-reaped) |
| JUNSO_BROWSER_IDLE_MS | 1800000 | reap an instance after this idle |
| JUNSO_BROWSER_PUBLIC_URL | — | base for handed-out CDP URLs (e.g. wss://browser.example.com) |
| JUNSO_BROWSER_CLOAK_PATH | auto | override the binary path |
API
All /api/* require the token (Authorization: Bearer …, x-junso-token, or ?token=).
| Method | Path | |
|---|---|---|
| GET | /health | status + cloak presence + running count |
| GET | /api/profiles | list (proxy masked) + running flag |
| POST | /api/profiles | { name, fingerprint?: { seed?, platform?, timezone?, locale? }, proxy? } |
| GET/PATCH/DELETE | /api/profiles/:id | read / update (incl. rotateSeed) / delete |
| POST | /api/profiles/:id/launch | start instance → { cdpUrl } |
| POST | /api/profiles/:id/stop | stop instance |
| GET | /api/profiles/:id/cdp | { cdpUrl } (auto-launches on connect) |
| WS/GET | /cdp/:id | CDP gateway (ws relay + /cdp/:id/json* discovery) |
Interactive viewport
Open http://<host>:8790/view/<profile>?token=<token> in any browser to watch and take control of a profile's browser — live screencast + your mouse/keyboard forwarded over CDP. Use it for human-in-the-loop steps (log in by hand, solve a CAPTCHA, check something) on the same fingerprinted profile the agent drives via MCP. CDP supports multiple clients, so the viewport and the MCP coexist. (Frames stream on repaint — a static page shows one frame until you interact.)
Optional browser MCP
junso-browser ships the same browser MCP over two transports — the way to give any MCP client (Jun, Codex, Claude Desktop) browser tools without a local browser. It drives profiles through the host's CDP gateway, so it works whether junso-browser is local or remote.
HTTP transport — no local install (recommended)
Since 0.3.0 the host exposes the MCP at POST /mcp (Streamable HTTP), so a client adds junso-browser as a plain URL with nothing installed locally. Token-authed via Authorization: Bearer <token> (also accepts x-junso-token: <token> or ?token=<token> in the URL).
The auth header config differs per client — this is the #1 setup gotcha:
Claude Code (.mcp.json at the repo root, or ~/.claude.json for user scope) — uses a headers object:
{
"mcpServers": {
"junso-browser": {
"type": "http",
"url": "http://<host>:8790/mcp",
"headers": { "Authorization": "Bearer <token>" }
}
}
}Codex / Jun (config.toml) — uses http_headers (NOT headers — Codex silently ignores a headers block, connects with no auth, gets a 401, and hangs at startupState:"starting"):
# may also require this at the top level on some builds:
experimental_use_rmcp_client = true
[mcp_servers.junso-browser]
url = "http://<host>:8790/mcp"
[mcp_servers.junso-browser.http_headers]
Authorization = "Bearer <token>"Codex also supports bearer_token_env_var = "MY_TOKEN_ENV" and env_http_headers to source the token/headers from env vars instead of hardcoding.
Common to all clients:
- Append
?profile=<id>to the URL to set the session's initial profile (elsedefault, auto-created). - Behind the Caddy TLS front, use
https://<host>:8443/mcp(or whatever subdomain reverse-proxies to127.0.0.1:8790). - The MCP runs in-process on the host, one isolated server per session (
Mcp-Session-Idheader), idle-reaped after 10 min. A plainGET /mcp→ 405 (the bridge is request/response only, no server→client SSE) — that's expected, not an error. browser_solve_captcha's 2captcha key is supplied by the connector:x-twocaptcha-keyheader or?twocaptcha=(keeps the secret with the client).
Troubleshooting: if the client hangs on "loading" / shows a "Connect"/OAuth button / sits at authStatus:"unsupported", the token isn't reaching the host (wrong header key — see Codex http_headers above) → it 401s and never completes the handshake. Verify the host independently with a raw POST:
curl -i -X POST http://<host>:8790/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'A working host returns 200 with an mcp-session-id header and serverInfo: junso-browser. If curl works but the client doesn't, the problem is the client's auth-key syntax, not the host. Also confirm you're hitting an actual junso-browser host: GET /health returns JSON {"name":"junso-browser",…} — if it returns HTML, that hostname is serving a different app (e.g. the Jun web UI) and junso-browser isn't routed there.
stdio transport — junso-browser-mcp bin
Add it to a client's MCP config (once junso-browser is installed globally):
{
"command": "junso-browser-mcp",
"env": { "JUNSO_BROWSER_URL": "https://browser.example.com:8790", "JUNSO_BROWSER_TOKEN": "secret", "JUNSO_BROWSER_PROFILE": "default" }
}(or "command": "bunx", "args": ["-y", "junso-browser-mcp"] without a global install).
Tools
Drive: browser_status, browser_navigate, browser_snapshot, browser_click, browser_fill, browser_type, browser_press, browser_select, browser_scroll, browser_wait, browser_read, browser_eval, browser_screenshot, browser_exec. Identity: browser_list_profiles, browser_switch_profile, browser_create_profile. Proxy/IP: browser_get_proxy, browser_set_proxy, browser_exit_ip, browser_rotate_proxy, browser_probe_ip. CAPTCHA: browser_solve_captcha.
The active profile (env JUNSO_BROWSER_PROFILE for stdio / ?profile= for HTTP, default default) is auto-created on the host if missing; browser_switch_profile changes identity at runtime. browser_exit_ip returns the live exit IP + geo health (kind, tzMismatch); browser_rotate_proxy draws a fresh sticky IP and re-syncs the fingerprint — the host carries standing login instructions so clients follow the residential-IP → health-check → one-attempt-per-IP recipe automatically.
Status
Phase 1 MVP — verified e2e (profile → fingerprinted cloak → external CDP client sees the configured fingerprint, deterministic per seed). Next: Jun integration (opt-in JUN_BROWSER_CDP backend), single-binary deploy, optional fingerprint personas.
