invisible-playwright-mcp
Server Configuration
Describes the environment variables required to run the server.
| Name | Required | Description | Default |
|---|---|---|---|
| STEALTHFOX_SEED | No | Integer seed for a deterministic fingerprint (same seed, same identity). | |
| STEALTHFOX_PROXY | No | Your proxy URL (http://…, https://… or socks5://…). Bring your own. With it set, the session's timezone, locale and egress are derived from the proxy automatically. | |
| STEALTHFOX_BINARY | No | Path to a specific engine binary (otherwise fetched automatically). | |
| STEALTHFOX_HEADLESS | No | 0 to run headed; headless by default. | |
| STEALTHFOX_MCP_HOST | No | Bind address for the HTTP transport. Default 127.0.0.1. | 127.0.0.1 |
| STEALTHFOX_MCP_PORT | No | Port for the HTTP transport. Default 8765. | 8765 |
| STEALTHFOX_PROFILE_DIR | No | A directory for a persistent profile, so logins survive across runs. | |
| STEALTHFOX_MCP_TRANSPORT | No | http to serve over streamable HTTP instead of stdio. Default is stdio, which is what MCP clients expect. | stdio |
Instructions
Guidance the server publishes about itself, which clients place ahead of the tool catalog so the model reads it before choosing anything.
This server publishes no instructions, or was last inspected before Glama recorded them.
Capabilities
Features and capabilities supported by this server
Protocol revision2025-11-25
| Capability | Details |
|---|---|
| tools | {
"listChanged": false
} |
| prompts | {
"listChanged": false
} |
| resources | {
"subscribe": false,
"listChanged": false
} |
| experimental | {} |
Tools
Functions exposed to the LLM to take actions
| Name | Description |
|---|---|
| session_statusA | Who is browsing right now: the identity, the exit, the profile and the tabs. Ask whenever you need to know which person the browser currently is, or from
where its traffic leaves. The seed is what you would pass to It starts nothing. If no browser is running yet it says so, because until one is running there is no identity to report. |
| session_startA | Start a browsing session as a particular person, and say who that is. Call this when you want to control WHO is browsing: a fresh stranger, the same person as last time, or a saved profile that is already logged in somewhere. Calling it closes whatever browser is open and starts another, so anything not saved in a profile is gone. You do not have to call it at all. The first tool that needs a page starts a
session on its own; There is only ONE browser. Two identities are visited in turn, never at the same time, so a task that needs both accounts live at once cannot be done here and is worth saying so rather than half-starting. seed the browser identity. Same seed, same fingerprint, every time.
Leave it out and one is drawn, and the answer tells you which, so
you can ask for it again later.
profile a directory that keeps cookies and logins between sessions. A
profile also KEEPS ITS SEED: the first session on a new one stores
the identity inside it, and every session after reuses it, so a
login does not come back wearing different hardware. Pass "" to
insist on no profile at all, which is how you get sessions a site
cannot link to each other. A relative path is resolved against the
server's own directory, so the answer reports the full path it
used.
proxy where the traffic goes out, as |
| session_new_pageA | Open a new tab and make it the active one. Returns its page id. Tabs persist across calls and across clients, so this is how you keep one page while working on another rather than navigating back and forth. |
| session_list_pagesA | Every open tab: id, title, url, and which one is active. Use it before session_select_page: the id alone does not tell you which tab you are switching to. |
| session_select_pageA | Switch the active tab. Every other browser_* tool acts on it. Take the id from session_list_pages or from session_new_page. |
| session_close_pageA | Close a tab, or the active one when page_id is left out. |
| browser_navigateA | Go to a url in the active tab, opening one if none exists. wait_until is "domcontentloaded" by default, which returns as soon as the markup is parsed. Use "load" when the page needs its images and stylesheets, or "networkidle" for a single-page app that fetches its content after load. |
| browser_read_textA | The visible text of an element, with the markup gone. The cheapest way to read a page. Narrow the selector when you know where the answer is; use browser_read_html instead when the structure matters, or browser_snapshot when you need something to click. Long text is cut at max_chars (6000 by default) and the cut is marked in what comes back, so text that ends without that marker is the whole thing. |
| browser_snapshotA | Title, url, and the interactive elements that are actually visible. Each element carries a Elements with no Not the accessibility tree: on a real sign-up page a single country
|
| browser_read_htmlA | The page's HTML, cleaned down to what is worth reading. Use this when the STRUCTURE matters - a form and its labels, a table, what
a control is wired to. mode="form" keeps the interactive surface and the text explaining it, mode="text" returns the prose alone, mode="full" keeps the structure with the noise and the attribute soup removed. Unlike browser_read_text this is NOT capped: it returns the whole reduced page, which on a large one is tens of thousands of characters. That is deliberate, because cutting markup in the middle leaves tags that no longer mean anything - but it means the answer can be long. Reach for browser_snapshot when you only need something to click, or browser_read_text when you only need the words. |
| browser_take_screenshotA | One screenshot of the active tab, on demand. |
| browser_watchA | The whole browser window as a person at the machine sees it: tab strip, address bar, the page and the pointer, from a live capture kept running on the active tab. For watching the work, not for acting on it: the picture is window pixels, so do not feed its coordinates to browser_click_at; use browser_take_screenshot for that. |
| browser_clickA | Click the first element matching a CSS selector. Scrolls it into view and waits for it to be clickable. When no selector can describe the target, use browser_click_at with coordinates from browser_snapshot. |
| browser_click_atA | Click (or press-and-hold) a raw viewport coordinate instead of a selector - for targets a selector cannot reliably reach: a slider track, a canvas-drawn captcha, or a precise point inside a wider element. Moves the pointer there first (no teleport), then down, then up, holding first if hold_seconds is set. Returns a screenshot taken right after release. hold_seconds needs invisible-playwright 0.9.0 or newer to mean anything. In every earlier version the wait it is built on returned instantly, so the press and the release happened in the same frame and the hold never happened - on the one tool that exists for sliders and press-and-hold challenges. The floor in pyproject.toml is set accordingly. Coordinates are relative to the VIEWPORT, not to the page, so the ones in a
snapshot go stale the moment anything scrolls: a click, a keypress, a lazy
image loading in above the fold. Nothing raises when that happens - the
click simply lands on whatever is at that spot now. Take a fresh snapshot
after anything that could have moved the page, and prefer browser_click with
the element's |
| browser_typeA | Fill a field, replacing whatever it holds. This sets the value rather than typing key by key, so it will not fire the per-keystroke handlers an autocomplete needs. For those, click the field and use browser_press_key. |
| browser_select_optionA | Choose an option in a dropdown ( Use this rather than clicking the dropdown and pressing arrow keys: a click plus arrows cannot tell you which row it landed on, and setting the value through browser_evaluate changes it without the page seeing a real interaction. |
| browser_press_keyA | Press a key on whatever has focus: "Enter", "Tab", "Escape", "ArrowDown", "Control+a", or a single character. |
| browser_evaluateA | READ from the page with JavaScript and get the result as JSON. For what the other tools cannot see: a computed style, a value held in a framework's state, the length of a list. Acting on the page is refused, and the refusal names the tool to use.
Assigning to The refusal catches the obvious spellings, not every possible one. A script that slips past it is still the wrong way to do the thing: report it in your answer rather than using it. |
Prompts
Interactive templates invoked by user choice
| Name | Description |
|---|---|
No prompts | |
Resources
Contextual data attached and managed by the client
| Name | Description |
|---|---|
No resources | |
TDQS
Scored across 18 tools
Each tool targets a distinct operation: session lifecycle, tab management, navigation, reading modes, and input actions are cleanly separated. Even the overlapping read tools (text, HTML, snapshot) explicitly describe when to use each, so an agent can select them without ambiguity.
The set consistently uses snake_case with `session_*` and `browser_*` prefixes and mostly action+object names like `browser_select_option` and `session_close_page`. Minor noun-style exceptions such as `session_status` and `browser_snapshot` are small deviations but do not break the overall pattern.
18 tools is slightly above the typical 3-15 sweet spot, but for browser automation each tool earns its place by covering a distinct capability. There is little redundant filler, so the count feels reasonable rather than bloated.
The surface covers session/identity management, tab lifecycle, navigation, multiple read modes, and the common input actions well. Obvious gaps are minor—no explicit wait-for-selector, hover, upload, or back/forward—but agents can usually work around them using navigate, wait_until, click, or press_key.