Labsco
MCP SERVER

sallyport

by ginkida

Drive your own Chrome from an MCP client through a signed local bridge, with every tool refusing to run on a domain you did not put on the allowlist.

Browser Automation, Control & ScreenshotsVerified
Summary
The allowlist is enforced in the extension, not asked for politely in a prompt.

Every frame between daemon and extension is HMAC-SHA256 signed, with timestamp drift capped at 30 seconds and a rolling cache of 4096 nonces that survives service-worker eviction, so a process that merely reaches the local port cannot drive the browser. Arbitrary JavaScript is off even on an allowed domain unless that entry opts into it, fill refuses password fields unless you say otherwise, and close_tab requires an explicit tab id because there is no safe default for destroying someone's work. Every call and its outcome lands in an audit log of the last 500 entries, and the popup has a pause button that rejects everything.

What it is

A browser-automation bridge with the security boundary made explicit. A Python daemon speaks MCP on stdio and hosts a WebSocket server on 127.0.0.1:10086; an unpacked Chrome extension connects to it and drives the page over CDP. Thirty-one tools cover reading, acting, waiting and capturing.

What you get
  • Page reading that does not depend on guessing CSS: an accessibility tree with stable @e1-style refs every other tool accepts, text capped by default at 20 000 characters and raisable to 200 000 with maxChars, with truncated, totalChars and nextOffset when it runs over, and a cheap probe that answers up to 10 assertions at once without a full snapshot (snapshot, read_text, get_state, find).
  • Clicking with an escalation path — DOM .click() first, then a real pointer sequence with hover, press and release for React pointer-event routers that ignore a bare press, plus hover on its own for menus a click would dismiss (click, mouse_click, hover).
  • Input that verifies itself: fill sets the value, dispatches the events, reads the value back, and falls back to insertText when a controlled input reverted it; native <select> is set in the DOM rather than through an OS popup that cannot be clicked; and focused-element typing and key dispatch cover editors and shortcuts (fill, select_option, key_type, send_keys).
  • Waiting instead of sleeping: poll for a selector or visible text to appear — or with absent=true, to disappear — and settle for when there is nothing concrete to wait for and you just need the DOM to stop moving (wait_for, settle).
  • Two escapes from pages with no readable DOM: the recent console errors and uncaught exceptions, which tell a wedged SPA from a slow one, and the recent XHR and fetch responses, which is where a canvas dashboard's numbers actually live (console_tail, network_tail).
  • Navigation with the allowlist enforced at every step — open, reload with an optional cache bypass, go back or forward through the tab's own history, list the tabs this session owns, and close one by explicit id (navigate, reload, history_go, list_tabs, close_tab).
  • Handling for native JS dialogs, which freeze the page's JavaScript and cannot be seen or clicked by any other tool (handle_dialog).
  • Getting at content off-screen or off-viewport: scroll a virtualized container while hunting a target until it gets a ref, or scroll deterministically without a predicate, and emulate a device viewport for one tab without touching the human's windows (reveal, scroll, set_viewport).
  • Capture that keeps bytes out of the model's context: screenshots as native image blocks with downscaling, PDFs and base64 blobs written into a download sandbox under ~/Downloads/sallyport/ (screenshot, print_to_pdf, save_to_file).
  • Two deliberate escape hatches and a health check: arbitrary JavaScript, refused unless that domain has evaluate explicitly enabled; fetch from the page's own context with its cookies, for downloading behind a login; file upload from absolute paths into an <input type=file>; and an instant daemon status that never queues behind a running call (evaluate, fetch_in_page, upload, status).
Requirements

Python 3.10 or newer for the daemon (`pip install --user sallyport`), plus a checkout of the repository to build the Chrome extension, which ships as source rather than through a package registry: `npm install && npm run build` in extension/, then load extension/dist as an unpacked extension. On first run the daemon generates a 32-byte secret at ~/.config/sallyport/secret and prints it for you to paste into the extension popup; `sallyport-daemon doctor` checks the Python version, the secret, its permissions and the port, then prints the remaining steps. Register the daemon with an absolute path — a GUI-launched client often does not inherit your shell PATH. Nothing works on a domain until you add it to the extension's allowlist as example.com, *.example.com or https://x.com/path/*; a bare * is rejected.

Setup effort

One command — pip install --user sallyport