Metadata-Version: 2.5
Name: invisible-playwright-mcp
Version: 0.11.0
Summary: Stealth Firefox browser as an MCP server for AI agents
Project-URL: Homepage, https://github.com/feder-cr/invisible-playwright-mcp
Project-URL: Repository, https://github.com/feder-cr/invisible-playwright-mcp
Author-email: feder-cr <85809106+feder-cr@users.noreply.github.com>
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agent,anti-detect,browser-automation,firefox,mcp,playwright,stealth
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Requires-Dist: invisible-playwright>=0.9.0
Requires-Dist: mcp<2,>=1.2
Requires-Dist: selectolax>=0.3.21
Provides-Extra: test
Requires-Dist: pytest-asyncio>=0.23; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# invisible-playwright-mcp

A stealth Firefox, exposed as an [MCP](https://modelcontextprotocol.io) server, so
an AI agent can browse and act on real websites without being blocked by anti-bot
systems.

It wraps [`invisible-playwright`](https://github.com/feder-cr/invisible_playwright),
a real Firefox patched at the C++ source, and hands your client the usual browser
tools: navigate, click, type, read, screenshot. The fingerprint is set inside the
engine, not bolted onto the page.

## First, one prerequisite

**Python 3.11 or newer**, on **Windows (x86_64) or Linux (x86_64, arm64)** - macOS
is not supported, the last engine build for it was `firefox-20`. Then
[uv](https://docs.astral.sh/uv/), because the command below starts with `uvx`:

```bash
curl -LsSf https://astral.sh/uv/install.sh | sh              # Linux
```
```powershell
irm https://astral.sh/uv/install.ps1 | iex                   # Windows
```

## Then pick one

**Do you already use an AI assistant that can run tools?** If yes, it brings the
model and you add this browser to it. If no, or if you would rather watch the work
happen, there is an interface that brings a model too.

### 1. Add it to the assistant you have

**This package.** Your assistant brings the model.

Claude Code, once for every project on the machine:

```bash
claude mcp add -s user stealth -- uvx invisible-playwright-mcp
```

Drop `-s user` if you want it in the current project only. Then check it took,
before trusting it:

```bash
claude mcp list
```

For every other client, see **Adding it to your client** below.

### 2. Run the interface instead

**[AIHawk](https://github.com/feder-cr/AIHawk).** No assistant needed. Bring an
[OpenRouter](https://openrouter.ai) account and its key, get a page with the chat
on the left and the live browser on the right.

```bash
uvx aihawk ui --openrouter-key sk-or-...
```

Open **http://127.0.0.1:8765**.

It is a client of this server like any other, with no private path to the page -
which is the reason to believe the tools below are enough to build on.

## The download nobody warns you about

The browser is about a quarter of a gigabyte and it is **not** fetched when the
server is installed, nor when it starts. It arrives on the **first tool call that
needs a page**, so the first thing you ask your assistant to do sits there, and on
a slow connection you get a timeout that says nothing about a download.

Get it over with first, in a terminal where you can watch it:

```bash
uvx invisible-playwright fetch
```

Cached afterwards, and shared with anything else that uses this engine.
Per-platform sizes are in the
[engine's README](https://github.com/feder-cr/invisible_playwright).

## Adding it to your client

Four clients have a command for this. The rest take a config file, and the file
is not the same everywhere: **three different top-level keys, and one of them is
not even JSON.** Find yours below.

### If your client has a command

**Claude Code.** `-s user` because the default scope is the current project, so
without it the tools do not appear anywhere else and nothing reports an error:

```bash
claude mcp add -s user stealth -- uvx invisible-playwright-mcp
```

**Codex:**

```bash
codex mcp add stealth -- uvx invisible-playwright-mcp
```

**Gemini CLI.** No `--` separator here, unlike the two above:

```bash
gemini mcp add -s user stealth uvx invisible-playwright-mcp
```

**VS Code** (GitHub Copilot agent mode):

```bash
code --add-mcp "{\"name\":\"stealth\",\"command\":\"uvx\",\"args\":[\"invisible-playwright-mcp\"]}"
```

### If your client takes a config file

**Most use a top-level `mcpServers`** - Claude Desktop, Cursor, Windsurf, Cline:

```json
{
  "mcpServers": {
    "stealth": {
      "command": "uvx",
      "args": ["invisible-playwright-mcp"]
    }
  }
}
```

| Client | File |
|---|---|
| Claude Desktop (macOS) | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Claude Desktop (Windows) | `%APPDATA%\Claude\claude_desktop_config.json` |
| Cursor | `.cursor/mcp.json` in the project, or `~/.cursor/mcp.json` for every project |
| Windsurf | `~/.codeium/windsurf/mcp_config.json` |
| Cline | `~/.cline/data/settings/cline_mcp_settings.json`, or the **Configure MCP Servers** button in its MCP panel, which opens whichever file your version uses |

**Zed calls the key `context_servers`**, not `mcpServers`, in
`~/.config/zed/settings.json` (`%APPDATA%\Zed\settings.json` on Windows):

```json
{
  "context_servers": {
    "stealth": {
      "command": "uvx",
      "args": ["invisible-playwright-mcp"]
    }
  }
}
```

**VS Code calls it `servers`**, in `.vscode/mcp.json` for a workspace:

```json
{
  "servers": {
    "stealth": {
      "type": "stdio",
      "command": "uvx",
      "args": ["invisible-playwright-mcp"]
    }
  }
}
```

**Codex uses TOML**, in `~/.codex/config.toml`:

```toml
[mcp_servers.stealth]
command = "uvx"
args = ["invisible-playwright-mcp"]
```

**Continue** uses YAML with its own block format, which changed recently enough
that we would rather point you at
[their documentation](https://docs.continue.dev/customize/deep-dives/mcp) than
print a block here that may already be stale.

### Where a proxy and the other settings go

Everything in **Settings** below goes under `env` on the server entry, in
whatever shape your client uses:

```json
{
  "mcpServers": {
    "stealth": {
      "command": "uvx",
      "args": ["invisible-playwright-mcp"],
      "env": {
        "STEALTHFOX_PROXY": "http://user:pass@proxy.example.com:8080",
        "STEALTHFOX_SEED": "4242"
      }
    }
  }
}
```

In Codex's TOML that is a `[mcp_servers.stealth.env]` table; on the command line,
Claude Code and Codex take `-e KEY=value` and `--env KEY=value`.

⛔ **"Added" is not "connected".** Every one of these writes a config entry
without running anything, so a typo, a missing `uv`, or the first-run browser
download all surface later as a server that will not start. Check before you
trust it: `claude mcp list`, `codex mcp list`, or your client's MCP panel.

## Settings

Environment variables, all optional. A proxy is the one worth adding: without it
the exit IP, timezone and locale are your own machine's, which is a real gap
between what the browser says it is and where it appears to be.

| Variable | Meaning |
|---|---|
| `STEALTHFOX_PROXY` | Proxy URL, e.g. `http://user:pass@proxy.example.com:8080` or `socks5://proxy.example.com:1080`. Host and port are both required. Bring your own. With it set, the session's timezone, locale and egress are derived from the proxy. |
| `STEALTHFOX_NO_PROXY` | `1` to go out from this machine's own address even when `STEALTHFOX_PROXY` is set. |
| `STEALTHFOX_SEED` | Integer seed for a deterministic fingerprint (same seed, same identity). A profile's own seed wins over this one. |
| `STEALTHFOX_PROFILE_DIR` | A directory for a persistent profile, so logins survive across runs. |
| `STEALTHFOX_BINARY` | Path to an engine binary you already have. It must be the build the packaged seal pins, or startup refuses. |
| `STEALTHFOX_HEADLESS` | `0` to run headed; headless by default. |
| `STEALTHFOX_MCP_TRANSPORT` | `http` to serve over streamable HTTP instead of stdio. Default is stdio, which is what MCP clients expect. |
| `STEALTHFOX_MCP_HOST` | Bind address for the HTTP transport. Default `127.0.0.1`. |
| `STEALTHFOX_MCP_PORT` | Port for the HTTP transport. Default `8765`, which is also the AIHawk interface's default: change one of the two if you run both. |

## Tools

`session_start`, `session_status`, `session_new_page`, `session_list_pages`, `session_select_page`, `session_close_page`, `browser_navigate`, `browser_read_text`, `browser_snapshot`, `browser_read_html`, `browser_click`, `browser_click_at`, `browser_type`, `browser_press_key`, `browser_evaluate`, `browser_take_screenshot`, `browser_select_option`.

### Who is browsing

You can ignore this entirely. Say nothing and every session is a different
stranger, which is the right default.

When you do care, `session_start` chooses the person and `session_status` tells
you who you currently are. There is one browser, so two identities are visited
in turn rather than at once.

- **`seed`** is the identity. The same seed is the same fingerprint every time.
  Leave it out and one is drawn for you, and the answer says which, so a session
  worth repeating can be repeated.
- **`profile`** is a directory holding cookies and logins. **A profile also owns
  its seed**: the first session on a new one stores the identity inside it and
  every session after reuses it, so a login never comes back wearing different
  hardware. Ask for a seed that contradicts the one a profile carries and you get
  a refusal naming both numbers, never a silent choice between them.
- **`proxy`** is where the traffic leaves. Pass `""` to either of the last two to
  insist on *none*, which is how you get sessions a site cannot link together
  even when the environment sets a default.

**A profile does not own its exit the way it owns its seed.** Timezone, locale
and geography come from the exit, so the same login arriving from another country
is as visible as one arriving on different hardware. You are warned when a
profile's exit changes - but only when *you* change it. A provider rotating its
own addresses behind one host and port is indistinguishable from here, and no
warning should be read as saying otherwise.

### The order to try them in

The server hands every client this ladder, because a model that cannot find a
way down it invents one:

1. **A named tool with a selector** - `browser_click`, `browser_type`,
   `browser_select_option`, `browser_press_key`. `browser_snapshot` supplies the
   selector.
2. **Coordinates** - the snapshot reports `at: [x, y]` for every element, and
   `browser_click_at` moves the pointer there. For a canvas, a slider, a map, a
   widget built out of divs.
3. **A screenshot** - `browser_take_screenshot`, then `browser_click_at` on what
   you can see. For what the snapshot does not list at all.
4. **`browser_evaluate`**, to READ what none of the above can see.

`browser_evaluate` reads; it will not act. Assigning to `value`, `checked` or
`selected`, or calling `click()`, `dispatchEvent()` or `submit()`, is refused,
and the refusal names the tool to use instead. Script reaches the page with no
keystroke and no pointer, so the event carries `isTrusted` false, which is the
clearest signal a page can collect that nobody is really there. Reading those
properties is fine.


`browser_click_at` takes viewport coordinates instead of a selector, moves the
pointer there rather than teleporting, and optionally holds before releasing -
for a slider, a canvas-drawn challenge, or a press-and-hold.

`browser_snapshot` returns the title, the url and the visible interactive
elements rather than the accessibility tree. Every element carries `at: [x, y]`,
its centre in the viewport, and a `selector` as well when one can reach it -
pass that straight to `browser_click` or `browser_type`. `browser_read_html`
returns reduced markup instead, for when the structure is what matters.

Tool names mirror the Microsoft Playwright MCP, so prompts written for it work
here too.

**Why each of those returns what it does**, with the measurements behind it:
[docs/tool-design.md](docs/tool-design.md).

## More than one client on the same browser

Over stdio the browser belongs to the client that opened it. Set
`STEALTHFOX_MCP_TRANSPORT=http` and it does not: the session is owned by the
server, so a second client can attach to the browser the first one left open,
and closing a client no longer kills the browser.

To SEE the browser rather than share it, use
[AIHawk](https://github.com/feder-cr/AIHawk) from column 2, which shows the live
page beside the conversation.

```bash
STEALTHFOX_MCP_TRANSPORT=http uvx invisible-playwright-mcp        # Linux
```
```powershell
$env:STEALTHFOX_MCP_TRANSPORT = "http"; uvx invisible-playwright-mcp   # Windows
```

## Notes

- This is a browser, not a captcha solver. It does not solve or bypass challenges for you; it makes an ordinary Firefox session look like a real one.

## License

[MIT](https://github.com/feder-cr/invisible-playwright-mcp/blob/main/LICENSE),
the same as the engine it wraps.
