Skip to main content
Glama

Tabduct

Give your CLI coding agent a handle on the real browser you're already using — the tabs you're logged into, not a throwaway sandbox.

CI license manifest node local protocol

Tabduct is a tiny, local, agent-agnostic bridge. It exposes your already-open, already-logged-in browser tabs to any agent that speaks the Model Context Protocol (MCP) — Claude Code today; Kilo, OpenCode, Cursor, and anything MCP-capable tomorrow.

No built-in chat. No embedded LLM. No vector DB. No telemetry. No native modules. It does exactly one thing: hand your agent the tab you point it at — under your consent, on your machine only.

   CLI agent (Claude Code / Kilo / OpenCode / …)
        │  MCP  (streamable HTTP, 127.0.0.1)         ← standard, language-neutral
        ▼
   Tabduct host   (Node — the reference host)         ← implements /protocol
        │  Chrome Native Messaging (stdio)           ← Tabduct wire protocol
        ▼
   Tabduct extension  (MV3 background service worker) ← the one shared impl
        │  chrome.tabs / chrome.scripting
        ▼
   Your live browser tab (cookies, sessions, DOM)

Why Tabduct

  • Your real session. The agent works with your logged-in tabs — no re-login, no captchas, no throwaway profile.

  • Local-only & private. Binds 127.0.0.1, guarded by a per-session bearer token. Nothing ever leaves your machine — no server, no telemetry, no external calls.

  • You're always in control. Default-deny consent: share one tab or everything, block- or allow-list origins, read-only mode, auto-expiry, and a visible "⚡" group of shared tabs (drag a tab in to share it).

  • Agent- and language-agnostic. MCP to the north, a tiny documented wire protocol to the south. One extension is the fixed point; every host is a thin adapter.

  • Minimal & auditable. Reference host ~1–1.5k lines, zero native dependencies.

Related MCP server: Chromanche

Quickstart

Runs on macOS, Linux, and Windows, with Chrome, Chromium, Edge, or Brave. Requires Node ≥ 18.

git clone https://github.com/ultrathinker/tabduct.git && cd tabduct
npm install
npm run register        # installs the native-messaging manifest for your OS + browser
                        # other browsers: node hosts/node/bin/tabduct.js register --browser edge|brave|chromium

register writes the manifest to the right place automatically — ~/Library/Application Support/…/NativeMessagingHosts on macOS, ~/.config/…/NativeMessagingHosts on Linux, or an HKCU registry key on Windows (and makes the launcher executable on POSIX). Then:

  1. Open chrome://extensions → enable Developer mode → Load unpacked → select the extension/ folder.

  2. Click the Tabduct toolbar icon → Start (launches the local server; the header dot turns green). Click the dot again to Stop: a dialog asks what you mean, because the hub is shared by every connected browser: restart the hub for all browsers (shared tabs stay shared; the way to pick up new code), stop in this browser only, or stop for all browsers (every browser disconnects and stays stopped until you press Start in it).

  3. Open Settings (⚙) → copy the MCP endpoint and Authorization token.

  4. Paste them into your agent's MCP config (below) and reload the agent.

  5. Share what the agent may touch: Share Current Tab, or Share Everything. That's it.

Diagnose the host anytime with npm run doctor. In-app help lives under Settings → How it works.

Point your agent at it (MCP)

Use the Node host. The "shared hub" below is implemented only in hosts/node/src/hub.js, and the current extension requires it. The Python and .NET hosts are direct-mode ports that predate the hub (see the host table below), so they cannot be used with this extension yet.

With the shared hub (on by default when using the Node host), every browser you connect appears behind one stable endpoint with a token that never changes:

{
  "mcpServers": {
    "tabduct": {
      "type": "http",
      "url": "http://127.0.0.1:12311/mcp",
      "headers": { "Authorization": "Bearer PASTE_TOKEN_FROM_SETTINGS" }
    }
  }
}

Reload your agent and it discovers the Tabduct tools below. Start Tabduct in one browser and any other browser you open joins the same hub automatically — you always point your agent at 127.0.0.1:12311, never at a per-browser port.

Tip — add it at the global / user scope. Some agents (Claude Code included) scope MCP servers to the directory you configured them in, so a session launched from another folder won't see Tabduct. Register it globally instead — e.g. in Claude Code: claude mcp add --scope user tabduct http://127.0.0.1:12311/mcp --header "Authorization: Bearer <token>". Cursor and most GUI clients already store MCP servers globally.

Tools

Tool

What it does

list_tabs / get_active_tab

Enumerate / get the focused tab — filtered to shared tabs only

get_page_content / get_dom_snapshot

Read a shared tab's text/HTML, or a compact outline of its interactive elements

screenshot

Capture the visible tab (returned as an MCP image)

click / type

Click an element / type into a field (or pick a <select> option), by CSS selector. trusted: true sends real browser-level input

press_key

Press a key (Enter, Tab, Ctrl+C…) as real keyboard input — terminals, custom widgets

wait_for

Wait for a selector, a piece of visible text, a URL fragment, or a load state (bounded; any one of them ends the wait)

navigate

Point a shared tab at a URL

open_tab / activate_tab / close_tab

Tab management

get_console_logs

Read the tab's console output (plus uncaught errors, in CDP mode)

list_network_requests / get_network_request

Inspect captured network traffic (CDP developer mode)

execute_script

Run arbitrary JS in a shared tab — read and modify the page

list_frames

List the iframes inside a shared tab — target one with frameId

Frames. Embedded forms and widgets often live in an iframe from another domain (HubSpot, Typeform, payment and sign-up forms), where page scripts can't reach. get_dom_snapshot outlines each visible frame under its own --- frame N --- header, and the page tools (get_page_content, get_dom_snapshot, click, type, wait_for, execute_script, get_console_logs) take a frameId to work inside it. The origin filter applies to each frame's own site.

Trusted input. Terminals (xterm.js — AWS CloudShell), Cloudscape/Material dropdowns and canvas editors only react to events the browser itself generated. type / click with trusted: true, and press_key, send exactly that through the DevTools Protocol. It needs the same opt-in as CDP eval (Allow CDP eval, read-only off) and works inside cross-origin frames for typing and keys. A terminal: click it (or type into its helper textarea, .xterm-helper-textarea) with trusted: true, then press_key Enter. Paste shortcuts are refused (they would read your clipboard).

Most tools — including click / type / wait_for / get_dom_snapshot — run as injected functions, so they work even on strict-CSP sites (GitHub, banks, SaaS). Only arbitrary-string execute_script is blocked by a page's CSP; for that, opt into CDP mode (see below). Unshared tabs are completely invisible — the agent can't even read their title.

Using Tabduct without disturbing the user (rules for agents)

Tabduct works with the user's real browser, so an agent that raises the window interrupts a game, a call or a presentation. The rules, also written into the tool descriptions:

  1. Read in the background. get_page_content, execute_script, get_dom_snapshot, list_frames, console and network reads work on a minimized window or a background tab. Prefer them. A screenshot needs a drawn page: use it only when text will not do.

  2. For polling and scheduled checks pass quiet: true. The call never brings the window forward, never activates a tab; when the page cannot answer without that you get an error that says so, and you can report it instead of interrupting the user.

  3. Without quiet, where the user allows waking the tab, screenshot, type, click, press_key, get_page_content and get_dom_snapshot may bring a minimized or covered window forward and put it back afterwards. activate_tab, screenshot with activate:true and open_tab with active:true always take the user's focus: last resort only.

  4. The user decides, per tab, whether a tab may be woken (the 🔔 / 🔕 next to the tab's ✕ in the Tabduct popup; new shares take the default from the settings, on) and can switch waking off for every tab at once with Silent mode. list_tabs shows wakeAllowed:false for a tab you may not bring forward: activate_tab and screenshot with activate:true are then refused with WAKE_NOT_ALLOWED, and in Silent mode open_tab opens the tab in the background. Respect it: do not retry, work with the tab where it is, or tell the user.

  5. A page Chrome has frozen is thawed silently (see below) before the call. If that fails and the call is quiet:true (or waking is off for the tab) you get TAB_FROZEN at once, with the reason; repeat the call without quiet to wake it (the window is brought forward, which the user allowed by default), or leave it. A call the page does not answer ends with TIMEOUT after 18 s: look for a dialog ("Leave site?") instead of retrying blindly, and do not call location.reload() from execute_script on a tab the user has been using.

Why a tab stops answering. Chrome slows timers in a hidden tab (once a second, once a minute after 5 minutes), can unload it from memory (Tabduct marks shared tabs non-discardable) and, with Energy Saver on, freezes one that has been hidden and silent for more than five minutes: a frozen page runs nothing, so every call to it hangs. Tabduct sees tab.frozen and thaws the page through the debugger (Page.setWebLifecycleState), which needs Allow CDP eval on, and keeps the debugger attached to that tab for the call and 20 s after it (Chrome does not freeze a page that is being inspected) and tries up to three ways of thawing in turn. Chrome 154 does not let the debugger thaw every kind of freeze (reproduced on a tab Chrome froze by itself; one frozen from chrome://discards thaws), so the dependable way is the window, as when you click the tab yourself: a call without quiet brings the window forward where waking is allowed, and the call goes on. A quiet call, or one on a tab where waking is off, gets TAB_FROZEN with what each way of thawing did. list_tabs shows frozen: true. Chrome has no extension API for its "Always keep these sites active" list, so Tabduct does not touch it.

The endpoint is token-authenticated — not merely bound to localhost (which every local process shares). On Start the extension mints a bearer token; the host requires Authorization: Bearer <token> on every request, rejects Origin-bearing requests, and pins the Host header (DNS-rebinding defense).

Consent is default-deny and enforced inside the extension (the sole path to the browser). All of these are in the popup:

  • Origin filter — Block mode (listed sites are never shared) or Allow mode (only listed sites can ever be shared). Overrides every sharing mode.

  • Lock shared tabs to their domain (default on) — a shared tab that navigates to another site is paused: the agent is refused and the tab disappears from its list, so a shared shopping tab can't follow you into your bank; the share is kept and resumes when the tab is back. It is a live setting — switching it off frees tabs you already shared.

  • Read-only — the agent may look but never click, type, navigate, run scripts, or open/close tabs.

  • Auto-expire — un-shares tabs after a chosen time (5 min … 10 h), counted from when each was shared or from when you changed the setting.

  • Let the agent wake a tab by bringing the browser forward (default on; the starting value for each tab you share) — a minimized window, a window covered by others, a background tab or a tab Chrome has frozen does not draw or answer, so screenshot, type, click, press_key and page reads come back stale, ignored or never come back. Where waking is allowed the extension brings that window forward for the call and puts it back afterwards (a minimized one is minimized again a few seconds after the last call; Windows gives the focus back to the app you were in). A window you are working in is never touched. Each shared tab has its own 🔔 / 🔕 switch next to its ✕ in the popup (it works for the tabs of the other browsers listed there too), and the Silent button in the popup header switches waking off for every tab in every browser at once (a mute mark shows on the toolbar icon); your per-tab choices are kept and come back when you turn Silent mode off. In Silent mode an agent also cannot take the focus with activate_tab, and open_tab opens tabs in the background. A tab Chrome unloaded to save memory is never woken, since that would reload the page; shared tabs are marked as not discardable while shared.

  • Full access / Safe defaults — one click over the flags above for "let the agent work freely on what I shared" (lock off, read-only off, no expiry, CDP eval on), or back to the safe side (lock on, CDP options off; read-only and expiry stay as you set them). The origin list is never touched.

  • Don't auto-share tabs the agent opens (default on).

  • CDP mode (Advanced, opt-in, default off) — lets execute_script bypass a page's CSP via the DevTools Protocol, with an optional "developer mode" that routes all eval through it and full console/error capture. Chrome forbids requesting debugger at runtime, so it's a required permission granted at install — but nothing attaches until you flip this toggle on, and use is still gated by consent (never in read-only). Chrome shows a "being debugged" banner whenever it's actually in use.

  • Sharing resets when the browser restarts (so nothing stays shared by accident), but survives reloading the extension — handy after a git pull.

The full trust model and honest limitations are in SECURITY.md — which is also where to report a vulnerability (please don't open a public issue).

Multiple browsers & profiles

This section describes the Node-host hub. The Python and .NET hosts expose one per-instance endpoint each, do not aggregate behind a shared endpoint and do not work with the current extension.

Install Tabduct in each Chrome profile you use (each Google account / profile is separate). Start it in one browser — any other profile you open joins the same hub automatically (no need to Start each). They all sit behind the one endpoint (127.0.0.1:12311), and the agent tells them apart by their Label (auto-named like Chrome-abcd — rename to Work / Personal in Settings).

The popup gives you one place to manage all of them: the shared-tab list is grouped by browser (Current first, then each other browser that's sharing something), so you can see everything that's exposed across every profile at a glance. The ✕ next to any tab unshares it even on another browser; a browser in Share-Everything mode shows a single ⚡ Sharing all tabs row you can switch off the same way; and a Revoke all sharing link clears every browser at once. (Cross-browser management flows through the hub over a separate, token-authed control channel that the agent never sees — it can only ever reduce sharing, never grant it; see protocol/PROTOCOL.md §11a.)

Two protocols, one extension, many hosts

Tabduct is defined by contracts, not implementations:

  • North (agent ↔ host): MCP. Already standardized; SDKs for Node, Python, .NET. Nothing to invent.

  • South (host ↔ extension): the Tabduct wire protocol. Chrome Native Messaging framing + message schema + tool catalog. Specified once in protocol/ — the single source of truth.

  • The extension is the fixed point (it must be JS): it defines what the browser can do; every host is a thin relay of MCP calls to it (~1k lines in any language — see the per-host counts in docs/ARCHITECTURE.md).

Host

Status

Hub

Notes

hosts/node

✅ reference impl

✅ yes

zero native deps, Node ≥ 18, MCP SDK wired, conformance-passing; ships the shared hub facade (hosts/node/src/hub.js)

hosts/python

⚠ direct mode only

❌ no

official mcp SDK + register (macOS/Linux/Windows); predates the shared hub and the version/feature handshake, so the current extension (which requires the hub) can't use it

hosts/dotnet

⚠ direct mode only

❌ no

ModelContextProtocol SDK, net10.0 + register; same limitation

Hub is currently a Node-host feature. The shared hub (one stable endpoint behind which every connected browser appears) is implemented only in hosts/node/src/hub.js. The Python and .NET hosts do not read payload.hub and expose only their own per-instance MCP endpoint. See docs/ROADMAP.md for plans to bring the hub to the other hosts.

New languages need no permission — implement protocol/PROTOCOL.md and pass protocol/conformance/.

Project layout

extension/            MV3 extension (the fixed point): consent, sharing, popup, icons
hosts/node/           reference host — CLI (register/doctor/run/instances/hub) + src/
protocol/             PROTOCOL.md + JSON schemas + conformance runners
docs/                 ARCHITECTURE, DESIGN-consent-and-multibrowser, ROADMAP
scripts/              tests (consent, store, handlers, gate, wake, host; opt-in real-Chrome e2e), icon/key generators

Run the full test suite (pure JS, no browser needed): npm test — the consent, store, handler, wake, gate and host tests (mock chrome / fake host) + host conformance + hub conformance. Two opt-in runs use a real, throwaway Chrome: npm run test:e2e (headless) and npm run test:e2e-wake (opens a window for a few seconds).

Status

Working reference implementation, pre-1.0. Developed and exercised on Windows; the macOS/Linux code paths are implemented (per-OS manifest install, POSIX file modes, launcher chmod) but deserve a smoke test on each before you lean on them. See docs/ROADMAP.md.

Originality

Tabduct is written from scratch. It reuses no third-party source code — only standard, public interfaces: Chrome's Native Messaging framing (a documented OS transport) and the Model Context Protocol. Nothing here carries a third-party attribution obligation.

License

MIT — see LICENSE.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Related MCP Connectors

Related MCP Servers