NAME
mcp-webbrowser - expose opted-in browser tabs to MCP
SYNOPSIS
mcp-webbrowser --stdio
mcp-webbrowser --print-url
mcp-webbrowser --stdio --bind 127.0.0.1:31741
mcp-webbrowser --connect http://host:port/TOKEN/browser
DESCRIPTION
The companion userscript lets you explicitly grant an MCP client access to individual browser tabs. Only opted-in tabs connect to this broker over WebSocket; MCP clients can then discover those tabs, execute JavaScript in their page context and add DOM elements. Code can inspect or change the page, including logged-in content and forms, and cause side effects. Treat allowing a tab as giving the MCP client access to that page, not merely its URL or title. The broker still applies its own MCP permission rules.
This executable is the broker, not a browser extension. The userscript handles per-tab authorization and page execution. The broker cannot see unapproved tabs merely because the userscript is installed.
The HTTP/WebSocket listener stays active alongside MCP stdio or an outgoing --connect. Listener configuration, token derivation and URL discovery follow mcp-vim. Tokens authenticate both MCP HTTP and browser WebSocket connections. Keep the broker URL private. The userscript accepts the http:// URL printed by mcp-webbrowser --print-url, converts it to WebSocket internally and appends its own opaque per-tab target ID.
USERSCRIPT INSTALLATION AND USE
The self-contained userscript is webbrowser/mcp-webbrowser.user.js in the AnyEvent::MCP source distribution. Installing the Perl distribution does not install it in a browser. Install a userscript manager first; see the ScriptCat installation guide. ScriptCat on Chromium and Firefox has been tested (Firefox 153 on Linux, including page execution, DOM insertion and navigation). ScriptCat is available for Chrome, Edge and Firefox. Tampermonkey provides the required GM_getTab/GM_saveTab APIs and may work, but has not been tested with this userscript. Other managers are unverified.
Install the source-distribution file through your userscript manager, or open the published userscript URL in the browser and install it there:
https://dist.schmorp.de/mcp-webbrowser/1/mcp-webbrowser.user.js
The /1/ is the major version of the private browser/broker protocol, not the MCP protocol or Perl distribution version. The published script declares that URL for installation and automatic updates; updates depend on the userscript manager. The source file contains the entire userscript and needs no external JavaScript import.
Start the broker (directly or through your MCP client/multibroker) and obtain its listener URL with:
mcp-webbrowser --stdio
mcp-webbrowser --print-url
The second command prints the URL and exits; run it with the same service name and configuration as the running broker. It does not launch a second listener. On a page you want to expose, select Allow MCP access to this tab from the userscript menu. The first time, enter the full URL printed by --print-url when prompted. Do not append a tab name or convert http to ws yourself. The URL is remembered across tabs; the grant is per tab, not per website. Pages remain unexposed until explicitly allowed, and the menu on an unapproved tab contains only the Allow command.
When allowed, the menu offers Disconnect MCP access to this tab, the broker URL configuration and independent toggles for a tab-title indicator and a notification in the page when switching to that tab. Both indicators are enabled by default. The title prefix is [○MCP] while connecting, [●MCP] when connected and [⊗MCP] after prolonged failure. The toast shows the same status and disappears shortly after a successful connection. The indicator prefix is omitted from titles reported to the broker. The permission remains granted during temporary disconnections and normally survives navigation or reload until explicitly revoked.
If a tab does not connect, check that the broker is running and its URL is correct. Depending on browser settings, you may also need to allow the page to access a local-loopback or local-network address; browser permission blocking can look like a WebSocket failure. The title and toast indicators report connectivity, not a trustworthy security boundary.
Note: ScriptCat currently loses the saved per-tab permission when Chromium replaces a discarded tab. After such a discard and restoration, you must choose Allow again. This is tracked in https://github.com/scriptscat/scriptcat/issues/1791.
FIRST TOOL CALL
After allowing a tab, ask your assistant:
Call tabs and show the connected tabs. On one of them, execute
document.title and show its result.
Use a target returned by tabs for the execute call. Listing requires read permission; even this simple JavaScript expression needs exec permission, because execution has full page authority. The tool returns an object containing value; there is no read-only mode.
With a multibroker prefix browser, the tools are browser.tabs and browser.execute. If no tab is listed, confirm that the userscript was allowed on that tab and that its indicator shows a connection. Installing the userscript does not grant access to any tabs. An execution error or timeout does not prove that nothing happened; do not blindly retry JavaScript that changes page state.
TOOLS
- tabs
-
Return
{tabs: [...]}with each connected tab'starget, current documentinstance_id, integerrevision, and client-reportedurlandtitle. The revision starts at 1 and advances on document replacement, not on an ordinary WebSocket reconnect or a same-document navigation. Disconnected tab identities are retained for up to 30 seconds. This is cached registration metadata, not a fresh DOM inspection. Requiresreadpermission. - execute
-
Run
codeontarget. Optionalinstance_idandrevisionfromtabsreject a replaced document before dispatch.timeoutdefaults to 30 seconds and can be 1..300 seconds. Withnavigatefalse (the default), the browser awaits the evaluation result. Results are{value: JSON_VALUE}; exceptions become tool errors. Replies default to a 16 KiB UTF-8 JSON limit;max_bytescan raise the limit to 2 MiB (minimum 1024 bytes). The size includes the JSON envelope. Oversized results yield an error instead of truncating JSON or disconnecting the browser. Code has already executed when the size error is returned; do not blindly retry commands with side effects.With
navigatetrue, the broker sends the command, awaits the browser'sreadymessage, and sendsproceedbefore the browser evaluates it. The broker then waits for either a new document registering on the same tab or an observed same-document navigation. The result hasnavigationequal todocumentorsame-document, the resultingrevision, and reported URL/title. If neither completion is observed before the timeout, it returnsnavigation: "timeout"with a reason, current revision and available metadata. Timeout does not imply that the command was not executed; no automatic retry is attempted. One navigation operation at a time is allowed per tab.The browser client injects
codeas the body ofasync () => codein the page's JavaScript world. A single expression yields its value withoutreturn; use a braced function body with explicitreturnfor multiple statements. Parenthesize object literals, e.g.({title: document.title}).awaitworks inside either form. Declarations in the body do not persist between calls unless assigned to page globals. Results are awaited. This does not useevalorFunction. Syntax errors can prevent the injected script from starting; a temporary page-world error listener tries to return the native syntax-error message, otherwise the client reports an injection failure. Source injection is serialized until each script acknowledges successful compilation or fails; asynchronous execution thereafter may overlap. Code has full page-context authority, with no read-only evaluation mode or sandbox claim.Cancellation and timeout send a best-effort cancel message. They cannot undo already executed code or guarantee that JavaScript stops. Disconnect fails ordinary pending calls without replaying them. A navigation request persists across disconnects and may complete on the next document's registration. Concurrent human navigation can satisfy a pending request; success reports observation, not proof that the submitted code caused the navigation.
- add-element
-
Insert an element through the userscript's
GM_addElementin the content sandbox. This can bypass page CSP for stylesheets and scripts. Requiredtargetandtag; optionalparentis a document CSS selector (default: document root).attributescontains HTML attribute strings, whilepropertiescontains DOM property values, such astextContentorinnerHTML. Properties are supplied during element creation; attributes are set explicitly as well. An unsuccessful selector or insertion is an error. The result is{value: {tag: TAG, id: ID_OR_NULL}}and does not assert that the caller'sidis unique. No automatic ID or registry is maintained: give elements a distinctiveidordata-*attribute when later page-contextexecutecalls need to modify or remove them. Usesexecpermission and supports the same optionalinstance_idandrevisionguards asexecute.
PERMISSIONS
The default rule file is ~/.config/anyevent-mcp/rules/webbrowser-default.ini, under $XDG_CONFIG_HOME when set. --name NAME replaces default. read covers tab discovery; exec covers arbitrary JavaScript.
Exec prompts offer this exact code in this target, any code in this target, or any code in all targets. Saved target grants also cover future tabs using the same target name. Client-reported URLs and titles are display information, not verified origin identities or saved permission scopes. --ask selects the same helper, tty, yad, mcp or auto modes as other servers.
OPTIONS
Common options, including --name, --bind, --token, --stdio, --connect, --print-url, --auto-allow and --auto-deny, are documented in "mcp_getoptions" in AnyEvent::MCP::Server::Simple. This broker retains its HTTP/WebSocket listener when serving MCP over stdio or --connect. --tool-help prints registered tools and arguments without listening. --auto-allow '*,*' approves both discovery and JavaScript execution: only explicitly opted-in tabs are exposed, but code has full authority in those pages.
BROWSER PROTOCOL
WebSocket authentication happens at /TOKEN/TARGET. TARGET is one percent-encoded path component. Before ordinary messages, send:
["mcp-webbrowser", 1, 0, INSTANCE_ID, {"url": URL, "title": TITLE}]
INSTANCE_ID identifies one loaded document. Keep it across reconnects of that document and change it on navigation/reload. The private browser protocol is 1.0, independent of MCP and the old xpcse browser-transfer protocol. The broker answers ["welcome", TARGET, INSTANCE_ID, REVISION]. The requested target represents a browser tab: a different document with the same target replaces its previous connection and increments the revision. Reconnecting the same document retains that revision. A disconnected tab's identity expires after 30 seconds with no pending navigation. No page is exposed before a valid hello.
A tab session starts on first hello, remains active across temporary socket disconnects and document replacements, and finishes on an explicit quit, after 30 seconds of inactivity, or when the broker stops. Session start and finish are logged at info; ordinary socket connection and disconnection events are logged at trace. Revision and request IDs are transmitted as JSON numbers, even when Perl string operations have touched those scalars. Approved JavaScript is logged at trace when dispatched for ordinary evaluation, or when proceed is sent for navigation (never during the permission prompt).
Broker to browser:
["call", ID, "execute", {"code": CODE, "max_bytes": BYTES}]
["call", ID, "navigate", {"code": CODE, "max_bytes": BYTES}]
["call", ID, "add_element", {"tag": TAG, "parent": SELECTOR, "attributes": ATTRS, "properties": PROPS}]
["proceed", ID]
["cancel", ID]
Browser to broker:
["reply", ID, true, JSON_VALUE]
["reply", ID, false, ERROR_STRING]
["ready", ID]
["navigated", ID, {"url": URL, "title": TITLE}]
["state", {"url": URL, "title": TITLE}]
["quit"]
The browser replies ready to a navigate call without evaluating it. Only proceed starts evaluation. navigated reports a completed same-document navigation; a different document's hello is the other completion path. reply with false can report an evaluation failure. Only JSON values cross the wire; undefined becomes null. Errors are strings. Late replies after cancellation are ignored. State updates replace cached metadata. Malformed messages close the connection with status 1002. The initial hello has a 15-second deadline; a silent connection is closed.
The distributed userscript evaluates in the page's JavaScript world via an injected script and returns serialized results through DOM events. Its Navigation API listener is installed in that same world. The broker cannot authenticate page-reported values or infer application-rendering readiness from navigation success. The userscript retries disconnected WebSockets; it has no event subscriptions or separate SPA revision counter.