Yoke drives the Chrome you are already using: an extension plus a native messaging host, so there is no browser to launch, no second profile, and no browser-wide debugging port. Chrome spawns the host itself when the extension connects, and the endpoint in between (a Unix socket at mode 0600 inside a directory whose 0700 mode is verified, or a named pipe on Windows) exists only while that connection is live. There is one per Chrome profile, so two profiles with the extension loaded are both reachable and told apart. It does use the Chrome DevTools Protocol, one tab at a time, through the extension's own debugger permission, which is why a tab with DevTools open cannot be driven.
list_tabs returns every tab in every window of every connected Chrome profile, and every operation on an existing tab names a tab_id, because there is no acting on the active tab when none was named. Any tab yoke drives or reads joins a visible tab group called yoke, and read_page and find describe a password field without returning its value. URL fields in tool output are cut to origin and path by default, with full_urls: true on list_tabs as the way to opt out, and the package has zero runtime dependencies on Node 22 or later.
Chrome requires a three part bridge:
MCP client <--stdio--> yoke mcp server <--Unix socket--> native host <--connectNative--> Chrome extension
The MCP client starts yoke mcp. Chrome starts the native messaging host when the extension connects to it. The server reaches that host through a local socket. A client cannot connect straight to the extension because Chrome only gives native messaging connections to hosts that Chrome started.
The project and repository are named yoke. The npm package is yoke-mcp because the bare package name belongs to an unrelated project. The command is yoke, and the extension is displayed as Yoke.
Every operation on an existing tab requires a tab_id, with no active tab fallback. open_tab creates a tab and therefore has no existing id to name.
Yoke runs through an extension in the Chrome profile you are already using. It can reach every tab in every window of that profile, with its existing cookies, logged in sessions, and other extensions. Load it in a second profile and both are reachable at once: list_browsers names each, list_tabs covers both, and a tab id is enough to route a call to the profile that owns it, because Chrome numbers tabs once for the whole browser.
Yoke uses chrome.debugger, which is Chrome's extension API for the Chrome DevTools Protocol. It attaches to one tab at a time and does not require a Chrome launch flag or a browser-wide remote debugging port. The local bridge is a per-profile Unix socket or Windows named pipe that exists only while Chrome has the extension connected.
Any tab yoke drives or reads joins a visible tab group named yoke. Grouping is a visual marker and is not used to find a tab. Chrome permits one debugger client per tab, so yoke cannot drive a tab while DevTools is open on it.
URL fields are reduced to origin and path by default, and list_tabs can opt out with full_urls: true. This redaction does not filter page text, console messages, evaluated JavaScript results, or screenshot pixels. read_page and find describe password fields without returning their values.
The package declares zero runtime dependencies and uses Node builtins only. The published 0.1.6 tarball is 47 files, all first-party.
TypeScript throughout with strict and noUncheckedIndexedAccess on. The wire
shapes are declared once in src/protocol.ts and imported by both halves, so a
request the server can send is one the extension is compiled to answer, and the
compiler is what enforces that rather than a comment asking nicely.
The permissions and what each one buys are in a table further down, along with
the native messaging setup and the reasons debugger and host access are
required. None of that makes yoke safe: it drives a browser you are signed into,
which is privileged by construction. It is documented so the trade is yours to
judge rather than yours to discover.
The cells describe the verified versions and their defaults. Flags that change an answer are named in the cell or the numbered notes.
| Capability | Yoke (npm 0.1.6) | @playwright/mcp 0.0.79 (2026-08) | chrome-devtools-mcp 1.7.0 (pub. 2026-08-10) | BrowserMCP: server 0.1.3 (2025-04-11), extension 1.3.4 (2025-05-07), unmaintained |
|---|---|---|---|---|
| Runs in the Chrome profile you are already signed in to | Yes. Chrome spawns the host via connectNative; there is no browser-launch code |
No by default: its own persistent profile keyed to the client's cwd [1]. Yes with --extension |
No by default: its own profile at ~/.cache/chrome-devtools-mcp/chrome-profile. Yes with --autoConnect [2] |
Yes. A Web Store extension in the running browser |
| Reaches tabs that were already open | Yes, every tab in every window of every Chrome profile the extension is loaded in [3] | No by default (fresh browser, so those tabs do not exist in it). Yes with --extension [4] |
No by default. With --autoConnect, all windows of one profile [2] |
One tab at a time, picked by the human in the popup. No tab listing and no tab creation tool [5] |
| Explicit target on every operation | Yes, tab_id, with no active-tab fallback anywhere [6] |
No. Implicit current page, switched by browser_tabs with a positional index [7] |
No. Server-side selected page via select_page [8] |
No. The single connected tab; no tab id in any of the 12 exposed tool schemas |
| Needs a Chrome launch flag or a browser-wide debugging port | No | No for the default launch and for --extension; yes for --cdp-endpoint [9] |
Yes on all three attach paths; no when it launches its own browser over a pipe [10] | No |
| How it reaches the browser, and what listens locally | Native messaging. A per-user endpoint (Unix socket mode 0600 in a directory whose 0700 mode is verified; a named pipe on Windows) that exists only while Chrome has the extension connected. CDP one tab at a time via chrome.debugger 1.3 |
Playwright drives a browser it launched. --extension starts an in-process CDP relay the extension dials over WebSocket |
Puppeteer over a pipe for its own browser; a CDP HTTP endpoint or ws:// URL for the three attach paths |
WebSocket server on fixed port 9009, bound to all interfaces, no auth and no origin check, and it kills whatever already holds that port [11] |
| Runs with your existing cookies and logged-in session | Yes | No by default. Yes with --extension [12] |
Only with --autoConnect [13] |
Yes |
| Marks the tabs it touches | Yes. Any tab it drives or reads joins a visible tab group named "yoke" [14] | No in-tab marker. A separate headed window on its own profile, with automation signals actively suppressed [15] | Nothing added by the server. Chrome's own affordances only, and on --autoConnect that is a one-time consent dialog rather than an ongoing marker [16] |
Chrome's own debugger infobar, incidental rather than designed, and suppressible [17] |
| URL redaction on by default | Yes. URL fields are cut to origin plus path; opt out with full_urls on list_tabs [18] |
No. The only mechanism is opt-in substitution of known secret values (--secrets) |
No, at no setting. Header redaction exists, covers headers only, and is off by default | No. The full page URL is in every snapshot header |
| Declared runtime dependencies, and install footprint | 0 declared, Node builtins only. The published 0.1.6 tarball is 47 files, all first-party | 2 declared, both pinned to a 1.63.0 alpha. 19 MB installed. No browser download on the default chrome channel |
0 declared, and roughly 14 MB of vendored bundle (puppeteer 25.5.0, lighthouse 13.4.0) [19] | 5 declared, 97 transitive as resolved on 2026-08-24, and rising with no new release [20] |
@playwright/mcpdefaults tobrowserName: chromiumwithchannel: 'chrome', so it drives system-installed Google Chrome, but in~/Library/Caches/ms-playwright-mcp/mcp-{channel}-{sha256(cwd)[0:7]}. That profile persists between runs and accumulates its own logins, so it is not your profile and not a throwaway either. Its own--helptext and README call it a temporary directory, which is wrong. Different project directories silently get different profiles.--autoConnectneeds Chrome 144 or later plus a manual toggle atchrome://inspect/#remote-debugging, and it attaches to one profile (the default, as Chrome determines it), not to every window on the machine. The documented--browser-urlpath steers users away from their real profile: the README tells them to pass--user-data-dirso their browsing data is not exposed.- Since 0.1.4 each profile has its own endpoint,
list_browsersnames them, and the tab tools route by tab id, described under Several Chrome profiles below. On 0.1.2 a second profile with the extension loaded could take the single per-user endpoint silently, and 0.1.3 made one profile win deterministically. Two different browsers, Chrome and Brave say, number tabs independently, so a tab id both report is refused rather than guessed. --extensiongenuinely attaches to every tab the extension already knows about (chrome.debugger.attachper tab), so the honest answer is not by default, and supported with--extension.- Only from extension v1.3.3 (2025-04-14) could BrowserMCP automate tabs that existed before the extension was installed. The selected tab id is persisted in extension storage and survives a server restart.
open_tabcreates the tab, so there is no id to name, and it lands in Chrome's current window.scrollwithout arefuses a fixed viewport point, still inside the named tab.browser_click'stargetis an element reference from a prior snapshot rather than a tab handle. Element targeting is explicit there; page targeting is not.--experimentalPageIdRouting(default false, inert under--slim) injectspageIdinto page-scoped tools, so the stateful selection is a default rather than an architectural limit: a selected page by default, with opt-inpageIdrouting.--browser-urlon chrome-devtools-mcp also requires closing every running Chrome first.@playwright/mcp's--extensionroute is gated differently rather than ungated: it throws unless the Playwright Extension is present in the profile.- The three attach paths are
--autoConnect(readsDevToolsActivePortfrom the user data dir after the manual toggle),--browserUrl(needs--remote-debugging-port=9222and a non-default user data dir), and--wsEndpoint. There is no fourth. The self-launched browser passespipe: trueand opens no port. - Verified empirically:
address()returns{"address":"::"}. Startup runslsof -ti:9009 | xargs kill -9. A second connection silently closes the first. @playwright/mcpdoes not disable extensions: its default chromium switch list contains--disable-extensionsand the MCP launch path cancels it throughignoreDefaultArgs. It carries none of your extensions because the profile is fresh, not because extensions are off. That is why the extensions half was cut from this row's title.- On chrome-devtools-mcp 1.7.0 the two halves land on mutually exclusive paths: session state needs
--autoConnect, while the extension tooling is documented as unsupported onautoConnect,browserUrlandwsEndpointuntil Chrome 149 and works only over the pipe connection, that is the dedicated profile that holds no user extensions. Yoke and BrowserMCP carry the user's other extensions by construction; BrowserMCP has had to fix breakage from 1Password and LastPass injecting elements into snapshots. - Every operation that drives or reads a tab marks it:
navigate,get_page_text,read_page,find,run_javascript,screenshot,click,type_text,press_key,scroll,read_console,read_network, andopen_tabgroups the tab it creates. Deliberately unmarked:list_tabs,list_tab_groups,close_tab,ping,release_tab,ungroup_tabs. The tab group is present in published 0.1.3. Chrome's own "started debugging this browser" banner also appears when the debugger attaches, and yoke does not suppress it. That is Chrome's behaviour rather than a guarantee from either project: it is not stated on Chrome'schrome.debuggerreference page. The same caveat applies to the BrowserMCP cell. - Headed is the default (headless is auto-selected only on Linux with no
DISPLAY). The server pushes--disable-blink-features=AutomationControlled,--disable-infobarsis in the default switch list, and--enable-automationis absent. The indication is structural (a separate window on a separate profile) rather than a badge. What--extensiondoes here could not be established from the package, so this row says nothing about it. - chrome-devtools-mcp adds no title change, overlay or badge of its own, and it inherits puppeteer's default args, which contain
--enable-automationand--disable-infobarsin the same array. Whether Chrome still draws the infobar with both flags present was not tested, so this row claims nothing either way. - Chrome's
--silent-debugger-extension-apiand force-install viaExtensionInstallForcelistboth remove the infobar, so in those configurations the answer becomes No. BrowserMCP ships no indicator of its own. - Redaction covers URL fields, not the whole payload. Unredacted by design:
read_consolemessage text,run_javascriptreturn values (solocation.hrefordocument.cookiecomes back whole, with no flag needed), the page body fromget_page_text, and screenshot pixels.full_urlsexists onlist_tabsonly; everywhere else redaction is unconditional. - This is the devDependencies trap in reverse.
npm view chrome-devtools-mcp@1.7.0 dependenciesis empty and the tarball'spackage.jsonhas nodependencieskey, so a reader who runsnpm lsagainst a "substantial" claim finds nothing. The substance is real but vendored by rollup, not installed. Neither chrome-devtools-mcp nor@playwright/mcpdownloads a browser on its default path: both use system Chrome. - Five direct deps, none of them the five
workspace:*entries (those are unpublished devDependencies bundled intodistat build time). The transitive count grows with no release because@modelcontextprotocol/sdk ^1.8.0floats and now resolves to 1.30.0, pulling in express 5.2.1.
yoke grew out of measurements of Anthropic's Claude in Chrome MCP bridge. That bridge could only see and act on tabs in its own managed group, and each reply included its full tab list with raw URLs. Those limits could not be fixed by an outside client. yoke addresses every tab by id and redacts URLs by default. MOTIVATION.md records the observations behind those choices.
yoke is not affiliated with or endorsed by Anthropic or Google. It works with any client that speaks Model Context Protocol.
The extension is on the Chrome Web Store as Yoke. Both halves are needed, because Chrome only hands a native messaging connection to a host it started itself:
npm install -g yoke-mcp
yoke installThen add the extension from the link above and run yoke doctor, which names the first broken link in the chain rather than leaving you to guess.
Every store version is reviewed before it ships, and review takes days, so the listing sits behind npm after a release. The badge at the top says which version is published today. Building from source is how you run the newest extension code.
Build from source to run extension code the store has not published yet, or to audit what you are loading.
You need Node 22 or later and a Chromium based browser. The manifest requires Chrome 116 or later. Chrome, Chromium, Edge, and Brave registration paths are present. Only macOS has been exercised so far.
git clone https://github.com/hamzahamidi/yoke.git
cd yoke
npm install
npm run build
node dist/cli.js installDo not skip the build. extension/manifest.json loads extension/browser/background.js, and extension/browser/ is generated output that is not stored in Git.
node dist/cli.js install registers the native messaging host with each Chrome family browser it finds. If the yoke binary is already on your PATH, yoke install runs the same command.
On Windows, yoke install does not complete registration. It prints a registry instruction instead of changing the registry. The Windows and Linux paths have not been tested yet.
In Chrome, Chromium, or Brave:
- Open
chrome://extensions. - Turn on Developer mode.
- Choose Load unpacked.
- Select the
extension/directory inside the clone, not the repository root.
In Edge, use edge://extensions and follow the same steps.
An unpacked load gets the id oceljemfocgfidhhdlbojkbkmlbfclna, pinned by a public key in extension/manifest.json. A Chrome Web Store install gets a different one, mebojgahcmmffbaonhnmmjhmbdbfbamm, because an uploaded package may not carry a key and so cannot choose its id. yoke install allowlists both, so either install works. A build whose id is neither is refused by native messaging, which is the point of pinning it.
The native messaging host id is io.github.hamzahamidi.yoke.
node dist/cli.js doctorWhen setup is complete, the last line is:
Working. Every link in the chain answered.
If something is wrong, doctor stops after the first broken link, names it, and prints a suggested fix. It checks the compiled host, browser registration, the local sockets, which profiles answered, and for each profile whether the extension replies and tabs are visible.
For a shorter connection check, run:
node dist/cli.js statusAn MCP client needs to start the command yoke with the argument mcp.
If you want the binary from this checkout on your PATH, run this once from the repository:
npm linkA typical MCP server entry then looks like this:
{
"mcpServers": {
"yoke": {
"command": "yoke",
"args": ["mcp"]
}
}
}Clients use different names and locations for their MCP configuration. If you do not want to run npm link, set the command to node and pass the absolute path to dist/cli.js before mcp.
yoke exposes 20 MCP tools. They are grouped here by the job they help with.
| Job | Tools | What they do |
|---|---|---|
| Work with tabs | list_browsers, list_tabs, list_tab_groups, open_tab, navigate, close_tab, group_tabs, ungroup_tabs |
See which profiles are connected and every window in each, open background tabs, visit HTTP or HTTPS URLs, close tabs, and manage Chrome tab groups. |
| Read a page | get_page_text, read_page, find, screenshot |
Read visible text, describe interactive elements, find a matching element, or capture a foreground or background tab. |
| Act on a page | click, type_text, press_key, scroll, run_javascript |
Use trusted input by element reference, scroll the page, or evaluate JavaScript in the page's own world. |
| Debug a page | read_console, read_network, release_tab |
Read recorded console and network activity, then detach yoke and clear that tab's buffers. |
Any tool that acts on an existing tab requires an explicit tab_id obtained from list_tabs. There is no active tab default. yoke never acts on whichever tab you happen to be viewing because no tab was named.
read_page returns references such as e1 for interactive elements. click, type_text, press_key, and reference based scrolling use those references. A reference expires when the page navigates or renders the element again, so call read_page again when a reference is stale.
navigate and open_tab accept HTTP and HTTPS URLs. New tabs open in the background unless the caller asks to focus one.
Chrome starts one native host per profile, and each host listens on its own endpoint named after an id the extension mints the first time it runs in that profile. list_browsers lists every profile that answers, with its id, the label you gave it in the popup, its tab and window counts, and the sites it mostly has open, so a profile is recognisable before it has a name.
With one profile connected nothing changes. With more, list_tabs and list_tab_groups include every profile and add a browser column, and every tool that names a tab_id reaches the profile holding that tab without being told which: Chrome allocates tab ids from one counter for the whole browser, so two profiles never share one. open_tab names no existing tab, so with more than one profile connected it needs browser, an id or label from list_browsers, and refuses rather than picking.
Two different browsers, such as Chrome and Brave, do number tabs independently. A tab id both report is marked in list_tabs and refused by the tab tools, naming both browsers.
The id and label are stored in the extension's own storage in that profile. They never leave your machine.
Every tab opened by yoke goes into a cyan group titled yoke, so the tab strip names what is driving those tabs. Pass group_title to open_tab for a different label, which is worth doing when more than one agent works in the same browser. The extension creates that group when needed and reuses a group with the same title in the same window. Reuse comes from Chrome's current group state, so it still works after Chrome has stopped and restarted the extension service worker. You do not collect a row of identical group pills.
Any tab yoke drives or reads joins the group as well, whether or not it opened that tab, so the strip always shows what is under automation. Tabs are never moved between windows to achieve it. Grouping is visual only, so no tool depends on it to find a tab. ungroup_tabs removes tabs from their group without closing them. When the group becomes empty, Chrome removes its pill.
yoke reduces URLs in tool output to origin and path by default. Query strings and fragments are omitted because they can contain session tokens or credentials. Non HTTP wrapper URLs are reduced to their scheme because their path can contain another full URL.
list_tabs accepts full_urls: true when a caller needs raw URLs. The other tools that print URLs currently keep them redacted.
Installing Yoke gives a local MCP client broad control over the browser profile where you are signed in. It can read site content, run JavaScript, send trusted input, and observe network activity. That can include private accounts and private data. Install it only for MCP clients and agents you trust with that access.
The extension requests these permissions:
| Permission | What yoke uses it for | What the grant means |
|---|---|---|
tabs |
List tabs in every window, including ids, titles, URLs, and group membership. Open, navigate, and close named tabs. | The extension can see which pages are open and can change the tab strip. |
tabGroups |
List groups, create or reuse them, set their title and colour, and ungroup tabs. | The extension can inspect and change tab grouping in every window. |
nativeMessaging |
Connect to the local host named io.github.hamzahamidi.yoke. |
The extension can exchange data with a program installed on your computer. |
scripting |
Inject the functions that read visible text, collect interactive elements, and resolve element references. | The extension can read and run code inside allowed pages. |
debugger |
Use the DevTools Protocol for trusted clicks and typing, JavaScript evaluation, background tab screenshots, console messages, and network requests. | The extension gets deep control and inspection access on each tab it attaches to. |
host_permissions: ["<all_urls>"] |
Let scripting work on ordinary sites regardless of host. |
The site grant is broad. It is not limited to a list of sites chosen during installation. Per site permission is future work. |
Chrome's own pages and the Chrome Web Store still block page script injection. yoke reports that restriction instead of returning an empty page.
An extension cannot create trusted input through page JavaScript. A click made by a content script reaches the page with isTrusted: false, and many sites ignore it. chrome.debugger is the extension route to Input.dispatchMouseEvent, which creates input the page cannot distinguish from a person's input.
Chrome shows its "started debugging this browser" bar when yoke drives a tab. This is expected. Chrome also permits one debugger client per tab. A tab with DevTools open cannot be driven by yoke, and DevTools cannot open on a tab while yoke is attached. Call release_tab to detach yoke and clear that tab's console and network buffers. The tab stays in its group.
The same permission lets screenshot capture a background tab. chrome.tabs.captureVisibleTab can only capture the active tab in a window.
The bridge does not open a TCP port. On macOS and Linux, each native host listens on a Unix socket inside a directory readable only by your user account, one socket per Chrome profile. The socket itself is also restricted to that account. Windows uses a named pipe.
Chrome owns the native host process. It starts the host when the extension calls connectNative and stops it when that connection closes. This is why the Unix socket between the MCP server and host is required.
yoke has zero runtime dependencies. TypeScript and the Chrome type declarations are used only during the build.
After npm link, the command surface is:
| Command | Purpose |
|---|---|
yoke install |
Register the native messaging host with each detected Chrome family browser. |
yoke doctor |
Check each link from the build through tab visibility and suggest the first fix. |
yoke status |
Report which Chrome profiles answer, one line each with id, label, tab count and socket path. |
yoke uninstall |
Remove the native host registration. |
yoke mcp |
Run the MCP server over standard input and standard output. This is what an MCP client starts. |
From a checkout that has not been linked, replace yoke with node dist/cli.js in these commands.
- yoke is young and pre 1.0. Tool names, arguments, and results can change.
- Only macOS has been exercised. Linux and Windows install paths are written but untested. On Windows,
yoke installprints a manual registry step but does not make the registry change. - The project has one offline test file,
test/mcp-server.test.ts. It checks the MCP surface and URL redaction, but this is not yet a well tested browser project. - A driven tab shows Chrome's debugging bar and cannot share its debugger slot with DevTools.
- Element references expire after navigation or a page render. Console and network history starts when yoke first attaches to that tab, not before.
- Screenshots capture the page viewport, not browser chrome such as the address bar or tab strip.
- Two different browsers that both run the extension, such as Chrome and Brave, number their tabs independently. A tab id both report is refused rather than routed, because it cannot be sent to both. Two profiles of the same Chrome never collide.
- Chrome's internal pages cannot be read or driven. Neither can the Chrome Web Store or the extensions gallery: Chrome refuses both
chrome.scriptingandchrome.debuggerthere, so no extension can automate them, including this one. Publishing an extension is therefore a manual job by design.
Remove the browser's native host registration with:
yoke uninstallThen remove Yoke from the browser's extensions page. If you linked the checkout with npm, remove that link with:
npm unlink --global yoke-mcp