npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

browser-broker

v0.6.1

Published

Leases over tabs in a fixed set of browsers: bounded capacity, a queue, reclamation, and an enforced capture policy.

Readme

Browser Broker

Several agents, two browsers, no collisions.

Browser Broker brokers access to a small, fixed set of real browsers. A caller asks for a lease; it gets back a secret key and one tab it exclusively owns. Every later call carries that key, and the service uses it to route the call, to enforce what that caller may touch, and to account for what it costs. There is nothing running in the background: the service is started by its caller and exits with it, and the browsers outlive any one of them.

What it gives you

  • A hard ceiling on browser processes. Concurrency is expressed in tabs inside a fixed set of browsers, so process count is bounded by configuration rather than by how many clients connect. A lease is exactly one tab, so the budget, the pool bound and the number of live leases are one integer that cannot disagree with itself. Need two tabs, claim twice.
  • Leases, with a queue. When capacity is full a caller is queued rather than refused, and told its position and when to check back.
  • Reclamation from callers that die. Every key carries a time to live that any call renews, so a client that vanishes mid-work returns its capacity on its own. This is the failure a client-side convention cannot cover, because the client that should clean up is the one that is gone. Nothing expires on a timer: every arbitration call first expires whatever has lapsed across the whole store, then answers from the reconciled state.
  • A shared signed-in profile that no single caller can destroy. Nothing browser-scoped is exposed. A caller can close its own tab; it cannot close a browser, and no operation reaches a browser outside the ones the service is configured to run.
  • References, not payloads. Screenshots and page snapshots are written to disk and returned as a path with its dimensions and size. An agent opens one only when it genuinely needs to look, so a capture is paid for once instead of on every subsequent turn.
  • A capture policy applied by the thing that takes the capture. Screenshots come back at a low resolution unless you ask for more, and asking for the most expensive tier costs a stated reason in free text. Nothing is ever refused — going over budget warns loudly and names the cheaper way to get the same answer.
  • Changed-region review. A capture can name an earlier capture to compare against, and the regions that actually moved come back as crops — so a repeat review looks at what changed instead of at everything. There is no canonical picture to bless first: a capture is a capture with an identifier, and the caller says which one it means. If that image is missing, the full screenshot comes back with an explanation rather than a refusal.
  • Nothing to configure before it runs. Every value is an environment variable with a working default, so a fresh install runs with nothing set, and .env.example documents the whole set. The one value several processes must agree on — the tab budget — is written into the store by the first process to open it, and a later process whose environment disagrees refuses to start and names both numbers.

Surfaces

The rules live in one service layer, and every surface is a thin adapter over it: twelve tools, served by the caller's own spawned process, and a broker command line that runs the same logic in the process you typed it in. A shared conformance suite asserts that the same operation and the same refusal happen on both.

Nothing is served over a socket. The operations view is a self-contained HTML file that a command generates and a person opens from disk — a snapshot, labelled with the moment it was taken, which does not refresh. It is generated from inside a live session, so it reads each tab's address from the browser itself; a browser that does not answer within a timeout renders as unreachable rather than hanging the report.

Install

Installation is the whole of deployment. There is no image to pull, no daemon to register and no service to keep running: the process is started by whatever calls it and exits with it. So getting it working is an install and one more fetch below — there is no step after that.

You need Node 22.18 or newer. Either path below also needs a browser binary, which is a separate fetch from either install step — see Browser binary once you've picked a path.

From the registry

The package ships compiled JavaScript, so nothing is compiled on your machine. That is not the same as nothing to set up: a browser binary is still owed, and this path does not fetch it — see Browser binary below before your first broker doctor.

npx -p browser-broker broker doctor

The package installs two executables — broker, the command line, and broker-tool, the surface a client spawns — and neither is named for the package, so npx browser-broker cannot tell which you meant and refuses. -p names the package and the word after it names the executable.

A client that spawns the tool surface names the same package, and npm revalidates the version on every run — so a published release arrives without anything being pulled or rebuilt by hand:

{
  "mcpServers": {
    "browser-broker": {
      "command": "npx",
      "args": ["-y", "-p", "browser-broker", "broker-tool"]
    }
  }
}

npx costs a registry round-trip on every spawn — measured at ~1.1s warm and ~3.7s cold, against ~0.4s for a path on disk. Against a typical 30s MCP connect timeout that is ample headroom, so prefer this form even on a machine that develops the service: a config file shared between machines cannot carry an absolute path that is correct on all of them.

⚠️ If handshakes start timing out, prune the npx cache before blaming npx. An unpruned 837MB _npx cache once pushed spawn cost to 9.7–33.2s and blew a 30s connect timeout outright. The cost is the cache, not the mechanism. Two related traps: --prefer-offline can serve a packument that predates a release, so npx resolves a version it then cannot fetch (ETARGET); and the local npm cache lags the registry independently, so npm cache clean --force is the fix when npm view and npx disagree about what exists.

From a checkout

For working on the service itself. The sources are TypeScript and run through the runtime's own type stripping, so there is no build step in the development path:

git clone https://github.com/Zaida-3dO/browser-broker.git
cd browser-broker
npm install

That compiles the one runtime dependency's native binding. It does not fetch a browser binary — see Browser binary below before your first broker doctor. Then run the broker itself:

node src/bin/broker.ts

The first run creates the store, brings its schema up to the version the build expects, prints where the file is, and exits:

store: <the resolved store location>
schema: stepped from version 0 to version 1 (1 step(s) applied)

Run it again and it says the schema is already where it should be. Every spawn does this, not just the first — with no long-lived process, there is no other moment at which it could happen, and a caller that has upgraded and one that has not may both start within the same minute.

To get the command on your path as broker, link the package from the checkout:

npm link          # then: broker --help

Browser binary

Neither install path above fetches a browser. This repository depends on playwright-core, not the full playwright distribution, precisely because the browser binary is spawned by this service, detached and by path, rather than downloaded and managed by the package. playwright-core does not fetch a browser on install, so a machine that has never had one fetched by some other tooling has none, and broker doctor's automation check will genuinely fail with exit code 11 until you run:

npx -p [email protected] playwright-core install chromium

The version must match the playwright-core version pinned in package.json, exactly. Run the install with no -p playwright-core@<version> at all and it resolves to whatever is latest on the registry, which fetches a Chromium build the pinned library was never tested against; the pinned library then resolves an executable path that build does not have, and the automation check fails with no way to tell from the error alone that the fetch itself was the problem. npm run check:pinned-install (wired into CI) fails the build if this number and the one above ever drift apart, so the version above is not a fact you have to remember to update by hand.

Run this once per machine, before the first broker doctor — it applies whether you installed from the registry or from a checkout, because it is a fetch neither install step performs. It is the same install mechanism the full playwright package would run automatically on npm install; playwright-core just does not run it for you.

On a network that inspects TLS

The fetch above downloads a browser build over HTTPS, so on a corporate or home network that re-signs traffic it fails with SELF_SIGNED_CERT_IN_CHAIN (or UNABLE_TO_GET_ISSUER_CERT_LOCALLY). Node trusts a compiled-in certificate list and does not read the operating system's trust store, so a certificate your browser already accepts is still unknown to the fetch.

Point NODE_EXTRA_CA_CERTS at the PEM bundle holding the CA your network re-signs with — the file your platform's administrators publish, which is what the browser and the rest of the machine already trust:

NODE_EXTRA_CA_CERTS=/path/to/corporate-ca.pem \
  npx -p [email protected] playwright-core install chromium

It is read by Node at process start and adds to the built-in list, so ordinary certificates keep verifying. Set it in the environment if the same network fronts anything else you run.

This is environmental rather than a setting of this service — nothing here reads the variable, and there is no default for it to have. It is named here because the failure lands on the first-run path, where the error alone does not say which fetch was blocked or what would unblock it.

Configuring it

Nothing needs setting. Every value is an environment variable with a working default, so the install above runs as-is; .env.example documents the whole set, with placeholders rather than real values. Nothing reads that file — configuration is the process environment.

The default store location is a directory of the service's own under the per-user application-data location your platform defines. It is computed rather than written down anywhere, because writing one down would name one machine. To put it somewhere else, set BROKER_DB:

BROKER_DB=/some/writable/path/broker.db node src/bin/broker.ts

The browsers are configuration too. Two lists, at most three names each, defaulting to one of each — regular and private. A name is what a caller claims by and what its profile directory is called, and a caller that names no browser gets the first signed-in one:

BROKER_REGULAR_BROWSERS=regular,checkout      # persistent, signed in, at most 3
BROKER_PRIVATE_BROWSERS=private               # ephemeral, at most 3

Every browser launches the automation library's own Chromium, which a machine fetches once. The browser a caller names selects an identity and its profile directory, not a different binary.

Two signed-in browsers is how two identities are exercised at once: tabs within one browser share its cookie jar, so they are isolated from other browsers and not from each other. Note that each browser is a process before it holds a single tab, which the tab budget does not count — .env.example gives the arithmetic beside the variables.

A variable that is set but cannot be read as its type refuses the spawn and names the variable, rather than quietly falling back to the default — a configuration nobody chose is worse than a refusal nobody missed. For a list, the refusal names the offending entry: a duplicate within one list, a name in both lists, more names than the cap, or a name that is not a usable word. A store location that resolves to a network share is refused for the same reason it has to be: the write-ahead log coordinates through shared memory that requires every process using the file to sit on one host.

Pointing a client at the tools

The twelve tools are served over standard input and output by src/bin/broker-tool.ts, which speaks the Model Context Protocol — revision 2025-06-18, with 2025-03-26 accepted for a client that asks for it. A client spawns that file, opens with initialize, and the twelve tools are listed to it.

Most clients read a JSON file naming the servers they may spawn. The block is the same shape in all of them; put it in whichever file yours reads — commonly .mcp.json in a project, or the client's own configuration:

{
  "mcpServers": {
    "browser-broker": {
      "command": "node",
      "args": ["/absolute/path/to/browser-broker/src/bin/broker-tool.ts"]
    }
  }
}

The path has to be absolute, because the client chooses the working directory it spawns from and it is rarely the checkout. Nothing else is required: there is no port to configure, no token to issue, and no process to have started first — the client starts it, and it exits when the client closes the pipe.

If that configuration file is itself synchronised between machines, an absolute path is the one thing in it that cannot travel. A home directory differs per machine and often per user, so one entry naming a checkout is correct on the machine it was written on and names nothing on the other — where it fails as a connection that closes immediately, which reads as a broken service rather than as a path that does not exist. Give each machine its own entry under its own server name, or point the shared entry at the published package, which carries no machine's path:

{
  "mcpServers": {
    "browser-broker": {
      "command": "npx",
      "args": ["-y", "-p", "browser-broker", "broker-tool"]
    }
  }
}

To point it at a store other than the default, add the environment to the same block:

{
  "mcpServers": {
    "browser-broker": {
      "command": "node",
      "args": ["/absolute/path/to/browser-broker/src/bin/broker-tool.ts"],
      "env": { "BROKER_DB": "/some/writable/path/broker.db" }
    }
  }
}

Every client sharing a store sees the same leases, which is the point of the store being a file: the capacity being arbitrated is one set of browsers, and two clients that could not see each other's claims would both think the whole of it was free.

To check the wiring without a client, speak the handshake by hand. This writes three messages and reads two back — the notification is the one that draws no reply:

printf '%s\n' \
  '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{}}}' \
  '{"jsonrpc":"2.0","method":"notifications/initialized"}' \
  '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
  | node src/bin/broker-tool.ts

The first response carries the negotiated protocolVersion, the server's capabilities and its serverInfo; the second message is a notification and is deliberately not answered; the third lists the twelve tools. A client that gets that far will work.

First run

There is one step a person performs by hand, and it happens once: signing the shared browser in. Everything else — creating the profiles, starting browsers, adopting them, keeping them alive — the service does for itself. This is the whole of it, from a clean clone:

git clone https://github.com/Zaida-3dO/browser-broker.git
cd browser-broker
npm install

node src/bin/broker.ts init     # create the store and both browser profiles
node src/bin/broker.ts login    # open a browser and sign in, by hand
node src/bin/broker.ts doctor   # confirm the sign-in took

broker init creates the store, steps its schema, and establishes a profile directory for each browser. It reports each one as created or found, which is the distinction worth reading: a profile reported as created on a machine where you expected a sign-in is the earliest possible warning that you are about to be asked to sign in again. It never recreates or clears a profile that is already there.

broker login opens the shared browser, headed, against that profile and hands it to you:

A browser window is open for you to sign in.

  1. Switch to the browser window that just opened. It is the regular browser,
     running against the profile at regular under the configured
     profile root — which is the profile every caller will share.
  2. Go to whichever site you want this service to be signed in to, and sign in
     normally. This is a real browser and a real sign-in: what you type goes to
     that site exactly as it would in your own browser.
  3. Close the window when you are done. Closing it is what ends this step.

Sign in to whatever you want the service to have access to, then close the window — that is what ends the step. While it is open the browser is not serving callers: anything that asks for it is told a person is signing in and to try again shortly, and anything already waiting in the queue keeps its place and its timer. A sign-in is a pause, not a cancellation.

Two things it will refuse, both on purpose:

  • A browser with live work on it. If a caller holds a tab there, signing in would mean driving the window by hand underneath somebody's work. It names the leases holding it; waiting is enough, because every lease expires on its own if its holder stops calling in.
  • The private browser. Its profile is discarded when it exits, so a sign-in there would appear to work and leave you signed into nothing.

Nothing records what you type. The sign-in is written into the browser's own profile directory by the browser itself. This service never sees a credential and stores nothing about one anywhere — which is also why there is no way to copy a sign-in between machines: the profile is the identity.

broker doctor then tells you whether it took, without opening a browser:

[ok  ] The regular browser’s profile carries a sign-in
         The profile holds 1 stored cookie(s), so a session was established and written down.

Before you have signed in, the same line reads:

[--  ] The regular browser’s profile carries a sign-in
         The profile has a cookie store and it holds no cookies. That is what a profile nobody has
         signed into looks like — though a site that keeps its session only in local storage would
         look the same, so this is the absence of evidence rather than evidence of absence.

It never reports this as a failure, and it will say unknown rather than guess. A profile with no session is the ordinary state of every installation until somebody signs in, and a check that went red on a working machine is one people learn to ignore. It also cannot see everything: it reads the browser's stored cookies, so a site that keeps its session somewhere else is invisible to it, and a browser that is still running has not necessarily written its cookies down yet — in that case it says so and tells you to close the browser and ask again.

Checking an install

npm run check:install

This spawns the executable as a real process against a temporary store, and asserts it creates the file, steps the schema to the version the build expects, answers a command and exits. It is what continuous integration runs on a clean hosted runner, and it is the check that stands in for the one an image build would have given: proof that the thing actually starts.

Run everything the pipeline runs with npm run check.

Recovering from leaked browsers, on Windows

The emergency sweep in scripts/reap-broker-browsers.ps1 reports leaked broker browsers by default and does not touch them; pass -Execute to terminate what it found, and add -PruneDirs -OlderThanHours <n> to also clear stale profile directories older than that many hours. Not part of the pipeline: a browser this project launches is spawned detached: true by design (see the header of src/browser/launch.ts) so it survives the process that started it, and every known way that leak could happen has since been fixed at its source — see the script's own header for the list. Keep it installed anyway as insurance against whatever the next one turns out to be, and as the tool to run if a machine is already in the frozen state a leak like this can cause. Matches processes by profile-directory command line, never by image name, so it cannot touch an unrelated Chrome window or this repository's own Playwright MCP tooling; see the script's own SAFETY section.

Status

Shipped, not under construction. The package is published at 0.4.0: the store, the executable, the pipeline and the arbitration surface — claim, queue, lease expiry, the twelve-tool MCP surface and its CLI parity — are all in place and covered by the test suite this repository runs in CI, not just planned. Usage is measured, not assumed: SCHEMA.md and DECISIONS.md cite counts pulled from 2,007 real session transcripts (resize alone: 578 calls across 140 sessions) rather than a guess at which verbs matter. Over ninety pull requests have merged.

What is still a runbook rather than a fact about the package: rollout is per-install, not global. Installing this repository does not by itself make it the sole route to a browser anywhere it is deployed — that is a deliberate, ordered migration an operator walks per environment, because a rollout with a window where some callers are brokered and others are not reproduces the exact failure this project exists to prevent. broker doctor reports whether a given install is healthy; docs/ROLLOUT.md is the runbook for taking one from installed to sole route without that window. Read docs/plans/PLAN.md for how it works, docs/plans/DECISIONS.md for why it is shaped this way, and docs/plans/MILESTONES.md for the work queue.

Releasing

The development path runs the TypeScript sources directly; the published package cannot, because Node refuses to strip types from any file under a node_modules path and an installed package is exactly that. No flag overrides it. So a release compiles to dist/ and the manifest's bin entries name the emitted JavaScript.

prepack runs the build, so npm publish and npm pack compile on their own — there is no way to publish a stale dist/, and no build step to remember.

npm version patch      # or minor / major — writes the tag and the commit
npm publish            # prepack builds, then the tarball goes up
git push --follow-tags

npm run check:package asserts what a published tarball owes: every bin target is emitted JavaScript rather than a TypeScript source, the whole built tree is included, and the tarball carries no tests or plans. It runs in CI. Worth knowing if you touch the files field: npm ships bin targets whatever files says, so a tarball can contain both executables and none of the modules they import — which installs cleanly and dies on first run. That is the case the check exists for.

Licence

MIT — see LICENSE, and docs/plans/DECISIONS.md §13e for the reasoning. A public repository without a licence file grants no rights to anyone, so the decision alone was never enough; the file carries it and package.json declares it.