Skip to main content
Glama
Browser-MCP Navigator

Browser-MCP Navigator

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_mcp internally.

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──►  Chrome

Related MCP server: Browser Jet Pilot

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 -d

Server 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 mode

Connecting 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 http://host.docker.internal:3000 instead of localhost:3000

ngrok

ngrok http 3000 → gives a public URL the container can reach

Local mode

(stdio) Chrome runs on your machine — localhost works normally


Available Tools

Tool

Description

browser_start

Launch / reconnect Chrome

browser_stop

Close Chrome

navigate

Go to URL, wait for load / networkidle

snapshot

Accessibility tree as compact text with @eN refs

click

Human-like click by coordinates

js_click

element.click() — reliable for React / Angular SPAs

fill

Clear + type text in an input

press_key

Key press (Enter, Tab, Escape, ArrowDown…)

select_option

Select native <select> by value or label

hover

Mouse hover (menus, tooltips)

set_value

React/Vue/Angular-safe input setter via native JS

scroll

Scroll page or element into view

js_eval

Run any JavaScript — drag, events, async fetch, bulk loops

js_eval_loop

Bulk operations — run a JS snippet once per item

cdp_call

Raw CDP protocol — file upload, device emulation, network intercept

get_text

innerText of element or full page

wait_for

Wait until text appears on page

read_console

JS console logs (filter by level/since_ms)

read_network

Network requests / responses

screenshot

PNG base64 (escape hatch)

current_url

Current URL + title

inject_persistent

Register a JS helper that survives navigate() (a full reload otherwise wipes window)

list_persistent

List identifiers of registered persistent scripts

remove_persistent

Remove a persistent script by identifier

click_popup_option

Resolve the currently-open dropdown/menu/listbox and click the matching option, in one call

act_and_observe

Act, then watch the DOM/console for watch_ms — one call instead of racing a toast's lifetime

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

MCP_TRANSPORT

stdio

stdio (local) or sse (Docker/remote)

MCP_HOST

0.0.0.0

SSE bind address

MCP_PORT

3067

SSE port

MCP_API_KEY

(empty)

API key header; empty = no auth

CHROME_PATH

auto

Explicit path to chrome.exe

CHROME_EXTRA_ARGS

(empty)

Extra Chrome flags

Fast-Browser-MCP_HEADLESS

0

1 for headless mode

Fast-Browser-MCP_HUMAN_DELAYS

1

0 removes human-like delays (faster)


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, reattach

Why 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.

  • @eN refs = stable backendNodeId — no DOM re-query per action.

Available Tools

21 tools
browser_startB

Lanza (o reconecta) Chrome con CDP. Debe llamarse antes que las demas tools.

NameRequiredDescriptionDefault
headlessNo

Output Schema

NameRequiredDescription
resultYes
browser_stopB

Cierra la conexion CDP. kill=True ademas termina el proceso Chrome.

NameRequiredDescriptionDefault
killNo

Output Schema

NameRequiredDescription
resultYes
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,...}')

NameRequiredDescriptionDefault
methodYes
paramsNo{}
use_sessionNo

Output Schema

NameRequiredDescription
resultYes
clickC

Clic humano (por coordenadas) sobre el elemento @eN. Devuelve snapshot nuevo.

NameRequiredDescriptionDefault
refYes

Output Schema

NameRequiredDescription
resultYes
current_urlA

Devuelve URL y titulo actuales (barato, sin snapshot).

NameRequiredDescriptionDefault

No parameters

Output Schema

NameRequiredDescription
resultYes
fillC

Enfoca @eN, limpia y escribe text. submit=True presiona Enter al final.

NameRequiredDescriptionDefault
refYes
textYes
submitNo

Output Schema

NameRequiredDescription
resultYes
get_textA

Devuelve innerText del elemento @eN, o del body completo si ref es None.

NameRequiredDescriptionDefault
refNo

Output Schema

NameRequiredDescription
resultYes
hoverA

Mueve el mouse al centro de @eN (dispara menus/tooltips hover).

NameRequiredDescriptionDefault
refYes

Output Schema

NameRequiredDescription
resultYes
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).

NameRequiredDescriptionDefault
refYes

Output Schema

NameRequiredDescription
resultYes
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.

NameRequiredDescriptionDefault
refNo
scriptYes

Output Schema

NameRequiredDescription
resultYes
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; """ )

NameRequiredDescriptionDefault
itemsYes
scriptYes
delay_msNo

Output Schema

NameRequiredDescription
resultYes
press_keyB

Presiona una tecla nombrada (Enter, Tab, ArrowDown...). ref opcional para enfocar.

NameRequiredDescriptionDefault
keyYes
refNo

Output Schema

NameRequiredDescription
resultYes
read_consoleA

Devuelve los mensajes de consola capturados. clear=True vacia el buffer.

NameRequiredDescriptionDefault
clearNo

Output Schema

NameRequiredDescription
resultYes
read_networkB

Lista requests capturadas (opcional filtra por substring de URL).

NameRequiredDescriptionDefault
clearNo
filterNo

Output Schema

NameRequiredDescription
resultYes
screenshotA

Escape hatch: PNG en base64 (usa snapshot de texto por defecto, no esto).

NameRequiredDescriptionDefault
full_pageNo

Output Schema

NameRequiredDescription
resultYes
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

NameRequiredDescriptionDefault
xNo
yNo
refNo

Output Schema

NameRequiredDescription
resultYes
select_optionB

Selecciona una opcion de un por value o por label visible.

NameRequiredDescriptionDefault
refYes
labelNo
valueNo

Output Schema

NameRequiredDescription
resultYes
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.

NameRequiredDescriptionDefault
refYes
valueYes

Output Schema

NameRequiredDescription
resultYes
snapshotC

Captura el arbol de accesibilidad como texto compacto con refs @eN.

NameRequiredDescriptionDefault
interactive_onlyNo

Output Schema

NameRequiredDescription
resultYes
wait_forA

Espera hasta que aparezca text en la pagina (o agota timeout_ms).

NameRequiredDescriptionDefault
textNo
timeout_msNo

Output Schema

NameRequiredDescription
resultYes

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 21 tool updatesv0.1.0
    • First observedbrowser_start
    • First observedbrowser_stop
    • First observedcdp_call
    • First observedclick
    • First observedcurrent_url
    • First observedfill
    • First observedget_text
    • First observedhover
    • First observedjs_click
    • First observedjs_eval
    • First observedjs_eval_loop
    • First observednavigate
    • First observedpress_key
    • First observedread_console
    • First observedread_network
    • First observedscreenshot
    • First observedscroll
    • First observedselect_option
    • First observedset_value
    • First observedsnapshot
    • First observedwait_for

TDQS

B3.3/5.0

Scored across 21 tools

Disambiguation4/5

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.

Naming Consistency3/5

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.

Tool Count4/5

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.

Completeness4/5

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

ActivityStale
ResponsivenessNo recent community issues

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    single-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.
    14
    MIT
  • A
    license
    B
    quality
    B
    maintenance
    A 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.
    26
    48 npm
    19
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    A 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.
    24
    MIT