Browser-MCP Navigator
This server lets an MCP-compatible AI agent control a real Chrome browser over CDP, using compact accessibility snapshots (@eN refs) instead of screenshots for fast, token-efficient automation.
Browser lifecycle:
browser_startlaunches/reconnects Chrome;browser_stopcloses it.Navigation & state:
navigatewith load/networkidle waits,current_url, andsnapshotfor the accessibility tree with@eNrefs.Page interaction: human-like
click,js_click,fill,set_valuefor React/Vue/Angular inputs,select_option,press_key,hover, andscroll.Reads & waits:
get_text,wait_for,read_console, andread_network.Arbitrary JavaScript / CDP:
js_evalruns custom JS,js_eval_loopperforms bulk operations in one call, andcdp_callexposes raw DevTools Protocol features like file upload, device emulation, and network interception.Advanced web-app handling: persistent scripts that survive navigation, one-call popup/dropdown resolution, and
act_and_observefor transient toasts/inline errors.Utilities: base64
screenshotescape hatch, clear error codes, and performance-optimized bulk patterns for N>5 operations.
Fast-Browser-MCP (Fast-Browser-MCP Navigator)
Ultra-fast browser automation server over Chrome DevTools Protocol (CDP), exposed as a Model Context Protocol (MCP) server.
No screenshots. No Playwright relay. Direct CDP WebSocket — 20–50× fewer tokens, ~10ms per action.
Note: The core Python package is named
fast_browser_mcpinternally.
What it does
Controls a real Chrome browser from any AI agent that supports MCP. The agent receives a compact accessibility-tree snapshot with @eN references after every action — no pixels, no heavy HTML blobs.
Agent ──MCP──► Fast-Browser-MCP Server ──CDP──► ChromeRelated MCP server: Browser Jet Pilot
Quick Start — Docker (Recommended)
git clone https://github.com/ingjohnfigueroablanco/Fast-browser-MCP.git
cd Fast-browser-MCP
cp .env.example .env # optionally set MCP_API_KEY
docker compose up -dServer ready at http://localhost:3067/sse.
Quick Start — Local (No Docker)
git clone https://github.com/ingjohnfigueroablanco/Fast-browser-MCP.git
cd Fast-browser-MCP
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
python -m fast_browser_mcp # stdio modeConnecting from Claude Code
Option A — Local subprocess / stdio
Add to your project's .mcp.json:
{
"mcpServers": {
"fast-browser-mcp": {
"command": "python",
"args": ["-m", "fast_browser_mcp"]
}
}
}Option B — Docker / SSE
{
"mcpServers": {
"fast-browser-mcp": {
"type": "sse",
"url": "http://localhost:3067/sse",
"headers": { "X-API-Key": "your-key" }
}
}
}Connecting from other agents / frameworks
Python agent (mcp SDK)
from mcp import ClientSession
from mcp.client.sse import sse_client
async with sse_client("http://localhost:3067/sse",
headers={"X-API-Key": "your-key"}) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("navigate", {"url": "https://example.com"})
print(result.content[0].text)Testing Localhost Apps (Docker)
The browser runs inside the container. localhost inside Docker ≠ your dev machine.
Approach | How |
Docker Desktop | Use |
ngrok |
|
Local mode | (stdio) Chrome runs on your machine — |
Available Tools
Tool | Description |
| Launch / reconnect Chrome |
| Close Chrome |
| Go to URL, wait for load / networkidle |
| Accessibility tree as compact text with |
| Human-like click by coordinates |
|
|
| Clear + type text in an input |
| Key press (Enter, Tab, Escape, ArrowDown…) |
| Select native |
| Mouse hover (menus, tooltips) |
| React/Vue/Angular-safe input setter via native JS |
| Scroll page or element into view |
| Run any JavaScript — drag, events, async fetch, bulk loops |
| Bulk operations — run a JS snippet once per item |
| Raw CDP protocol — file upload, device emulation, network intercept |
| innerText of element or full page |
| Wait until text appears on page |
| JS console logs (filter by |
| Network requests / responses |
| PNG base64 (escape hatch) |
| Current URL + title |
| Register a JS helper that survives |
| List identifiers of registered persistent scripts |
| Remove a persistent script by identifier |
| Resolve the currently-open dropdown/menu/listbox and click the matching option, in one call |
| Act, then watch the DOM/console for |
Error format
Every tool catches its own failures and returns ERROR code=<CODE> msg=<detail>
instead of raising — code is one of TIMEOUT, CONNECTION_LOST,
NAVIGATED_DURING_EXECUTION, JS_EXCEPTION, STALE_REF,
ELEMENT_NOT_VISIBLE, ELEMENT_GONE, BROWSER_NOT_STARTED, CDP_ERROR,
BAD_ARGUMENT. A TIMEOUT never comes back as a blank message.
@eN refs are single-use per snapshot
Every action tool returns a fresh snapshot with new @eN refs — always use
the refs from the MOST RECENT snapshot. A ref from an older snapshot raises
STALE_REF rather than silently resolving to a different element.
navigate always destroys window state
navigate is a full page load (like typing a URL and pressing Enter) — any
variable/function you defined via js_eval is gone afterward. The tool
reports reloaded=true|false on every call so you know when this happened;
use inject_persistent for helpers that need to survive it.
Raw text alongside the accessible name
A control's accessible name (what snapshot shows in "...") is the browser's
computed fusion of its <label>/aria-label/aria-labelledby — not
necessarily what a plain DOM read of the element shows. When they differ,
snapshot prints both: button "CONDUCTOR" (txt: "Selecciona un conductor").
No extra CDP calls — the raw text comes from StaticText descendants already
present in the accessibility tree.
Popup / dropdown resolution
Any dropdown, combobox, or menu that renders as a child of <body> (React
portals, Vue teleports, a plain absolutely-positioned <div>) can end up
sharing the DOM with unrelated background content that happens to have the
same role/text — a naive "find the option with this text" click can land on
the wrong element with no visible error. snapshot now marks the subtree of
the currently-open popup with [POPUP-ABIERTO] (resolved from
aria-expanded/aria-controls, zero extra CDP calls — it's already in the
accessibility tree every snapshot fetches). Prefer click_popup_option(text)
over clicking a raw @eN for a dropdown option: it resolves the open popup
the same way and clicks inside exactly that subtree in one call, falling back
to a JS-based visible-container search when the site has no ARIA wiring.
Transient UI feedback (toasts, inline errors) — act_and_observe
A toast/snackbar/inline validation message can appear and disappear entirely
within ~2.5-4s — shorter than the round-trip between one tool call that acts
and a separate one that reads the result. act_and_observe(action, ref=..., watch_ms=3000) performs the action and watches the DOM (via the bootstrap's
already-running MutationObserver — no installation delay) and the console
buffer for watch_ms, returning a timeline of what appeared/disappeared and
when, plus any new console output, in one call. For a suspected error after
any action, check console_delta (or read_console) before the DOM — an
error toast fades in seconds, but the console.error it usually also fires
persists in the buffer far longer.
click vs js_click
click dispatches real CDP mouse events only (matches what a user click
fires, including React 17+ delegated handlers) — it does not also fire a
JS element.click(), which would double-trigger the handler. Use js_click
explicitly when CDP mouse events genuinely don't reach a handler.
Performance & Bulk Operations
The bottleneck is LLM round-trips, not the browser
Each tool call costs one full LLM inference + HTTP round-trip. The browser executes CDP in ~10ms.
Pattern | Tool calls | Typical wall time |
20 × (click + fill + click) | 60 | ~10 min |
1 × js_eval_loop with 20 items | 1 | ~15 sec |
Rule: for N > 5 repetitions, use js_eval_loop
# GOOD — 1 tool call for 20 users
js_eval_loop(
items=users,
script="""
document.querySelector('.agregar').click();
await new Promise(r => setTimeout(r, 400));
// ... fill logic ...
document.querySelector('.crear').click();
return item.user;
""",
delay_ms=300,
)Environment Variables
Variable | Default | Description |
|
|
|
|
| SSE bind address |
|
| SSE port |
| (empty) | API key header; empty = no auth |
| auto | Explicit path to chrome.exe |
| (empty) | Extra Chrome flags |
|
|
|
|
|
|
Architecture
mcp/ FastMCP server + tool definitions
browser/ BrowserManager — lifecycle, navigate, snapshot, waits
snapshot/ AX tree → compact text with @eN refs (no screenshots)
actions/ mouse, keyboard, forms (ref → coordinates)
cdp/ Raw CDP WebSocket connection + event buffers
chrome/ Chrome launcher, port scan, reattachWhy it's faster than Playwright:
No Node.js relay — Python speaks CDP directly.
Chrome stays alive between calls (daemon) — 0 ms startup per tool call.
Snapshot = compact AX text, not full YAML or heavy screenshots.
@eNrefs = stablebackendNodeId— no DOM re-query per action.
Available Tools
21 toolsbrowser_startB
Lanza (o reconecta) Chrome con CDP. Debe llamarse antes que las demas tools.
| Name | Required | Description | Default |
|---|---|---|---|
| headless | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
browser_stopB
Cierra la conexion CDP. kill=True ademas termina el proceso Chrome.
| Name | Required | Description | Default |
|---|---|---|---|
| kill | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
cdp_callA
Llama cualquier metodo del protocolo CDP directamente.
method — dominio.metodo, ej: "Input.dispatchKeyEvent", "DOM.querySelector" params — JSON string con los parametros, ej: '{"type":"keyDown","key":"Enter"}' use_session— True (default) para el contexto de la pagina actual; False para nivel browser
Ejemplos de acciones que no tienen tool dedicada: Drag & drop real: cdp_call("Input.dispatchDragEvent", '{"type":"dragEnter",...}') Subir archivo: cdp_call("DOM.setFileInputFiles", '{"files":["/ruta/archivo.pdf"]}') Emular dispositivo: cdp_call("Emulation.setDeviceMetricsOverride", '{"width":375,...}') Interceptar red: cdp_call("Fetch.enable", '{"patterns":[{"urlPattern":"*"}]}') Geolocation: cdp_call("Emulation.setGeolocationOverride", '{"latitude":4.7,...}')
| Name | Required | Description | Default |
|---|---|---|---|
| method | Yes | ||
| params | No | {} | |
| use_session | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
clickC
Clic humano (por coordenadas) sobre el elemento @eN. Devuelve snapshot nuevo.
| Name | Required | Description | Default |
|---|---|---|---|
| ref | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
current_urlA
Devuelve URL y titulo actuales (barato, sin snapshot).
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
fillC
Enfoca @eN, limpia y escribe text. submit=True presiona Enter al final.
| Name | Required | Description | Default |
|---|---|---|---|
| ref | Yes | ||
| text | Yes | ||
| submit | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
get_textA
Devuelve innerText del elemento @eN, o del body completo si ref es None.
| Name | Required | Description | Default |
|---|---|---|---|
| ref | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
hoverA
Mueve el mouse al centro de @eN (dispara menus/tooltips hover).
| Name | Required | Description | Default |
|---|---|---|---|
| ref | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
js_clickB
JS element.click() directo — mas confiable para React SPAs y Angular. Usar cuando click() no dispara el handler (ej: botones con React 17+ delegation).
| Name | Required | Description | Default |
|---|---|---|---|
| ref | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
js_evalA
Ejecuta JavaScript arbitrario en la pagina. Devuelve resultado + snapshot.
ref=None → evalua script como expresion en window context.
ref=@eN → ejecuta script como funcion con el elemento como this.
Ejemplos basicos: scroll abajo: js_eval("window.scrollBy(0, 500)") drag elemento: js_eval("el.dispatchEvent(new DragEvent('dragstart',...))", ref="@e5") leer atributo: js_eval("return this.getAttribute('data-id')", ref="@e12") click forzado: js_eval("this.click()", ref="@e7") esperar async: js_eval("return await fetch('/api').then(r=>r.json())")
RENDIMIENTO — operaciones en lote: Cada tool call = un round-trip LLM. Para crear/editar/extraer N elementos, escribe UN loop async en JS en lugar de llamar N veces a click/fill/js_eval.
Ejemplo — rellenar y enviar un formulario 20 veces en UNA llamada: js_eval(""" (async () => { const users = [ {name:'Ana',user:'ana01',role:'Operador'}, {name:'Luis',user:'luis02',role:'Supervisor'}, ]; const set = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const fire = (el,v) => { set.call(el,v); el.dispatchEvent(new Event('input',{bubbles:true})); }; const results = []; for (const u of users) { document.querySelector('button.agregar, [aria-label*=gregar]').click(); await new Promise(r => setTimeout(r, 400)); const inp = document.querySelectorAll('input:not([type=checkbox])'); fire(inp[0], u.name); fire(inp[1], u.user); document.querySelector('button[type=submit], button.crear').click(); await new Promise(r => setTimeout(r, 300)); results.push(u.user); } return results; })() """)
Para datasets grandes usa js_eval_loop() que inyecta items automaticamente.
| Name | Required | Description | Default |
|---|---|---|---|
| ref | No | ||
| script | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
js_eval_loopA
Ejecuta script una vez por cada item — UNA llamada en lugar de N.
La variable item esta disponible en script como objeto JS plano.
Devuelve array JSON con {ok, result, error} por item + snapshot final.
USAR ESTO en lugar de llamar js_eval/click/fill N veces para operaciones bulk. Cada tool call = un round-trip LLM. Un loop aqui = 50-100x mas rapido para N>5.
Parametros:
items — lista de objetos, uno por iteracion
script — JS a ejecutar por item; puede usar await; item esta en scope
delay_ms — espera entre iteraciones (default 300ms; bajar si la app es rapida)
Ejemplo — crear 20 usuarios en una sola llamada: js_eval_loop( items=[ {"name": "Ana Garcia", "user": "agarcia", "phone": "3101234567", "email": "ana@corp.com", "area": "TI", "role": "Operador"}, ... ], script=""" document.querySelector('button[aria-label*="gregar"], button.agregar').click(); await new Promise(r => setTimeout(r, 400)); const inp = document.querySelectorAll('input:not([type=checkbox]):not([type=radio])'); const s = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; const fire = (el,v) => { s.call(el,v); el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true})); }; fire(inp[0], item.name); fire(inp[1], item.user); fire(inp[2], item.phone); fire(inp[3], item.email); fire(inp[4], item.area); const sel = document.querySelectorAll('select')[0]; const ss = Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,'value').set; ss.call(sel, item.role); sel.dispatchEvent(new Event('change',{bubbles:true})); document.querySelector('button[type=submit], button.crear').click(); return item.user; """ )
| Name | Required | Description | Default |
|---|---|---|---|
| items | Yes | ||
| script | Yes | ||
| delay_ms | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
press_keyB
Presiona una tecla nombrada (Enter, Tab, ArrowDown...). ref opcional para enfocar.
| Name | Required | Description | Default |
|---|---|---|---|
| key | Yes | ||
| ref | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
read_consoleA
Devuelve los mensajes de consola capturados. clear=True vacia el buffer.
| Name | Required | Description | Default |
|---|---|---|---|
| clear | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
read_networkB
Lista requests capturadas (opcional filtra por substring de URL).
| Name | Required | Description | Default |
|---|---|---|---|
| clear | No | ||
| filter | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
screenshotA
Escape hatch: PNG en base64 (usa snapshot de texto por defecto, no esto).
| Name | Required | Description | Default |
|---|---|---|---|
| full_page | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
scrollA
Desplaza la pagina o un elemento especifico.
ref=None → window.scrollBy(x, y) ref=@eN → scrollIntoView del elemento + scrollBy(x, y) relativo y>0 baja, y<0 sube; x>0 derecha, x<0 izquierda.
Ejemplos: scroll() — baja 400px scroll(y=-400) — sube 400px scroll(y=99999) — va al final de la pagina scroll(ref="@e5", y=0) — centra elemento en viewport
| Name | Required | Description | Default |
|---|---|---|---|
| x | No | ||
| y | No | ||
| ref | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
select_optionB
Selecciona una opcion de un por value o por label visible.
| Name | Required | Description | Default |
|---|---|---|---|
| ref | Yes | ||
| label | No | ||
| value | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
set_valueA
Establece el valor de un input/select usando el setter nativo de JS.
Necesario para React, Vue y Angular: los inputs controlados no responden a .value= directo porque el framework sobreescribe el setter. Este tool usa Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set y dispara eventos input+change para que el framework detecte el cambio.
Usar para: date pickers, spinbuttons, selects custom, inputs con validacion.
| Name | Required | Description | Default |
|---|---|---|---|
| ref | Yes | ||
| value | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
snapshotC
Captura el arbol de accesibilidad como texto compacto con refs @eN.
| Name | Required | Description | Default |
|---|---|---|---|
| interactive_only | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
wait_forA
Espera hasta que aparezca text en la pagina (o agota timeout_ms).
| Name | Required | Description | Default |
|---|---|---|---|
| text | No | ||
| timeout_ms | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
21 tool updates
v0.1.0- First observed
browser_start - First observed
browser_stop - First observed
cdp_call - First observed
click - First observed
current_url - First observed
fill - First observed
get_text - First observed
hover - First observed
js_click - First observed
js_eval - First observed
js_eval_loop - First observed
navigate - First observed
press_key - First observed
read_console - First observed
read_network - First observed
screenshot - First observed
scroll - First observed
select_option - First observed
set_value - First observed
snapshot - First observed
wait_for
TDQS
Scored across 21 tools
Most tools have clearly distinct purposes (e.g., click vs js_click vs cdp_call), but the difference between click and js_click might confuse an agent without careful reading. Overall, each tool targets a unique action.
All names use snake_case, but the pattern mixes simple verbs (click, hover, scroll) with verb_noun phrases (get_text, read_console, press_key) and compound noun_verb (js_eval, cdp_call). This inconsistency, while still readable, could be improved.
21 tools is slightly above the typical 3-15 range, but each tool serves a specific need in browser automation (navigation, interaction, JavaScript execution, bulk operations, etc.). The count feels justified for the domain.
Covers essential browser automation operations: navigation, clicking, typing, scrolling, waiting, reading page state, console/network monitoring, and JavaScript execution. Missing a dedicated file upload tool, but can be done via cdp_call. Minor gaps overall.
Maintenance
Related MCP Connectors
Official Browserless MCP server: browser automation, scraping, screenshots and PDFs for AI agents.
Web scraping, browser automation, screenshots and AI data extraction for AI agents via MCP.
Cloud browser, web extraction, screenshots, and remote CDP tools for AI agents.
Give AI agents a real cloud Chrome: open sessions, navigate, read pages, click, type and screenshot.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenancesingle-binary MCP server that gives AI agents a browser. 66 tools for navigation, form filling, data extraction, screenshots, and DOM diffing — built on pure Chrome DevTools Protocol.14MIT
- AlicenseAqualityDmaintenanceSelf-hosted MCP server for AI browser automation. Connects to your own Chromium instance via CDP, providing tools for browser control, navigation, interaction, and content extraction.191MIT
- AlicenseBqualityBmaintenanceA lightweight 30KB MCP browser automation server that uses raw Chrome DevTools Protocol to enable AI agents to browse the web, take screenshots, interact with elements, and capture live page events like console logs and network requests.2648 npm19MIT

kitewrightofficial
AlicenseNot gradedqualityBmaintenanceA lightweight MCP server for browser automation that gives AI agents navigate, screenshot, and extract tools via a single small binary, without needing Node.js or Playwright. It provides CDP-based browser control with efficient startup and resource usage.24MIT