universal-browser-bridge
Allows AI agents to control explicitly shared Chrome tabs via MCP tools, enabling navigation, clicking, typing, scrolling, screenshots, and tab management.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@universal-browser-bridgenavigate to example.com and take a screenshot"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Universal Agent Browser Bridge
A local, agent-agnostic bridge that lets MCP-compatible AI agents operate Chrome tabs you explicitly share or create under their agent label.
It works with Hermes, Claude Code, Codex, and other MCP clients while keeping your signed-in Chrome session, tab access, and consequential-action approvals under your control.
This is an independent project inspired by visible tab-consent browser-control systems. It is not affiliated with OpenClaw, Anthropic, Google, OpenAI, or Nous Research.
What it provides
A Chrome Manifest V3 extension using
chrome.debuggerExplicit per-tab access through a visible Agent Bridge tab group and ON badge
One local
servedaemon that owns the Chrome connectionMultiple simultaneous MCP clients connected through authenticated local IPC
Navigation, snapshots, clicking, typing, key presses, scrolling, screenshots, and tab management
Generation-scoped DOM references that become invalid after navigation
Chrome-hosted human approval for consequential actions, with configurable trust for agent-owned tabs
A user-editable site blocklist enforced by both the extension and daemon
Persistent label ownership of agent-created tabs in named Chrome tab groups
Per-tab serialization so multiple agents cannot interleave browser actions
Related MCP server: Tabryn
Architecture
Chrome extension
|
| authenticated WebSocket on 127.0.0.1:17321
v
Universal Browser Bridge daemon (`serve`)
|
| authenticated Unix socket / Windows named pipe
+---- MCP adapter (`mcp`) for Hermes
+---- MCP adapter (`mcp`) for Claude Code
+---- MCP adapter (`mcp`) for Codex
+---- MCP adapter (`mcp`) for another session or agentThe Chrome extension maintains one connection to the daemon. Start the daemon once, then connect as many MCP adapter processes as you need.
Quick start
1. Clone and build
Requirements:
Google Chrome or a compatible Chromium browser with Manifest V3 and
chrome.debuggerNode.js and npm (the project is verified with Node.js 22)
An MCP-compatible agent or client
git clone git@github.com:code-x-coffee/universal-browser-bridge.git
cd universal-browser-bridge
npm ci
npm run buildIf you cloned over HTTPS instead:
git clone https://github.com/code-x-coffee/universal-browser-bridge.git2. Generate the pairing token
npm run tokenCopy the printed token. It is stored at:
~/.universal-browser-bridge/tokenKeep it private. The extension and every local MCP adapter use it to authenticate with the daemon.
3. Load the Chrome extension
Open
chrome://extensions.Enable Developer mode.
Click Load unpacked.
Select the repository's
extension/directory.Open the extension's Details page.
Click Extension options.
Set Relay URL to:
ws://127.0.0.1:17321/extensionPaste the pairing token.
Click Save and connect.
The extension can be configured before the daemon starts. Its toolbar badge clears once the daemon is running and authentication succeeds.
Loading the extension from a different directory creates a different unpacked-extension identity in Chrome. You must enter the relay URL and token again for that new identity.
4. Start the daemon
Run this in a dedicated terminal and leave it running:
npm run serveEquivalent direct command:
node /absolute/path/to/universal-browser-bridge/dist/cli.js serveExpected startup output resembles:
Universal Browser Bridge daemon listening.
Extension endpoint: 127.0.0.1:17321
Control socket: ~/.universal-browser-bridge/daemon.sock
Token: ~/.universal-browser-bridge/tokenCtrl-C gracefully stops the daemon, disconnects all MCP adapters, and removes the control socket.
5. Configure your MCP client
The MCP command is:
node /absolute/path/to/universal-browser-bridge/dist/cli.js mcpUse an absolute path because MCP clients often launch subprocesses from a different working directory.
Standard MCP configuration:
{
"mcpServers": {
"browser-bridge": {
"command": "node",
"args": [
"/absolute/path/to/universal-browser-bridge/dist/cli.js",
"mcp"
],
"env": {
"UBB_CLIENT_LABEL": "my-agent"
}
}
}
}Use a different UBB_CLIENT_LABEL for each responsibility, for example research, marketing, or coding. The label appears in Chrome approval prompts and ownership errors.
Hermes Agent
Add this to ~/.hermes/config.yaml:
mcp_servers:
browser-bridge:
command: node
args:
- /absolute/path/to/universal-browser-bridge/dist/cli.js
- mcp
env:
UBB_CLIENT_LABEL: hermes
enabled: trueStart a new Hermes session after changing MCP configuration. The discovered tools are prefixed by Hermes with the configured MCP server name.
Multiple sessions and agents
Each MCP configuration launches a lightweight adapter. All adapters connect to the same daemon, so multiple agent sessions can use the bridge simultaneously without competing for port 17321.
Do not configure every adapter to run serve. Only one daemon should run. Every MCP client should run mcp.
6. Share a tab
Open a normal
http://orhttps://page.Click the extension toolbar icon.
Confirm the tab shows an ON badge.
Confirm Chrome placed it in the Agent Bridge tab group.
Ask your agent to call
browser_status,browser_tabs, orbrowser_snapshot.Click the toolbar icon again whenever you want to revoke access.
Chrome internal pages, extension pages, password-manager prompts, passkeys, native dialogs, and arbitrary desktop applications are intentionally outside the bridge's scope.
Agent-owned tabs
Set UBB_CLIENT_LABEL for an MCP adapter, or pass --label to the CLI. A labelled browser_new_tab creates a tab in the dedicated agent window and groups it under the label's exact title. It reuses a matching group in that window. By default, the agent window is created without a prompt and without taking focus. Chrome asks first only if you turn off Let agents open their window without asking in Extension options.
The package provides both ubb and universal-browser-bridge CLI names. From a checkout, use node dist/cli.js in place of ubb:
ubb --label Hephaestus browser_new_tab '{"url":"https://example.com"}'
ubb --label Hephaestus browser_list_agent_tabs '{}'
# Use the id from browser_new_tab, or tabId from browser_list_agent_tabs:
ubb --label Hephaestus browser_close_tab '{"tabId":123}'browser_list_agent_tabs returns {tabId, url, title, groupId} for each tab owned by the caller's label. browser_status and browser_tabs include hand-shared tabs and the caller's owned tabs. Every browser tool accepts owned tab IDs. Other labels, including clients without a label, cannot list, read, drive or close those tabs. A label is a caller-supplied identity: authenticated clients presenting the same label share ownership, even across separate CLI invocations or MCP sessions.
Current tab access is stored in chrome.storage.session; labelled ownership is also stored in chrome.storage.local under agentTabOwnersDurable. It survives service-worker restarts, extension reloads/updates, and daemon/client reconnects when the recorded tab, exact label/group title and agent window still validate. The group title is the literal label, including spaces and pattern characters; unlabelled tabs use Agent Bridge. Restoration never derives a label from a group or adopts other tabs in that group. Missing tabs, invalid mappings, duplicate records and ambiguous windows are dropped. The agent window is restored only from validated owned tabs. Hand-shared and unlabelled access remains session-only.
Closing, ungrouping, moving a tab to another group/window, renaming its group, clicking the toolbar to unshare, or cancelling debugging revokes ownership and removes its durable record. Durable revocation also prevents stale session data from restoring access. Merely placing an existing tab in a named group never grants an agent access to it.
Without a label, browser_new_tab keeps the existing behavior: it creates a shared, unowned tab in Agent Bridge. All clients may read and drive it. While its creating client remains connected, only that client may close it; after disconnect it may be closed by another client. browser_list_agent_tabs requires a label. browser_close_tab also accepts hand-shared tabs legitimately controlled by the caller. Hand sharing still uses Agent Bridge and the ON badge.
Tab creation requests are serialized. If a requesting client disconnects while its request is queued, the daemon drops the request. If it disconnects while Chrome is creating the tab, the daemon attempts to close the orphan before processing the next request, without recording ownership. Cleanup is best effort; if Chrome refuses the close, the tab may remain open.
Trust and site safety settings
Open Extension options to change Trust agent-owned tabs, Let agents open their window without asking, or Blocked sites, then click Save safety settings. These settings are stored in chrome.storage.local and apply immediately without restarting the daemon.
Trust agent-owned tabs defaults to enabled (trustAgentOwnedTabs: true when unset). Consequential clicks and Enter/NumpadEnter skip the approval popup only when the extension confirms that the target tab is currently owned by the calling label. Turn it off to require approval in owned tabs too. Hand-shared and unlabelled tabs always retain the approval flow for these actions. Clients cannot override the setting with a request flag. Labels remain caller-supplied identities, so authenticated clients using the same label share that trust.
Let agents open their window without asking defaults to enabled (autoCreateAgentWindow: true when unset). When no live agent window exists, the extension creates one with focused: false without an approval popup. Turn it off to require approval before creating the window; opening more tabs in an existing agent window does not prompt. The extension reads the setting for each window creation. With this setting on, any local process holding the bridge token can open agent tabs in your signed-in Chrome without a prompt.
Bank, card, payment and password-manager URLs, including the initial URL of a newly created tab, allow snapshots, screenshots and scrolling, but block clicks, typing, key presses and navigation even with trust enabled. The shared URL policy recognizes listed financial domains (including HDFC, ICICI, SBI, Axis, Kotak, PayPal, Stripe, Razorpay, American Express, Discover, Capital One, Visa and Mastercard), password managers (1Password, Bitwarden, LastPass, Dashlane, Keeper, Google Password Manager, NordPass and Proton Pass), common banking/payment hostname labels, and /checkout, /payment, /payments or /pay path segments. It checks current and pending URLs before execution, including after approval waits. Proton's /pass pages and pass.proton.me are protected without making its other pages read-only. This is conservative URL classification, not an exhaustive sensitive-site directory. The private-network opt-in does not disable it.
Blocked sites defaults to empty (blockedDomains: []). Enter one domain per line; pasted URLs are accepted. Saving lowercases domains, strips schemes, paths, ports, leading www. and trailing dots, converts international names to their ASCII form, and removes empty lines and duplicates. Invalid entries show an error without replacing the saved settings. For example, https://www.example.com/private becomes example.com and blocks that domain and every subdomain, but does not match notexample.com or example.com.other.org. To allow a site again, remove its entry and save.
Blocked pages are hidden from tab listings and refuse new tabs, navigation to the domain, snapshots, screenshots, clicks, typing, keys, scrolling and every CDP command. This applies to both owned and hand-shared tabs, regardless of trust. Both the live URL and pendingUrl are checked. The extension sends its normalized blocklist over the authenticated connection on connect and on changes; the daemon checks its copy before dispatch and rechecks live tab state through the extension. The extension independently checks again before executing. Refusals contain "blocked by your blocklist". The blocklist controls bridge access to tabs; it does not filter a page's network requests or stop you from opening sites yourself. Previously saved annotations remain available in the local annotation store.
Uploading files
Use browser_upload_file to attach local files through an existing file input or an upload button that opens a file chooser. Pass ref, description, snapshotId and files (an array of absolute file paths). tabId is optional and otherwise uses the selected tab. Take a fresh browser_snapshot after navigation. Optional mode controls how the files are attached:
auto(default): use an<input type="file">, an associated label, or a container with exactly one file input when available; otherwise click the ref and intercept the chooser.input: require a resolvable file input and retain the "Target is not a file input" error for other elements.chooser: always click the ref and intercept the chooser, including dynamically created inputs.
{
"ref": "ubb-3",
"description": "Attach the reference image",
"snapshotId": "7:0",
"files": ["/Volumes/NVme/Projects/Stories/reference.png"],
"mode": "auto",
"tabId": 7
}In Extension options, set Allowed upload folders, one absolute folder per line, then click Save safety settings. The default when unset is /Volumes/NVme/Projects/Stories/. An empty list disables uploads. Invalid and relative entries are ignored; saved paths are normalized and duplicates removed. Only extension settings can change this list. An older extension that does not send the setting leaves uploads disabled in the daemon.
Uploads require a labelled agent and a tab owned by that same label. Hand-shared tabs, blocklisted sites, and bank/card/payment/password-manager pages cannot receive files. Disabled file inputs are rejected; attaching multiple files requires the input's multiple attribute. Each call accepts 1–10 regular files, each at most 100 MB (104,857,600 bytes).
The daemon resolves files and allowed folders with realpath and checks containment, so sibling prefixes and symlinks escaping an allowed folder are rejected. Symlinks pointing into an allowed folder resolve to the real file path, which is passed to Chrome and returned as { "uploaded": 1, "files": ["/absolute/real/path"], "method": "input", "picker": "input" }. method is input or chooser; picker is input or showOpenFilePicker. The extension independently checks these real paths against its configured folder prefixes; when configuring a symlinked folder, also allow its real destination. Paths containing NUL or .. segments are refused.
Chooser mode uses the same scroll-to-center, hit-test and trusted mouse input as browser_click, with a user-activated script fallback for covered, zero-size or offscreen targets. Consequential clicks retain approval. The extension handles interception and attachment in one command, listens only for the target tab's chooser, and rejects multiple files if it opens in single-selection mode. If no chooser opens within 5 seconds, the error reports whether the showOpenFilePicker shim was triggered, with a file-scheme permission hint when that permission is off.
During a chooser call, window.showOpenFilePicker is temporarily replaced with a hidden file input that respects multiple and the file types in accept. Its promise resolves after the input's change event to file-handle-like objects with kind, name, getFile, permission methods and isSameEntry. These are not native FileSystemFileHandle instances; pages checking instanceof FileSystemFileHandle may fail. The extension always attempts to disable interception and restore the original picker after success, timeout or error. A 10-second page timer also restores the shim if normal cleanup cannot run.
Open chrome://extensions, find Universal Browser Bridge, open Details, and enable Allow access to file URLs. Reload the updated extension and rebuild/restart the daemon and MCP adapters before using the new tool. Live Chrome uploads are a separate manual check from automated tests.
Downloading files
Use browser_download to save media from a tab owned by your agent label directly to an allowed local folder. Blob/data downloads use the existing page-held Blob path. Same-origin HTTP(S) downloads also use that path first, with manual redirect handling. Only an opaqueredirect response releases the page entry and retries through Chrome debugger response interception and IO streams, which validate every hop. Other page-fetch results and failures keep their existing behavior. Trusted cross-origin HTTPS downloads go directly through interception, keeping cookies inside Chrome while bypassing page CORS body restrictions. A redirecting same-origin URL is requested twice: once to detect the redirect, then again to inspect and validate the chain. Non-redirecting same-origin downloads avoid interception. Chrome's Save As preference and the background agent window do not affect saving. Pass filename, description, and either ref + snapshotId from browser_snapshot or url. Snapshots include visible video, image and audio elements, plus source children of visible media, within the existing 250-element cap. Ref sources can be <video>, <audio>, <source>, <img> or <a>; the tool uses currentSrc, then src, then the first <source> child for video/audio without their own source, or href for links. Video/audio/source nodes include only the source hostname as srcHost (or blob: / data:), never its path or signed query.
{
"ref": "ubb-3",
"snapshotId": "7:0",
"filename": "scene.mp4",
"description": "Save the generated scene",
"subfolder": "renders",
"tabId": 7
}For a URL source, replace ref and snapshotId with url. HTTP(S) URLs can have the same origin as the tab; cross-origin sources require HTTPS and a matching exact hostname or explicit leading *. wildcard under Trusted download hosts in Extension options. Blob URLs must belong to the tab origin; data URLs are also allowed. Other schemes and cross-origin plain HTTP are refused. Every redirect hop must use HTTPS and be same-origin or trusted, with a maximum of 10 redirects. Both the extension and daemon check every hop before Chrome sends the next request. Blocklisted hosts are always refused, including redirect destinations. Downloads require a labelled agent, a tab owned by that label, and a writable, unblocked site. Hand-shared tabs cannot download files. Ref downloads require a current snapshot after navigation. The tool reads media without clicking the page.
In Extension options, edit Allowed download folders and click Save safety settings. This list is separate from upload folders and defaults to /Volumes/NVme/Projects/Stories/ only when the setting is unset. An empty list disables downloads; an older extension that does not send the policy also leaves downloads disabled. folder optionally selects a configured folder; the default is the first. subfolder is relative to it. Neither agents nor tools can edit the allowed list.
Trusted download hosts is empty by default and saved with the same Save safety settings button. Enter one bare ASCII DNS hostname per line, such as flow-content.google, or a leading *. wildcard such as *.googlevideo.com. Exact entries match only that hostname. *.example.com matches a.example.com and a.b.example.com, but excludes bare example.com, evilexample.com and example.com.evil.net. Matching ignores case and one trailing dot on both entries and URL hosts; surrounding entry whitespace and duplicates are removed. Exact and wildcard entries can coexist.
Only one leading *. label is allowed, with at least two labels in its base. *, *.com, *.google, other wildcard placements, schemes, paths, queries, ports, userinfo, spaces and IP literals (including alternate IPv4 spellings after *.) are rejected. There is no public suffix list check beyond the single-label restriction, so *.co.uk is accepted and trusts all its subdomains. Invalid lines are ignored with specific reasons for up to three lines, plus a count of additional ignored lines; long entries are shortened in the status. Changes sync immediately; missing or malformed policy trusts no cross-origin hosts. Agents and MCP/control actions cannot change this list.
For Google Flow on labs.google / flow.google.com, take a snapshot and inspect a clip's srcHost. Add *.googlevideo.com for per-node clip hosts such as rr4---sn-h5576n7k.googlevideo.com, and flow-content.google for thumbnails. The wildcard excludes bare googlevideo.com. A native same-origin clip URL can also redirect to storage/CDN hosts. A refusal identifies only the refused hostname, for example Download host storage.googleapis.com is not a trusted download host; add it under "Trusted download hosts" in Extension options. Add only hosts you intend to trust, retry, and repeat if another redirect host is reported. Do not paste signed media URLs into the host list. Cookies stay in Chrome; no session cookies are exported to the daemon.
When interception is needed, the Fetch domain is scoped to HTTP(S) page fetch responses for the download's duration. A unique isolated-script initiator and Network request ID identify the download, so simultaneous page requests to the same URL cannot bypass redirect checks. Ambiguous same-source responses wait for that identity; missing identity fails closed. Other requests continue immediately. Service workers are temporarily bypassed for this tab so worker-owned forwarding cannot hide redirects; normal service-worker routing is restored during cleanup.
Cleanup closes the stream, aborts the page request and disables interception, including on failure, navigation, permission revocation or daemon disconnection. If Chrome cannot confirm cancellation or cleanup, the transaction retains interception and its listener, rejects new transfers for that tab, and retries with bounded CDP calls every five seconds. It continues unrelated traffic and fails its own paused requests. Disabling interception before cancellation is confirmed could release a rejected redirect, so this exceptional state deliberately stays closed until cleanup succeeds or the debugger detaches. The daemon publishes the file only after Chrome cleanup succeeds. Operations have timeouts (120 seconds of inactivity and 10 minutes overall). This does not modify file chooser interception and needs no additional manifest permissions. Chrome cookie restrictions and page CSP may still affect a site's requests; initiator metadata and streaming need live Chrome/Flow validation.
Each download is limited to 500 MB (524,288,000 bytes), checked against Content-Length when present and actual streamed bytes in both the extension and daemon (or the fetched Blob size on the page path). The daemon transfers chunks of up to 1 MB into a private .<name>.<id>.ubb-partial file. It atomically publishes the completed file without overwriting existing files or following final-path symlinks. Collisions become scene (1).mp4, scene (2).mp4, and so on. The result is { "saved": "/real/path/renders/scene.mp4", "bytes": 12345, "mimeType": "video/mp4" }.
Filenames use only the basename, remove control characters, separators, colons and leading dots, and are trimmed to 200 characters while preserving the extension. Empty names are rejected. Subfolders cannot be absolute or contain .. segments; each segment is sanitized, created and resolved within the real allowed folder. Symlink escapes are refused before creating further directories outside that folder.
Errors remove the temporary file. For page-Blob downloads, cleanup also attempts to release the page-held Blob; a two-minute page timer releases abandoned fetches and Blobs when normal cleanup cannot run, and active chunk transfers refresh that timer. The interception path uses an abortable isolated-world request with its own ten-minute safety timer. Download operations recheck site access, source/redirect policy and the selected folder throughout transfer.
Annotations
Share a normal HTTP(S) tab with the toolbar icon, then enter annotation mode with Alt+Shift+A (macOS: Control+Shift+A) or right-click the toolbar icon and choose Annotate this tab. The toolbar's normal click still toggles sharing. If the shortcut conflicts, change it at chrome://extensions/shortcuts.
Hover to highlight an element, click to select it, or drag a rectangle. Enter a comment and click Save. Cancel discards the selection; Esc cancels the popup and exits annotation mode. Numbered pins show comments and reply threads on hover or click. Open pins with replies turn green, gain a badge dot, and have an Answered title and accessible label. Resolved pins stay grey even when they have replies. Updates appear on every shared tab at the same exact URL. Reloading that URL restores its pins. Long threads scroll; reply authors and local timestamps appear above plain-text replies.
Each save captures a PNG of the visible viewport with the annotation overlay hidden. Keep that tab active in its window until saving finishes. Metadata includes the selector, a short text snippet, page rectangle, viewport, scroll position, URL, title and timestamps. Pins follow the selector when it still exists, including the region's saved offset from its anchor; otherwise they use the original page coordinates.
The daemon stores private files in ~/.universal-browser-bridge/annotations/: annotations.json and <id>.png. Set UBB_DATA_DIR on serve to change the data root. The daemon is the sole writer and commits files with temporary-file renames. Annotation queries, resolution and replies also work when Chrome is disconnected.
universal-browser-bridge annotations list --status open --json
universal-browser-bridge annotations list --url 'https://example.com/'
universal-browser-bridge annotations get <id>
universal-browser-bridge annotations get --url 'https://example.com/'
universal-browser-bridge annotations resolve <id> --note 'Fixed the heading'
universal-browser-bridge annotations reply <id> --text 'Fixed the spacing' --author HephaestusCLI output is JSON. List defaults to all statuses; URL filters match exactly, including query strings and fragments. get <id> returns one record, and get --url returns an array. Both include an absolute screenshotPath. Resolution is idempotent and preserves the first resolution timestamp and note.
Every record includes replies, an array of {id, author, text, createdAt}. Older records load with an empty array. Reply text is trimmed and must contain 1–10,000 characters. Authors are trimmed and must contain 1–100 characters; omitted authors default to agent. Replying to a resolved annotation preserves its resolved state. annotations reply prints the updated record.
MCP provides the same operations. For an existing ubb <tool_name> '<json>' wrapper:
ubb annotations_list '{"status":"open","url":"https://example.com/"}'
ubb annotations_get '{"id":"<id>"}'
ubb annotations_get '{"url":"https://example.com/"}'
ubb annotations_resolve '{"id":"<id>","note":"Fixed the heading"}'
ubb annotations_reply '{"id":"<id>","text":"Fixed the spacing","author":"Hephaestus"}'Annotations use the existing token-authenticated WebSocket and control socket, with no additional listener. The extension adds scripting, contextMenus and activeTab, with no broad website host permission. Chrome grants activeTab on a toolbar click, annotation shortcut or context-menu invocation; automatic injection still requires the tab to be shared. Same-origin reloads retain the grant. After crossing origins, or on an agent-created tab without a user gesture, invoke the annotation shortcut or context menu again to grant access. Unsharing removes the overlay.
Annotations target the top-level document. Iframes and elements inside shadow roots cannot be individually selected. Screenshots show only the viewport; coordinates and selectors may become less accurate after major page changes. Data remains local until an agent reads it through the bridge or you enable a webhook.
Annotation webhook
To POST each newly saved annotation to an HTTP(S) endpoint, create <dataRoot>/webhook.json. The data root is UBB_DATA_DIR on serve, or ~/.universal-browser-bridge by default:
{
"url": "https://hooks.example.com/annotations",
"headers": {
"Authorization": "Bearer YOUR_TOKEN",
"X-Sender-Key": "YOUR_SENDER_KEY"
},
"enabled": true
}Keep the directory private (mode 700) and the config file private (mode 600, for example chmod 600 ~/.universal-browser-bridge/webhook.json). The CLI does not write this file. headers defaults to {} and enabled defaults to true. Set enabled: false to disable delivery. Set UBB_ANNOTATION_WEBHOOK_URL on serve to override the URL and enable delivery, including without a file; valid file headers still apply. File edits are read on the next send without restarting the daemon. A missing file with no override is silently disabled. Invalid JSON/configuration disables delivery, even with an env override, and emits one warning until the configuration becomes valid again.
Each successful new save sends this JSON metadata, never PNG bytes:
{
"event": "annotation.created",
"annotation": {
"id": "00000000-0000-4000-8000-000000000001",
"comment": "Fix the heading",
"url": "https://example.com/",
"title": "Example",
"textSnippet": "Page heading",
"selector": "#heading",
"createdAt": "2026-10-10T10:00:00.000Z",
"screenshotPath": "/absolute/data/root/annotations/00000000-0000-4000-8000-000000000001.png"
}
}Requests always use Content-Type: application/json. User-Agent: universal-browser-bridge is added unless overridden (header names are case-insensitive). Delivery runs asynchronously after saving and never delays or fails the extension's save response. Queries, replies and resolutions do not send webhooks.
Each attempt has a 10-second timeout. Network failures, timeouts, HTTP 429 and 5xx retry up to three times, with 1s, 2s and 4s backoff (four attempts total, about 47 seconds maximum). Other HTTP errors do not retry, and redirects are not followed. Failures log only the endpoint host and HTTP status or a generic error, never header values or URL paths/queries. Retries can deliver the same event more than once; consumers should deduplicate by annotation ID. Delivery is best-effort in memory, with no durable queue or restart recovery.
universal-browser-bridge annotations webhook status
universal-browser-bridge annotations webhook testBoth commands use the authenticated daemon control socket, so serve must be running. status prints {configured, enabled, source, host, headerNames} with source file, env or none, and never prints header values. test uses the same sender, headers, timeout and retry policy to send {"event":"annotation.test","sentAt":"<ISO timestamp>"}. It prints {ok, attempts} plus a status code or error, and exits non-zero for disabled configuration or failed delivery.
After updating, reload the extension at chrome://extensions and rebuild/restart the daemon and MCP adapters. Live annotation interaction in Chrome is a separate manual check from the automated mock, socket and process tests.
Available MCP tools
Tool | Purpose |
| Show extension connection state, shared tabs and caller-owned tabs |
| List shared tabs and caller-owned tabs |
| Open a tab in the agent window; labelled ownership survives reloads when its exact group and window still validate |
| List the caller label's current or validated restored tabs as tabId, url, title and groupId |
| Close a caller-owned tab or a tab otherwise legitimately shared with the caller |
| Read compact interactive elements and receive a |
| Navigate a shared tab to an allowed URL |
| Scroll to and click a snapshot element with trusted mouse input (script fallback); consequential approval applies unless the caller's owned tab is trusted |
| Replace the contents of a snapshot input element with user activation |
| Attach up to 10 allowed local files through an input or intercepted chooser in a caller-owned tab; modes: auto, input, chooser |
| Save same-origin, trusted HTTPS (exact hosts or explicit leading |
| Send trusted keyboard input; Enter and NumpadEnter require approval unless the caller's owned tab is trusted |
| Scroll a shared page by pixels |
| Capture a PNG screenshot of a shared tab |
| List saved annotations by exact URL and status |
| Get an annotation by ID or all annotations for a URL |
| Resolve an annotation and update live pins |
| Append a reply and update live pins, including on resolved annotations |
browser_click and browser_type require both the element ref and snapshotId returned by browser_snapshot. Take another snapshot after navigating.
browser_click scrolls the element into view and checks its center with document.elementFromPoint. When the center is visible, has size and hits the element or a descendant, it sends trusted CDP mouse move, left-button press and release events. Zero-size, covered or offscreen targets use a script click with userGesture: true instead. The result is { clicked: true, method: "mouse" | "script" }. The fallback click event remains synthetic, but the script runs with user activation for actions such as downloads and file pickers. browser_type also runs its focus/value script with user activation. browser_press uses trusted CDP key events and retains approval for Enter and NumpadEnter.
Running automatically
The daemon is deliberately a foreground process by default. For long-running use, supervise it with a tool appropriate to your operating system, such as:
macOS: LaunchAgent
Linux: systemd user service
Windows: Task Scheduler or a user service wrapper
Cross-platform development: tmux or another terminal multiplexer
The daemon must run under the same user account that owns the token and control socket. Do not expose it through a public proxy.
Standalone binary
With Bun installed, compile the CLI and dependencies into a self-contained executable:
npm run binaryOutput:
bin/universal-browser-bridgeRun it with:
./bin/universal-browser-bridge token
./bin/universal-browser-bridge serve
./bin/universal-browser-bridge mcpCross-compile by selecting a Bun target such as bun-linux-x64, bun-windows-x64, bun-darwin-x64, or bun-darwin-arm64.
Configuration
Variable | Applies to | Purpose |
|
| Token path; defaults to |
|
| Supply the token directly instead of reading a file |
|
| Control socket path; defaults to |
|
| Annotation data root; defaults to |
|
| Override and enable the annotation webhook URL; retain valid file headers |
|
| Extension WebSocket port; defaults to |
|
| Persistent agent ownership label, group title and approval identity; |
|
| Set to |
If you change UBB_PORT, update the extension Relay URL to match:
ws://127.0.0.1:<port>/extensionToken rotation
To invalidate the current token:
Stop the daemon and all MCP adapters.
Delete or move
~/.universal-browser-bridge/token.Run
npm run tokento generate a fresh token.Paste the new token into the extension Options page.
Restart the daemon and MCP clients.
A token rotation disconnects every process still using the previous token.
Testing
Automated tests without Chrome
npm run check
npm test
npm run buildThe test suite includes real local sockets and real subprocesses. It starts a daemon, multiple MCP adapters, and a fake extension WebSocket peer. No Chrome interaction is required.
Restricted environments can run the socket-free tests, including the real extension/bridge/daemon/MCP ownership, trust and blocklist flows over mocked socket and Chrome boundaries:
npm test -- --exclude src/bridge.test.ts --exclude src/daemon.test.ts --exclude src/background-owner-race.test.ts --exclude src/cli-integration.test.tsThose four files require local listeners, and the CLI integration also requires the tsx IPC socket. An EPERM listener failure is an environment restriction; the socket-free run does not replace real-socket or live Chrome acceptance.
Live Chrome E2E
First load and pair the extension, then run:
npm run e2eThe E2E suite:
Starts a real daemon
Connects two MCP adapters simultaneously
Creates an agent-owned tab
Verifies cross-adapter ownership isolation
Exercises snapshots, navigation, keys, scrolling, clicking, typing, and screenshots
Rejects
file://navigationRejects stale snapshot generations
Waits 45 seconds to verify Manifest V3 service-worker keepalive
Closes the created tab and cleans up all subprocesses
By default the E2E daemon uses an ephemeral port to avoid colliding with an existing daemon. The script prints the selected URL; update the extension Relay URL to that value. To use a stable test port:
E2E_PORT=17322 npm run e2eThen set the extension Relay URL to:
ws://127.0.0.1:17322/extensionThe agent-controlled window opens without a prompt by default. If you turned off Let agents open their window without asking, approve the window creation manually; the test never approves Chrome prompts itself.
Troubleshooting
Extension does not connect
Confirm
serveis running.Confirm the Relay URL uses the daemon's printed port.
Confirm the token in extension Options matches
npm run token.Click Save and connect after editing either field.
Confirm Chrome's Loaded from path points to the intended
extension/directory.Reload the extension after changing
extension/*.js.Restart the daemon after rebuilding
src/*.ts.
You can inspect daemon health locally:
curl http://127.0.0.1:17321/healthA successful pairing reports "connected": true.
The extension sends its existing 20-second ping while connected. A ubb-keepalive Chrome alarm runs every 30 seconds and reconnects when the socket is closed or unauthenticated, including after the service worker has stopped during a longer daemon outage. This version adds the alarms permission; reload the extension to apply the manifest change.
Daemon logs and detach diagnostics
serve writes logs to stderr, shown in its terminal or captured by your process supervisor. There is no built-in log file. To capture one yourself:
npm run serve 2>>/tmp/ubb-daemon.logEvery daemon log line, including startup, shutdown, webhook errors and diagnostics, has a local ISO timestamp with its UTC offset, for example [2026-10-20T12:34:56.123+05:30]. MCP stdout and CLI JSON output retain their existing format.
The log records extension connection/disconnection, restored tab ownership, and structured extension diagnostics with event, tabId, reason, optional label, and the event's at timestamp. Detach diagnostics use event: "debuggerDetached" and preserve Chrome's reason. Up to 50 diagnostics are queued in the worker while disconnected and flushed on reconnection.
canceled_by_user immediately revokes access, removes durable ownership and cancels recovery. For other reasons such as target_closed, a labelled tab that still exists in its exact owner group and window gets one re-attach attempt after about 500 ms. Attempts are limited to three per tab per minute. Commands are refused while recovery is pending. Failed validation, attachment failure or the rate limit drops ownership and is logged; a real closed tab cannot be recovered. Successful recovery logs debuggerReattached. Hand-shared tabs retain the existing revocation behavior.
Toolbar badge meanings
Badge | Meaning |
No badge | Connected or not currently reporting an error |
| Pairing token is missing from extension settings |
| Relay is disconnected |
| This tab is explicitly shared |
| Tab sharing failed; hover the toolbar icon to see the reason |
ERR badge when sharing a tab
Hover the toolbar icon to see why sharing failed.
Cannot access a chrome-extension:// URL of different extensionmeans another extension (e.g. a password manager or Grammarly) has a frame in the page. Openchrome://extensions, select that extension's Details, set Site access to On click, and reload the tab before trying again.Another debugger is already attachedmeans DevTools or another debugging extension is attached to that tab. Close DevTools or disconnect the other extension from the tab, then try again.
EADDRINUSE on port 17321
Another daemon or older single-process bridge is already listening.
lsof -nP -iTCP:17321 -sTCP:LISTEN # macOS/LinuxStop the old process or choose another UBB_PORT. Only one serve daemon should own a given extension port.
MCP reports that the daemon is not running
Start serve separately. The mcp command intentionally does not launch a hidden daemon:
npm run serveShared tab disappears
Confirm the tab remains in its exact label group, or Agent Bridge for a hand-shared tab.
Renaming the group or dragging the tab out revokes access by design.
Clicking the extension icon toggles sharing.
Chrome may refuse debugger attachment while DevTools is attached to the same target.
Inspect the daemon's
debuggerDetached,ownershipDroppedanddebuggerReattachFaileddiagnostics for the reason.
New unpacked folder lost its settings
Chrome assigns unpacked extensions an identity based partly on their loaded directory. Loading the same files from another checkout or worktree may create a separate extension with empty storage. Re-enter both the Relay URL and pairing token.
Changes are not taking effect
TypeScript/daemon change: run
npm run build, then restartserveand MCP adapters.Extension JavaScript change: click Reload in
chrome://extensions.Manifest change, including the new alarms permission: reload the extension. Labelled tabs restore only after their stored group and window pass validation.
MCP client configuration change: restart the MCP client or start a new agent session.
Security boundaries
All network listeners bind to IPv4 loopback.
The extension accepts only
chrome-extension://WebSocket origins with the pairing token.The local control socket is owner-only and also token-authenticated.
Only explicitly shared tabs are attached through
chrome.debugger.Password input values are omitted from snapshots.
File uploads require live ownership by the caller's label, writable/unblocked sites, and user-configured allowed folders; the daemon validates real paths, file types and sizes, and the extension independently guards attachment.
Downloads require a caller-owned, writable, unblocked tab and a separately configured allowed download folder. The daemon and extension independently enforce trusted HTTPS hosts (exact entries or explicit leading
*.wildcards that exclude the apex), every redirect before continuation (10-hop cap), the tab blocklist and destination-host blocklist. Wildcard bases require at least two labels; there is no public suffix list check, so*.co.ukis accepted. Trusted hosts default to empty and can only be changed in Extension options. Cookies remain inside Chrome. Source discovery and refusal errors expose hostnames only, never signed source paths or queries. The daemon enforces the 500 MB cap and real destination paths; the extension also checks streamed size, ownership, site access and the selected folder. Chunk transfers use private temporary files and atomic publication without overwrite. Streams and interception are cleaned up on failure. MCP tools cannot change download permissions.Chooser uploads retain those checks and consequential-click approval. The extension rechecks access before each operation, limits interception to one upload per tab, and restores the picker shim and interception after the call. If access is revoked mid-call, cleanup still attempts to disable interception and restore the shim.
Navigation and new tabs accept only allowed
http:andhttps:URLs.Localhost and private-network destinations are blocked unless explicitly enabled.
Consequential clicks and Enter presses require Chrome-hosted human approval unless the extension's trust setting is enabled and the live tab belongs to the caller's label.
Creating the agent window requires approval only when Let agents open their window without asking is turned off. It is enabled by default, so any local process holding the bridge token can open agent tabs in the signed-in Chrome without a prompt.
Bank, card, payment and password-manager pages always remain read-only, regardless of either trust or automatic window creation settings.
The daemon rechecks writable policy before CDP calls marked
requiresWritable. The extension independently treats everyInput.*method andDOM.setFileInputFilesas a write, even if the caller omits that flag. Trusted input and script user activation retain tab ownership, blocklist, snapshot and approval checks.Blocklisted domains and their subdomains refuse all page operations in owned and hand-shared tabs, with current and pending URL checks in both the daemon and extension.
The model cannot approve its own request.
Agent-created tabs belong to their creating adapter while it is connected.
Durable labelled ownership requires a stored record and an exact label/group/window match. Group titles alone never grant ownership. Explicit debugging cancellation and revoked durable records cannot trigger automatic re-attachment or restoration.
Per-tab queues prevent concurrent agents from interleaving actions.
Snapshot generations prevent old references from acting after navigation.
Dragging a tab out of the Agent Bridge group revokes access.
Do not share banking, password-manager, payment, healthcare, or other highly sensitive tabs. Web content can contain prompt injection, and the consequential-action heuristic cannot recognize every risky interaction.
Known limitations
A local malicious process that binds the extension port first could capture the pairing token when the extension connects. Rotate the token if you suspect local compromise.
Client identity is process-scoped. A harness that launches a fresh MCP adapter per turn or subagent gets a new identity.
UBB_CLIENT_LABELimproves attribution but is not an authorization credential.The confirmation heuristic is conservative but not comprehensive.
The extension is currently distributed as an unpacked extension rather than through the Chrome Web Store.
The daemon is not installed as an operating-system service automatically.
Development notes
Extension code is plain JavaScript; no browser build step is required.
Daemon and MCP code is TypeScript compiled to
dist/.npm testcovers unit, socket, process, ownership, and Chrome-event-ordering behavior.The live E2E was last verified at 17/17 passing with two simultaneous adapters and real Chrome.
Roadmap
Stable logical identities and expiring grants
Domain policies and sensitive-site defaults
Append-only action audit log
Prompt-injection detection and untrusted-page labelling
Native adapters for additional agent harnesses
Chrome Web Store packaging and reproducible releases
License
MIT. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Let AI agents observe and control Chrome tabs you explicitly share via Browse Bridge.
Connect AI agents to your own Chrome with scoped access, approvals, and verified task results.
Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.
Control a real browser from AI agents: navigate, click, type, screenshot, read the page.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceReal browser tabs for AI agents through a safe localhost MCP bridge, enabling control of Chrome tabs via MCP-compatible AI CLIs.2Apache 2.0
- AlicenseNot gradedqualityCmaintenanceConnects AI agents to your Chrome browser via MCP, enabling real-time control of existing tabs, sessions, and application state for development workflows.MIT
- AlicenseNot gradedqualityBmaintenanceEnables remote AI agents to control a local, authenticated desktop browser over MCP through an outbound WebSocket bridge and Chrome extension, supporting tab management, navigation, page reading, clicking, typing, scrolling, screenshots, and JavaScript evaluation.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to create, read, and operate granted background tabs in Chrome via a local stdio MCP server, preserving the human's front tab. Supports tab groups, navigation, clicking/typing, screenshots, and human-priority permissions.MIT