beifahrer
OfficialProvides browser integration for Brave, enabling an AI agent to list tabs, read pages, outline fields, take screenshots, fill fields, click, and open URLs in the user's existing Brave browser, subject to per-site permissions.
Allows an AI agent to interact with the user's Firefox browser, including listing tabs, reading page content, outlining fields, taking screenshots, filling fields, clicking, and opening URLs, with per-site permission controls.
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., "@beifahrerRead the page in my current tab and summarize it."
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.
Beifahrer
Let an AI agent ride along in the browser you already use, with you in the driver's seat.
Agents keep needing the browser that is already open in front of you. They need to know which tab you are looking at, to read a page you are logged into, or to rewrite a ticket description in place while you watch. The usual tools start a separate browser with no sessions and no idea of what you see. The ones that attach to your own browser only work with Chromium.
Beifahrer is a WebExtension for Firefox and Chromium-based browsers plus a small local MCP server. The agent gets a narrow set of tools: list tabs, read a page, outline its fields, find an element by name, wait for a page, take a screenshot, fill a field, click, open a URL, run a recipe, and, if you allow it, sort, pin, close, save and reopen your tabs. You decide, site by site, what it may do.
Status: early (0.1). Works end-to-end in Chromium and Firefox. Not yet on the add-on stores.
How it works
Firefox / Chromium
└─ beifahrer extension ─ WebSocket, 127.0.0.1 only, paired with a token ─┐
│
beifahrer mcp (MCP over stdio) ────┘
│
agentThe extension connects to the bridge on loopback. It serves requests only after you have paired it once with a token, and the bridge refuses any connection whose origin is a web page.
The bridge (
beifahrer mcp) is an MCP server over stdio that your agent starts. It routes requests and holds no policy.Every agent session gets its own connection. Each
beifahrer mcplistens on the first free port of 47813–47822, and the extension looks for sessions on that range every few seconds and connects to each one it finds. Sessions never go through each other, so a session started from an older build cannot hold back a newer one. All of them read the same token file. The popup lists the connected sessions by name (the agent and the folder it works in, e.g. "claude-code · werkstatt", orBEIFAHRER_SESSION_LABEL), and Disconnect shuts one out until it restarts. Why: ADR 0007.The policy lives in your browser. Every site is at one of three levels:
Level
The agent may
Nothing (default)
see that a tab on that site is open (host only: no title, no path)
Read
read the text, get an outline of links, buttons and fields, take a screenshot (if screenshots are on)
Read + edit
also fill fields and click. You confirm each change in a browser window unless you switch that off for the site.
When you raise a site's level, the browser asks you to grant access to that site. Beifahrer has no host access until you do. Setting a site to Nothing blocks it: nothing below reaches it.
All sites, for a while. In the popup, All sites lets the agent read (or edit) every site you have no rule for, for one hour, until the browser closes, or for one agent session. It is never permanent, the toolbar shows a blue dot while it lasts, and End stops it at once. Every change it allows asks you first, unless you switched Ask before changes off for that grant.
Asked when needed. When the agent needs a site it has no level for, a window asks "session wants to read site": Allow once, For this session, Always (sets the site's level) or Deny. No answer within two minutes is a no. You can switch the asking off in the options. Why both: ADR 0010.
Related MCP server: Cloudflare Playwright MCP
Seeing it, and stopping it
The toolbar button tells you what is going on. Its icon is the Beifahrer bus, and the rows are in the order they matter:
Icon | Means |
grey + red dot | no agent is connected: not paired, or no bridge running. Nothing can reach the browser, so the extension does nothing |
grey + yellow dot | paused: the agent gets nothing |
grey + blue dot | All sites is on for now (the bus still turns coloured while the agent works) |
coloured bus | an agent is using this browser right now (and for 5 s after its last request) |
grey bus | connected to an agent, nothing happening |
A missing connection outranks a pause: with no bridge there is nothing an agent could do anyway, and it is the one thing only you can fix (ADR 0014). Hover it for the same in words. Click it for the popup: one word of state and the pause button at the top, a banner over the page when something is not normal (not connected → Settings, paused → Resume), the level of the site you are on, the connected agent sessions, and the last few entries of the activity (what, which site by host, when, which session, and why a request was refused; the options page shows all of them). The activity list holds no page text and is gone when the browser closes.
The popup, the options page and the confirmation window are built with
@gjsify/adwaita-web, so they look like GNOME's own settings
under GNOME and like a plain, consistent settings page elsewhere. Light and dark follow your
system. They speak English and German so far, chosen by your browser's language; the store
listing does too. Adding a language is one file, extension/_locales/<lang>/messages.json
(ADR 0008). They say only what is not normal: a banner when
the agent is paused or cannot connect, explanations behind an info button
(ADR 0009).
The pages and the in-page pill use your desktop's accent colour (GNOME 47+, or macOS): every agent session's bridge reads it and tells the extension, which keeps the latest one, so this works in every browser. Without it they use the browser's own accent where it has one, otherwise Adwaita blue. Light and dark follow the browser's setting: in Firefox, the theme "System theme — auto" and Website appearance "Automatic"; in Chrome, Appearance → Mode "Device".
The pictures show the e2e's synthetic fixture page, never a real site.
While the agent reads or edits a page, the page shows it: a small pill, "Agent is reading" or "Agent is editing", with a Stop button, top right. It disappears a few seconds after the last action, never appears in a screenshot, and the page itself cannot read it.
Pause stops everything at once. Press Stop on that pill, the pause button in the popup, the switch in the
options, or press Alt+Shift+B (⌥⇧B on macOS). While paused, every tool the agent calls, even listing tabs,
answers paused, and the agent is told to ask you. Only you can resume, in the popup, the options
or with the shortcut: nothing the agent sends can.
Features. On top of the per-site levels, you choose which capabilities the agent may use at all (options page):
Feature | Tools | Default |
See open tabs |
| on |
Ask before spending a call |
| on |
Read page text |
| on |
Outline pages |
| on |
List rows |
| on |
Download by click |
| on, with Click and Download documents |
Take hidden downloads |
| off |
Screenshots |
| off |
See page requests |
| off |
Fill in forms |
| on |
Click |
| on |
Type one-time codes you relay |
| off |
Read a login QR code for you | a recipe step decodes the QR code a login page shows and hands its content to you through the messenger, so you can scan it with your own app. Only recipes call it, nothing is logged or kept, no tool exists for it (ADR 0022) | off |
Run scripts |
| off |
Point the browser at a URL |
| on |
Manage tabs and windows |
| off |
Saved sessions |
| off |
A tool whose feature is off answers feature_disabled and names the feature. Why and how:
ADR 0005.
Tabs, windows and saved sessions
Two features, "Manage tabs and windows" and "Saved sessions" (popup and options, both off by default), let the agent tidy up your browser for you:
Tool | Does |
| move a tab: to a URL, or |
| reorder tabs (also into another window), pin and unpin |
| close tabs or a whole window. You confirm in a window that lists them, unless you switch that off |
| tab groups, where the browser has them (Chromium, Firefox ≥ 139) |
| a new window with new tabs and/or tabs moved over |
| save windows (order, pins, groups) under a name and reopen them later, lazily |
| a workspace the agent puts together for a task |
| the browser's own list of closed windows and tabs |
Sessions are stored in your browser, not with the agent (ADR 0004). The options page lists them with Restore and Delete, saves the current windows under a name, and shows your recently closed windows. It works without any agent. It also keeps automatic snapshots of your windows (the last 20), so a window you close by mistake comes back in one click.
The per-site levels still apply: the agent sees a saved tab on a site below Read as its host only, and it can only put URLs of sites at Read or higher into a new window or workspace.
page_navigate moves your tab, so the level is checked twice and the second check is not
redundant. A URL the agent supplies needs Read on the site it names — exactly as tab_open does,
so what an agent just read cannot be carried off in the query string of a site you never allowed.
back and forward cannot be checked that way (no browser will say where a history entry points),
so they are bounded by the tab as it is, which is the page in front of you. Where the tab lands
is then asked about again: below Read the call answers forbidden with the origin, because the
move has already happened by then and the answer exists to stop the agent reading it, not to
pretend the move did not occur. Clicking a link is the better way to follow one — it confirms
with you, which a URL the agent typed itself does not.
Recipes
Some tasks take the same few steps on the same web app every time. On an OpenProject work package, for example, the comment box is a button until you click it, the editor appears a moment later, and only then can text go in. A recipe writes such a task down once, as data:
Tool | Does |
| elements by role and accessible name ("the button named Submit comment"), with refs for fill and click |
| wait for a tab to finish loading, or for an element to appear (up to 30 s) |
| wait for a condition — the Save button enabled, the spinner gone, the field holding a value, the number of matches — and report what it saw when the condition does not hold |
| run your own script in the page, and get a JSON result back (off by default — see below) |
| every recipe Beifahrer knows, where it came from, and files it refused |
| the recipes that fit a tab, by URL or by recognising the app on the page |
| run one step by step; stops at the first failing step and names it |
A recipe only has steps like find, click, fill, wait and read, each an ordinary beifahrer call. The browser checks every step like a call the agent made itself: the site's level, your confirmation, the Stop button. A recipe carries no code. A step that publishes (posting the comment, saving) runs only when the agent says you asked for exactly that. Otherwise the run stops before it and leaves the draft on the page for you to read.
page_expect is a different question from page_wait. page_wait asks is that element there
yet; page_expect asks is this true yet — the button is enabled, the spinner has gone, the field
holds that value, the page has exactly that many of them. A failed condition is an answer, not an
error: it comes back with what the elements are actually in, their values and their number, so the
next step is a decision rather than another read of the page. A query is a claim about the whole page
— "gone" means no match is visible, which is what waiting for a spinner to disappear means — and it
is asked of the frames you allowed as well, the answer naming the frame that decided it. One thing
it will not do is assert nothing: with no state, value, text or count given it is refused rather than
answered with a default visible, which would be a pass about a page nobody looked at.
Beifahrer ships openproject/add-comment and openproject/edit-description. They recognise
OpenProject on any domain. Your own recipes go in ~/.config/beifahrer/recipes/ or in the
directories listed in $BEIFAHRER_RECIPES (colon-separated). A later source replaces a recipe
with the same id. This repository is public: recipes that name a company, a customer's domain
or an internal process belong in your own directory. Format and how to contribute:
recipes/README.md. Why it is built this way:
ADR 0006.
Frames: what the page embeds
A checkout widget, a video, a comment box, a map: a modern page is mostly other people's pages in
boxes. page_read, page_outline, page_find, page_wait and page_expect see those too, each
under its own heading in the answer, so a reader can tell which content belongs to which site.
A frame is asked under its own origin, not the page's. A frame from a site you never allowed is
invisible — in the outline, in the text, in a search — even when the page around it is readable.
That is not a limitation but the point: granting a site does not grant the things that site embeds.
An extension that injected itself into every frame and checked the top page's permission would let an
agent write into a third party's widget through the page's consent, which is the whole reason
all_frames was off the table until now.
Where you click is the other half. Every frame has its own element registry, so a ref says which
document it belongs to: e12 in the page, b2e12 in the second frame. A write with b2e12 is gated
on the frame's origin and confirmed in a window that names the frame's origin — the site in
which the change actually happens. And a pill inside a frame says "Agent is reading this embedded
page" rather than the bare words that would name the wrong site.
A ref does not survive a navigation, and that is deliberate rather than a limitation to work around:
frame numbers belong to a document, so a b2e12 from before a page change finds no frame and is
refused. The alternative — reusing the number for whatever sits there now — would mean a ref silently
addressing a different element in a different document, which for a write is the one thing that must
never happen. The same holds for a frame that navigates: its origin is asked again every time, so it
loses access the moment the new origin is not one you allowed.
Two things it does not do, both on purpose. page_press without a ref stays in the top
document: the agent cannot see which frame the focus is in, and guessing would send keys into a
document nobody chose. And a frame that has not loaded, or that is on a site below Read, is not
asked at all — the answer is simply shorter, which is why the outline says truncated whenever
there was more than one document.
Keys, dropdowns and checkboxes
A form is not only fields. It is keys: Enter to submit, Tab to the next field, a character at a time into a search box that filters as you type, an ArrowDown in a listbox. Three tools cover what a person does with a keyboard, and they all sit behind the Fill in forms switch at Read + edit:
Tool | Does |
| one key ( |
| options in a |
| a checkbox, a radio or a |
The honest limit. The events Beifahrer sends are untrusted, and a browser performs no default action for an untrusted key event — a real Enter submits the form, a dispatched one does not. So Beifahrer produces those effects itself, per key, where the browser's behaviour is defined and reproducible: the character appears, Backspace deletes the selection, Tab moves the focus on, Enter submits the form the field is in (and types a newline in a text area), Space ticks a box. A page that listens for keys — a hotkey, a combobox, a code editor — needs nothing from that and gets exactly what it would have got.
Three things it will not do, on purpose:
A key of the browser's own (F12, PrintScreen, Ctrl+T) is refused by name, not sent into the void. Silently dropping it looks like a page that ignored the key, which sends an agent looking in the wrong place.
A modifier on its own is refused: it presses nothing, and answering "done" for a call that did nothing is the one answer no agent can work around.
A password field is never typed into, exactly like
page_fill.A one-time code the person relayed goes in only through
page_enter_code(below).
A widget built from divs instead of a <select> has no options to set. Beifahrer says so, names
the choices, and points at page_click — setting aria-checked by hand would make a widget look
ticked while the page's own state never heard of it, and that is the one thing a write must not do.
The focus is marked [focus] in the outline, because page_press without a ref goes wherever
the focus is, and an agent that cannot see the focus cannot use that form of the call at all.
Running a script in the page
A page agent is a visitor, not a keyboard. It cannot hover a menu open, drag something, or touch a
<canvas> — for those pages the agent can see the thing and cannot operate it. page_evaluate is
the way out, and it is guarded accordingly:
Where | Firefox only. In Manifest V3 a content script shares the extension's content security policy, and that policy cannot name |
Switch | Run scripts, in your options page, off by default — beside Screenshots and the other far-reaching capabilities |
Level | the site must be at Read + edit. A site you left at Read refuses a script even with the switch on |
Confirmation | by default every run opens a window showing the whole script, with Allow and Deny. No "Always allow" in that window, and Ask before changes never covers a script. Only switching off the separate Ask before running scripts, per site or for "All sites" and behind a warning, skips the window (ADR 0016) |
Reach | Beifahrer's own isolated world: the DOM is there, the extension's own APIs ( |
The script is a function body, so return gives the result and await works. In scope is
beifahrer, with the same verbs the named tools have — find, describe, click, fill,
press, select, check, read, outline, meta, sleep — so a script reads like a recipe
instead of reaching into the DOM blind:
const b = beifahrer.find({ role: 'button', name: 'Save' })[0];
beifahrer.click(b.ref);
await beifahrer.sleep(300);
return beifahrer.read(2000);The result comes back as JSON under a character budget, with truncated: true when something was
left out — a flag worth honouring rather than filling the gap by guessing.
Three limits, stated rather than hidden. A script that never yields holds the tab's main thread and
cannot be stopped from here; the person has to close or reload the tab — but one that does
await can be: its beifahrer.* calls stop working once the call has ended or you pressed Stop, and
the in-page Stop button is put back if the page removes it. A script may also not reach the page's own
JavaScript by name (window.appState) on Chromium; on Firefox it can ask for it through
window.wrappedJSObject, which every content script there has. So the world is not a boundary —
which is exactly why world: "main" is answered unsupported with a reason instead of being faked:
an installed extension has no API that runs its own code as a string inside a page's world. And the
price of all of it: a script may do anything a Read + edit grant allows on that site, reading a
password field included — which page_fill and page_press refuse. Why the conditions above are the conditions:
ADR 0012.
When a page did not do the thing
There is no console tool, and there will not be one. A content script shares the page's DOM but
not the page's JavaScript world: the page's console.log goes to the page's console, Beifahrer's to
the extension's, and no browser API bridges the two. The only way in is defeating the page's own CSP
with a <script> element, which is what ADR 0012
refuses. A tool that answered "no entries" would be a lie, and a lie there is worse than a gap: an
agent would conclude the page logged nothing when it cannot know.
What there is instead is page_network: what the tab requested and what came back — method, URL,
status, the browser's own type, and on failure the browser's own error (net::ERR_ABORTED and
friends). That is the question behind most "the button did nothing": the POST that returned 500, the
request a CSP or a CORS header refused before it went out, the third-party script that never loaded.
It is the furthest-reaching switch in the list, and the only one whose permission cannot be narrowed:
What you decide | two things in one click — the switch, and the browser's |
Why it is off by default | the permission is a request log of everything this browser does. There is no per-site form of it, so a release build keeps it optional and out of the install prompt |
What is never in it | the query and the fragment. |
What it can see | the page's own requests and the third-party ones it makes, at Read on the tab's own site. A frame from a site you never allowed is not asked, exactly as for a read |
How much | the last 200 requests of that tab, in memory, gone with the browser |
What it will never do
Run arbitrary JavaScript unless you switched it on.
page_evaluateis off by default, needs Read + edit on the site, and asks every single time. With the switch off, every action is one of the tools above, so the levels mean what they say.- Fill a password field. You type those yourself. The only exception is a one-time code you gave the agent, with Type one-time codes you relay switched on.Hold your credentials. It uses the session you are already logged into.
Send anything off your computer. The only channel is the loopback socket to the bridge you run yourself.
Switch your tab behind your back. A screenshot is only taken of the tab you are showing.
Rearrange or close your tabs unless you let it. Managing tabs and windows is a feature, off by default, and closing tabs asks you first.
Keep going after you press Stop. Paused means every request is refused until you resume.
Page text is written by whoever runs the site, and an agent reading it can be steered by it (prompt injection). That is the reason for the per-site levels: keep your bank at Read, and an injected instruction has nothing to write with.
Install (from source, for now)
Requires gjsify and GJS, nothing else: the extension is
bundled on GJS (ADR 0002), and the bridge runs on GJS or
on Node 24 — gjsify routes GJS-only imports per --app while building, so the two need separate
bundles: the GJS one keeps its gi://, which a bare node refuses.
gjsify install
gjsify workspace beifahrer-cli build # app/dist/beifahrer.gjs.mjs, run on GJS
gjsify workspace beifahrer-cli build:node # app/dist/beifahrer.node.mjs, run on Node 24
gjsify workspace beifahrer-extension build # extension/.output/<target>/, on GJSEvery target is a folder and, with --zip, its own download: chrome-mv3, edge-mv3,
firefox-mv2, safari-mv3. Edge gets the chrome-mv3 manifest unchanged — one Chromium, one
manifest — and reports itself as Microsoft Edge under the chromium family at runtime, so nothing
else in the build knows the difference.
Chromium:
chrome://extensions→ Developer mode → Load unpacked →extension/.output/chrome-mv3Edge: the same folder loads as
edge://extensions→ Developer mode → Load unpacked. Edge and Chrome are the same build;edge-mv3exists so the extension can be handed to a person on Edge without asking them to rename a folder.Firefox, to try it:
about:debugging#/runtime/this-firefox→ Load Temporary Add-on →extension/.output/firefox-mv2/manifest.json. Firefox forgets it on restart.Firefox, to keep it: sign it as an unlisted add-on (AMO signs it, nothing is published): put
WEB_EXT_API_KEY/WEB_EXT_API_SECRETfrom your AMO API key page into~/.config/beifahrer/amo.env, rungjsify workspace beifahrer-extension sign, and open the.xpifromextension/.output/signed/in Firefox.Firefox, to keep up to date: pass the tag —
gjsify workspace beifahrer-extension sign v0.2.0— and sign.sh also writesupdates.json, the file Firefox polls to find a newer build. Attach it to the release under exactly that name: the build names its update URLreleases/latest/download/updates.json, and Firefox keeps the URL from the installed version for good, so it has to stay put and stay version-free.
Pair it:
gjsify run app/dist/beifahrer.gjs.mjs token # prints the token; paste it in the extension's optionsRegister the MCP server with your agent. It needs mcp, and --allow-write if the agent may
fill and click at all (the extension's per-site level and its confirmations still apply):
{
"mcpServers": {
"beifahrer": {
"command": "gjsify",
"args": ["run", "/path/to/beifahrer/app/dist/beifahrer.gjs.mjs", "mcp", "--allow-write"]
}
}
}A session that just started. The extension finds a new session within a few seconds, so the
first call waits up to 15 s for the first browser (--wait <seconds> or BEIFAHRER_BROWSER_WAIT).
After a browser has left, a call fails at once: a person's disconnect is never retried.
Files on disk instead of base64. page_capture and recipe_run return a captured document as
base64, which costs an agent that only wants to hand the file on (to a Paperless upload, say) its
context. Start the server with --save-dir /abs/path (or BEIFAHRER_SAVE_DIR) and a call may pass
saveDir, an absolute directory at or below that one; the answer is then {path, filename, mime, size, sha256, via} and no bytes. Without the setting saveDir is refused, before anything is
clicked. Files are named after the download, never overwritten, and readable by you only
(ADR 0021).
beifahrer config <agent> prints this snippet in the format of your agent (Claude Code, opencode,
Codex CLI, Gemini CLI, Cursor, VS Code, Claude Desktop, Zed) and the file it goes in. It writes beifahrer
as the command, which is the installed binary; from a checkout, put the gjsify run … form above in
its place.
Then open a site, click the Beifahrer toolbar button, and pick a level.
If the browser says it is not connected, beifahrer doctor says why: no free port in the range, a
listener on a port that no session announced, no token or two different ones, no agent session, no
browser on it. Each finding comes with the one thing to do. It changes nothing (it does not even
create the token) and exits 1 only when something is broken, not for a warning.
Ten sessions at once is the default. For more, raise the range on both sides to the same number:
BEIFAHRER_PORT_COUNT (or --port-count) for the bridges, and Ports in the extension options.
Browsers
Chromium (Chrome, Brave, Edge, …) | Firefox | Epiphany (GNOME Web) | |
Manifest | MV3 | MV2 | MV2 |
Status | ✅ | ✅ | ⏸ blocked upstream: measured |
Without an MCP client
beifahrer tool runs any MCP tool from the command line, through the same server and the same
gates. It listens on its own port of the range, like any agent session, and a call waits (up to
--wait, 20 s) for the extension to find it. That helps in a session that started before
Beifahrer was registered, and it also works from scripts:
gjsify run app/dist/beifahrer.gjs.mjs tool --list
gjsify run app/dist/beifahrer.gjs.mjs tool tabs_list
gjsify run app/dist/beifahrer.gjs.mjs tool --allow-write recipe_run '{"tabId": 78, "id": "openproject/add-comment", "params": {"text": "…"}}'Development
gjsify workspace beifahrer-extension dev # Firefox with the extension, rebuilt + reloaded on every change
gjsify workspace beifahrer-extension dev:chromium # the same in Chromium (Playwright's build or $BEIFAHRER_E2E_CHROMIUM)The dev browser runs in its own persistent profile (~/.cache/beifahrer/dev-*), is paired
automatically with your local token, and looks for sessions on ports 47830–47839, so it never
meets the agent sessions of your everyday browser. Drive it with
gjsify run app/dist/beifahrer.gjs.mjs call <method> --port 47830.
gjsify workspace beifahrer-cli test # unit tests, on GJS and Node
node tests/e2e/browsers.e2e.mjs all # the full chain in headless Chromium + FirefoxDesign decisions: docs/adr/ — index: docs/. Contributor and agent rules: AGENTS.md.
Desktop window (preview)
beifahrer status prints which agent sessions are running and which browsers hang on them; the
native libadwaita window in desktop/ shows the same live (ADR 0017).
Its Agent page shows, per coding agent, the snippet that registers Beifahrer and the file it
goes in (beifahrer config <agent> prints the same). Its Browser page shows the pairing token to copy into each browser's extension (masked until you
reveal it), and warns when a second token file with a different token exists. Both it and the
command also say when something listens on a port of the range that no running session announced
(an older Beifahrer, a hung bridge, another program), by a plain TCP connect that sends nothing. It only looks: it cannot pause the agent or change what it may do, and it knows nothing of a pause
(that is the toolbar button's to show). Not packaged yet; from a checkout:
gjsify workspace beifahrer-desktop build && gjsify workspace beifahrer-desktop start.
Run so far on macOS arm64 only.
Releasing
Bump first with node tools/version.mjs set X.Y.Z and commit chore: bump to X.Y.Z: the extension,
the CLI, core and the root share ONE version, and the release fails before building anything if the
tag is not vX.Y.Z of exactly that. Then git tag vX.Y.Z && git push --tags runs the whole of CI — tests, type check, lint — and, if it
passes, builds and attaches every installable format to the tag's GitHub release:
browser extension zips (Firefox + Chromium)
the CLI:
.deb,.rpm,.flatpak, a macOS.app.zip(arm64 + x64), a Windows program directory.zipand.msi(x64)
All of it unsigned, which is a legitimate deliverable rather than a placeholder (gjsify ADR 0024
§ A13) — see the comment above the "Attach everything to the release" step in
release.yml for where --sign/--notarize would attach once a
signing identity exists. On macOS an unsigned .app downloaded in a browser carries the quarantine
attribute and is killed at launch without a message; run xattr -dr com.apple.quarantine Beifahrer.app once after unzipping. (Measured with a hand-set attribute, not a real browser
download; ad-hoc signing with --sign - did not change the result, so it is not used.) What is NOT
in the release: a signed Firefox .xpi (manual, needs a
person's AMO key) and any store submission — both documented in the same file.
License
The apps (app/, extension/, desktop/) and the repository root are
AGPL-3.0-or-later. The reusable packages under packages/ are
LGPL-3.0-or-later, each with its own LICENSE and COPYING.
This server cannot be deployed
Maintenance
Related MCP Connectors
Real Chrome for agents: start a browser, read pages as numbered markdown, click, type, hand off.
Stealth web automation for AI agents. Login, signup, navigate, screenshot.
Stealth web automation for AI agents. Login, signup, navigate, screenshot.
AI-powered browser automation — navigate, click, fill forms, and extract data from any website.
Related MCP Servers
- AlicenseNot gradedqualityAmaintenanceEnables AI assistants to control and automate your Chrome browser directly, leveraging existing login states and configurations for tasks like content analysis, semantic search across tabs, screenshots, network monitoring, and interactive operations.12MIT
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to control a browser for web automation tasks like navigation, typing, clicking, and taking screenshots.-
- FlicenseNot gradedqualityCmaintenanceEnables AI assistants to control a browser for web automation tasks such as navigation, typing, clicking, and taking screenshots.-
- AlicenseNot gradedqualityBmaintenanceEnables AI agents to request and receive user-granted, short-lived access to a single tab in the user's existing Chromium-based browser, then read, highlight, scroll, and navigate within that granted page without exposing other tabs or copying the browser profile.Apache 2.0