fed245a738
Capacidades nuevas y cambios (40 -> 42 tools): - page_perceive ahora se genera de forma NATIVA en Go sobre la conexion CDP viva del pool (cdp_get_ax_outline_go_browser). Elimina el subprocess `fn run cdp_perceive_outline` (Python), el venv y la dependencia del binario `fn` en runtime (se borra resolveRoot/exec.Command). Respeta tab_select. - page_perceive acepta frame_id para percibir DENTRO de un iframe. El campo tab_id queda obsoleto (se ignora; usar tab_select) pero se conserva por compatibilidad. - frame_get_text (nueva, lectura): innerText de un iframe via cdp_get_text_in_frame_go_browser. Activa tambien bajo --read-only. - dom_click_xy (nueva, MUTA): click humanizado por coordenadas absolutas via cdp_click_xy_human_go_browser, con mode human/fast/instant y auto-observe. Fallback para actuar sobre lo que el LLM ve en page_screenshot. - page_screenshot devuelve la imagen como image content (cdp_screenshot_bytes_go_browser + mcp.NewToolResultImage) para que el LLM vea los pixeles; path pasa a ser opcional (si se da, ademas guarda a disco). - Auto-observe de las tools *_ref sube su truncado de 4000 a 8000 chars. - Fix de seguridad documental: todas las descripciones del parametro port que decian "Default 9222" (navegador diario del usuario) corregidas a "Default 9333" (Chrome aislado del MCP). El codigo ya usaba 9333; la doc era falsa y podia inducir al modelo a tocar pestanas de banca/correo. uses_functions del app.md: +cdp_get_ax_outline, +cdp_get_text_in_frame, +cdp_screenshot_bytes; -cdp_perceive_outline_py_pipelines. Verificacion: go build OK, go test OK (4 unit pass, 3 e2e skip gated BMCP_E2E=1), go vet OK, gofmt limpio, sin "Default 9222" en el codigo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
303 lines
18 KiB
Markdown
303 lines
18 KiB
Markdown
---
|
|
name: browser_mcp
|
|
lang: go
|
|
domain: infra
|
|
version: 0.6.0
|
|
description: "Servidor MCP que expone control total del navegador via CDP (42 tools: navegación, DOM, cookies, iframes, teclado/scroll, diálogos, estado de sesión, selección determinista de pestaña, lectura compacta texto/AX nativa + bucle percibir→actuar por #ref con auto-observe, percepción y lectura de texto dentro de iframes, click por coordenadas, y screenshot devuelto como image content que el LLM ve) reusando funciones del dominio browser del registry con un pool de conexiones CDP vivas. Por defecto opera sobre un Chrome aislado (puerto 9333) separado del navegador diario."
|
|
tags: [mcp, browser, cdp, automation, scraping]
|
|
e2e_checks:
|
|
- id: build
|
|
cmd: "cd projects/web_scraping/apps/browser_mcp && go build -o browser_mcp ."
|
|
timeout_s: 120
|
|
- id: unit
|
|
cmd: "cd projects/web_scraping/apps/browser_mcp && go test -count=1 ./..."
|
|
timeout_s: 120
|
|
- id: leak_no_orphans
|
|
cmd: "cd projects/web_scraping/apps/browser_mcp && go test -c -o /tmp/bmcp_e2e.test . && systemd-run --user --quiet --collect --unit=bmcp_e2e_ci --wait -p Type=oneshot --setenv=BMCP_E2E=1 -p StandardOutput=journal /tmp/bmcp_e2e.test -test.run TestE2E -test.v"
|
|
timeout_s: 180
|
|
severity: warning
|
|
uses_functions:
|
|
- chrome_launch_go_browser
|
|
- cdp_connect_go_browser
|
|
- cdp_close_go_browser
|
|
- cdp_navigate_go_browser
|
|
- cdp_list_tabs_go_browser
|
|
- cdp_new_tab_go_browser
|
|
- cdp_close_tab_go_browser
|
|
- cdp_activate_tab_go_browser
|
|
- cdp_nav_back_go_browser
|
|
- cdp_nav_forward_go_browser
|
|
- cdp_wait_load_go_browser
|
|
- cdp_wait_idle_go_browser
|
|
- cdp_get_html_go_browser
|
|
- cdp_evaluate_go_browser
|
|
- cdp_screenshot_go_browser
|
|
- cdp_click_go_browser
|
|
- cdp_click_human_go_browser
|
|
- cdp_click_text_go_browser
|
|
- cdp_type_text_go_browser
|
|
- cdp_find_by_text_go_browser
|
|
- cdp_find_ref_by_text_go_browser
|
|
- cdp_wait_element_go_browser
|
|
- cdp_press_key_go_browser
|
|
- cdp_scroll_go_browser
|
|
- cdp_handle_dialog_go_browser
|
|
- cdp_set_cookie_go_browser
|
|
- cdp_get_cookies_go_browser
|
|
- cdp_delete_cookies_go_browser
|
|
- cdp_clear_cookies_go_browser
|
|
- cdp_list_frames_go_browser
|
|
- cdp_eval_in_frame_go_browser
|
|
- cdp_get_frame_html_go_browser
|
|
- cdp_save_storage_state_go_browser
|
|
- cdp_load_storage_state_go_browser
|
|
- cdp_get_text_go_browser
|
|
- cdp_get_text_in_frame_go_browser
|
|
- cdp_connect_target_go_browser
|
|
- cdp_get_ax_outline_go_browser
|
|
- cdp_screenshot_bytes_go_browser
|
|
- cdp_click_ref_go_browser
|
|
- cdp_type_ref_go_browser
|
|
- cdp_hover_ref_go_browser
|
|
- cdp_click_xy_human_go_browser
|
|
uses_types: []
|
|
framework: ""
|
|
entry_point: "main.go"
|
|
dir_path: "projects/web_scraping/apps/browser_mcp"
|
|
repo_url: ""
|
|
---
|
|
|
|
# browser_mcp
|
|
|
|
Servidor MCP (Model Context Protocol) en Go que expone el control de navegador via CDP
|
|
del registry `fn_registry` como tools MCP. Cualquier cliente MCP (Claude Code, otros
|
|
agentes) puede manejar un Chrome/Chromium vivo: navegar, leer el DOM, hacer clicks,
|
|
gestionar cookies, evaluar JavaScript, operar iframes y persistir/restaurar sesiones.
|
|
|
|
Clona el patrón de `apps/registry_mcp/` (librería `github.com/mark3labs/mcp-go` v0.52.0,
|
|
`server.NewMCPServer` + `server.ServeStdio`, tools con `mcp.NewTool` + handlers tipados
|
|
via `mcp.NewTypedToolHandler`, transporte stdio por defecto + HTTP opcional con `--http`,
|
|
slog a stderr porque stdout pertenece al JSON-RPC).
|
|
|
|
## Arquitectura: pool de conexiones CDP
|
|
|
|
A diferencia de `registry_mcp` (que abre la DB una vez), `browser_mcp` mantiene un
|
|
**pool de conexiones CDP vivas** indexado por puerto (`pool.go`). Razón:
|
|
`browser.CdpConnect(port)` hace un handshake WebSocket contra una tab "page" de Chrome
|
|
(~50-200ms) y esa conexión ES una sesión viva (soporta `Page.*`, `Runtime.*`, `Input.*`).
|
|
El agente llama muchas tools seguidas (navigate → wait → click → eval); reconectar en
|
|
cada tool pagaría el handshake repetidamente y perdería estado entre tools (los event
|
|
handlers persistentes, como el de `handle_dialog`, viven mientras la conexión esté viva).
|
|
Por eso reusamos la conexión por puerto.
|
|
|
|
- `connPool.get(port)` devuelve la conexión cacheada o abre una nueva.
|
|
- `connPool.drop(port)` cancela el handler de diálogo (si lo hay) y cierra la conexión.
|
|
- `connPool.connectTarget(port, match)` descarta la conexión actual y reconecta a un target
|
|
determinista (por id o substring de URL). Es lo que usa `tab_select` para fijar la pestaña.
|
|
- `connPool.setCancel(port, cancel)` registra el cancel del auto-handler de `handle_dialog`.
|
|
- `connPool.closeAll()` se ejecuta con `defer` en `main()`.
|
|
- `deps.withConn(port, fn)` ejecuta `fn` con la conexión del pool y, si el error indica
|
|
conexión muerta (`isConnErr`: connection close, broken pipe, use of closed, ws read, EOF),
|
|
descarta la conexión y reintenta UNA vez (Chrome pudo cerrar la tab entre tools).
|
|
|
|
Toda tool con argumento `port` usa `portOr(a.Port)` (default 9333). Las tools de tabs
|
|
(`tab_list`, `tab_new`, `tab_close`, `tab_activate`, `tab_select`) usan el endpoint HTTP `/json`
|
|
de CDP directamente (host `localhost`), no el pool, porque no requieren una sesión WebSocket viva.
|
|
|
|
## Seguridad: Chrome aislado por defecto (puerto 9333)
|
|
|
|
**El default del MCP es operar sobre su PROPIO Chrome aislado, no sobre el navegador diario.**
|
|
|
|
En este ecosistema el chromium diario del usuario tiene CDP habilitado globalmente en el
|
|
puerto **9222** (via `/etc/chromium.d/cdp`). Si el MCP usara 9222 por defecto, el agente
|
|
podría manipular pestañas ajenas del usuario (banca, correo). Para evitarlo:
|
|
|
|
- `portOr` devuelve **9333** por defecto (no 9222) — el Chrome dedicado del MCP.
|
|
- `browser_launch` sin `user_data_dir` usa un perfil DEDICADO y aislado:
|
|
`<tmp>/browser_mcp_userdata` (se crea si hace falta) en el puerto 9333.
|
|
- Para adjuntarte deliberadamente al navegador diario, pasa `port: 9222` explícito en cada
|
|
tool. Hazlo solo con cuidado.
|
|
|
|
## Tools (42)
|
|
|
|
### Sesión (`tools_session.go`)
|
|
- `browser_launch` (MUTA) — lanza Chrome con CDP. args: port, headless, user_data_dir, url.
|
|
- `browser_connect` — abre/poolea la conexión CDP del puerto. args: port.
|
|
- `browser_disconnect` — cierra y descarta la conexión del puerto (no mata Chrome). args: port.
|
|
|
|
### Navegación + tabs (`tools_nav.go`)
|
|
- `tab_navigate` (MUTA) — `Page.navigate`. args: port, url.
|
|
- `tab_list` — lista targets via `GET /json`. args: port.
|
|
- `tab_new` (MUTA) — abre tab via `PUT /json/new`. args: port, url.
|
|
- `tab_close` (MUTA) — cierra tab por ID. args: port, tab_id.
|
|
- `tab_activate` — pone tab en foreground. args: port, tab_id.
|
|
- `tab_select` — fija la pestaña sobre la que operan las siguientes tools, eligiéndola por id
|
|
o por substring de su URL (determinista). Usar tras `tab_list` para no operar sobre la
|
|
pestaña equivocada. args: port, match.
|
|
- `nav_back` (MUTA) — atrás en el historial. args: port.
|
|
- `nav_forward` (MUTA) — adelante en el historial. args: port.
|
|
- `page_wait_load` — espera el evento load. args: port, timeout_ms (default 10000).
|
|
- `page_wait_idle` — espera red idle. args: port, timeout_ms (default 15000).
|
|
|
|
### Lectura (`tools_read.go`)
|
|
- `page_get_html` — HTML serializado (truncado a 200000 chars). args: port.
|
|
- `page_get_text` — texto visible (innerText) de la página o de un elemento (selector CSS),
|
|
truncado a `max_bytes`. Preferir sobre `page_get_html` cuando solo necesitas leer contenido
|
|
— no revienta el contexto. args: port, selector (opcional), max_bytes (default 20000).
|
|
- `page_perceive` — outline indentado y accionable del árbol de accesibilidad (roles, nombres,
|
|
`#ref`): la forma compacta de que el agente "perciba" la página sin reventar el contexto.
|
|
**Nativo en Go** sobre la conexión CDP viva del pool (`cdp_get_ax_outline_go_browser`) — ya
|
|
no lanza subprocess `fn run` ni levanta el venv de Python. Para elegir la pestaña usa `tab_select`
|
|
ANTES (la conexión del pool ya está fijada a esa pestaña); el campo `tab_id` queda obsoleto y se
|
|
ignora (se conserva por compatibilidad). Si se pasa `frame_id`, percibe DENTRO de ese iframe
|
|
(obtén el id con `frame_list`). args: port, tab_id (obsoleto), frame_id (opcional), max_chars (default 20000).
|
|
- `page_eval_js` (MUTA) — `Runtime.evaluate`. args: port, expression.
|
|
- `page_screenshot` — captura la página y la **devuelve como image content** para que el LLM vea los
|
|
píxeles (vía `cdp_screenshot_bytes_go_browser`, sin tocar disco). Si se pasa `path`, además guarda la
|
|
imagen en ese archivo; el image content se devuelve siempre. Útil cuando el outline de `page_perceive`
|
|
no basta (canvas, mapas, layouts visuales): mira la captura y actúa con `dom_click_xy`. args: port,
|
|
path (opcional), full_page.
|
|
|
|
### DOM (`tools_dom.go`)
|
|
- `dom_click` (MUTA) — click por selector. args: port, selector.
|
|
- `dom_click_human` (MUTA) — click con movimiento humano. args: port, selector.
|
|
- `dom_click_text` (MUTA) — click sobre el primer elemento con ese texto. args: port, text.
|
|
- `dom_type` (MUTA) — escribe texto en el elemento enfocado. args: port, text.
|
|
- `dom_find_by_text` — devuelve un selector CSS único para un texto visible. args: port, text.
|
|
- `dom_wait_element` — espera a que aparezca un selector. args: port, selector, timeout_ms (default 10000).
|
|
- `dom_click_ref` (MUTA) — click humanizado por `#ref` (backendDOMNodeId del outline de `page_perceive`) + auto-observe. args: port, ref, mode.
|
|
- `dom_type_ref` (MUTA) — enfoca el `#ref` y escribe texto + auto-observe. args: port, ref, text.
|
|
- `dom_hover_ref` (MUTA) — hover humanizado por `#ref` + auto-observe. args: port, ref, mode.
|
|
- `dom_click_xy` (MUTA) — fallback de click por coordenadas absolutas (x, y en CSS pixels del viewport) con
|
|
movimiento humanizado por defecto. Pensado para usarse sobre lo que el agente VE en `page_screenshot`
|
|
cuando el outline no basta (canvas, mapas, layouts visuales); prefiere `dom_click_ref` cuando el elemento
|
|
aparece en el outline. Devuelve el outline actualizado (auto-observe). args: port, x, y, mode.
|
|
|
|
#### Bucle percibir→actuar (por `#ref`)
|
|
|
|
`page_perceive` devuelve un outline accionable (generado de forma nativa en Go
|
|
sobre la conexión CDP viva) donde cada elemento lleva un `#ref` estable (su
|
|
`backendDOMNodeId`). Las tools `dom_click_ref` / `dom_type_ref` / `dom_hover_ref`
|
|
actúan directamente sobre ese `#ref` — no necesitas resolver un selector CSS.
|
|
Tras la acción esperan un settle breve (400ms) y **devuelven el outline
|
|
actualizado** (auto-observe, truncado a 8000 chars), cerrando el bucle
|
|
percibir→actuar:
|
|
|
|
```
|
|
page_perceive → outline con #ref de cada elemento
|
|
dom_click_ref → click humanizado + outline nuevo tras la acción
|
|
dom_type_ref → escribe + outline nuevo
|
|
```
|
|
|
|
Cuando el elemento no aparece en el outline (canvas, mapas, layouts puramente
|
|
visuales), el fallback es **mirar** con `page_screenshot` (que devuelve la imagen
|
|
al LLM) y **actuar** por coordenadas con `dom_click_xy`, que también devuelve el
|
|
outline tras el click.
|
|
|
|
Las tools `*_ref` y `dom_click_xy` aceptan `mode` (`human` por defecto con
|
|
Bézier+jitter anti-bot, `fast` para scraping masivo, `instant` sin movimiento de
|
|
ratón). La humanización es el default en todas para no facilitar la detección.
|
|
|
|
### Input (`tools_input.go`) — todas MUTA
|
|
- `press_key` — presiona una tecla nombrada (Enter/Tab/Escape/ArrowDown/...). args: port, key.
|
|
- `scroll` — scroll por (delta_x, delta_y). args: port, delta_x (default 0), delta_y (default 300).
|
|
- `handle_dialog` — arma un auto-handler de diálogos JS (vive en la conexión del pool). args: port, accept (default true), prompt_text.
|
|
|
|
### Cookies (`tools_cookies.go`)
|
|
- `cookie_get` — todas las cookies como JSON. args: port.
|
|
- `cookie_set` (MUTA) — set cookie. args: port, name, value, domain, path, http_only.
|
|
- `cookie_delete` (MUTA) — borra cookies por nombre. args: port, name, domain.
|
|
- `cookie_clear` (MUTA) — borra todas las cookies. args: port.
|
|
|
|
### Iframes (`tools_frames.go`)
|
|
- `frame_list` — lista frames con sus IDs. args: port.
|
|
- `frame_eval` (MUTA) — evalúa JS dentro de un frame. args: port, frame_id, expression.
|
|
- `frame_get_html` — HTML de un frame (truncado a 200000). args: port, frame_id.
|
|
- `frame_get_text` — texto visible (innerText) de un iframe, truncado a `max_bytes`. Para leer
|
|
contenido atrapado dentro de un iframe — `page_get_text` solo cubre el documento de nivel superior.
|
|
args: port, frame_id, max_bytes (default 20000).
|
|
|
|
### Estado de sesión (`tools_storage.go`)
|
|
- `storage_save` — guarda cookies + localStorage a JSON. args: port, path.
|
|
- `storage_load` (MUTA) — carga cookies + localStorage desde JSON. args: port, path.
|
|
|
|
## Cómo lanzarlo
|
|
|
|
Transporte stdio (default, para clientes MCP):
|
|
|
|
```bash
|
|
cd projects/web_scraping/apps/browser_mcp
|
|
go build -o browser_mcp .
|
|
./browser_mcp
|
|
```
|
|
|
|
Transporte HTTP (Streamable HTTP):
|
|
|
|
```bash
|
|
./browser_mcp --http :7740 # bind 127.0.0.1:7740
|
|
./browser_mcp --http :7740 --bind 0.0.0.0 # requiere REGISTRY_API_TOKEN (bearer auth)
|
|
```
|
|
|
|
### Flag `--read-only`
|
|
|
|
Con `--read-only`, el servidor NO registra las tools mutantes (marcadas MUTA arriba):
|
|
solo expone las 19 tools de lectura/control (`browser_connect`, `browser_disconnect`, `tab_list`,
|
|
`tab_activate`, `tab_select`, `page_wait_load`, `page_wait_idle`, `page_get_html`, `page_get_text`,
|
|
`page_perceive`, `page_screenshot`, `dom_find_by_text`, `dom_find_ref_by_text`, `dom_wait_element`,
|
|
`cookie_get`, `frame_list`, `frame_get_html`, `frame_get_text`, `storage_save`). Útil para sesiones
|
|
de inspección sin riesgo de modificar el estado del navegador.
|
|
|
|
## Omitido en v1
|
|
|
|
Funciones del dominio `browser` que NO se exponen como tools en esta versión, con su razón:
|
|
|
|
- **`cdp_har_record_go_browser`** — graba el tráfico de red (HAR). Requiere un callback de
|
|
larga duración (registrar handlers + un punto de "stop" que devuelve los datos
|
|
acumulados); no encaja en el modelo request/response de una tool MCP simple. Pendiente
|
|
de un diseño con tool de start + tool de stop.
|
|
- **`cdp_get_ax_tree`** — expuesto via la tool `page_perceive`. Desde v0.6.0 el outline se genera
|
|
de forma **nativa en Go** (`cdp_get_ax_outline_go_browser`) sobre la conexión CDP viva del pool;
|
|
ya no se invoca el pipeline Python `cdp_perceive_outline` por subprocess (`fn run`). El acceso al
|
|
árbol AX en bruto sigue sin exponerse: la tool devuelve directamente el outline accionable.
|
|
- **Funciones de perfiles Chrome (Bash: create/delete/appearance/reset)** — requieren que
|
|
Chrome esté CERRADO para modificar el `Local State` / `Preferences` del perfil; son
|
|
incompatibles con un MCP cuyo propósito es controlar un Chrome vivo. Quedan disponibles
|
|
como `fn run` aparte.
|
|
|
|
## Capability growth log
|
|
|
|
- v0.6.0 (2026-06-06) — Percepción visual y de iframes + perceive nativo. (1) `page_perceive` se
|
|
generó hasta ahora por subprocess `fn run cdp_perceive_outline` (Python); ahora es **nativo en Go**
|
|
sobre la conexión CDP viva del pool (`cdp_get_ax_outline_go_browser`) — mata el subprocess, el venv
|
|
y la dependencia del binario `fn` en runtime (se eliminó `resolveRoot`/`exec.Command`). (2) Acceso a
|
|
datos dentro de iframes: nueva tool `frame_get_text` (innerText de un iframe, `cdp_get_text_in_frame_go_browser`)
|
|
y nuevo parámetro `frame_id` en `page_perceive` para percibir DENTRO de un iframe. (3) Click por
|
|
coordenadas absolutas: nueva tool `dom_click_xy` (`cdp_click_xy_human_go_browser`), humanizada por
|
|
defecto, pensada para actuar sobre lo que el LLM ve en una captura. (4) `page_screenshot` ahora
|
|
**devuelve la imagen como image content** (vía `cdp_screenshot_bytes_go_browser` + `mcp.NewToolResultImage`)
|
|
para que el LLM vea los píxeles; `path` pasa a ser opcional (si se da, además guarda a disco). (5) El
|
|
auto-observe de las tools `*_ref` subió su truncado de 4000 a 8000 chars (outlines grandes se cortaban).
|
|
(6) Fix de seguridad documental: todas las descripciones del parámetro `port` que decían "Default 9222"
|
|
(el navegador diario del usuario) corregidas a "Default 9333" (Chrome aislado del MCP); el código ya
|
|
usaba 9333, la doc era falsa y podía inducir al modelo a tocar pestañas de banca/correo. 40 → 42 tools.
|
|
- v0.5.0 (2026-06-06) — Fix del leak de RAM (chromium huérfanos, apagón 06/06/2026). El pool
|
|
ahora registra el PID del Chrome que lanzó por puerto (`pids` map + setPID/getPID/clearPID/
|
|
launchedCount). `browser_disconnect` (drop) y el shutdown (closeAll) matan el grupo de proceso
|
|
completo SOLO si el PID está registrado (lo lanzó el MCP) — un Chrome externo (navegador diario
|
|
en 9222) nunca se mata, solo se cierra el WebSocket. `browser_launch` es idempotente por puerto,
|
|
reusa un Chrome ya vivo (`ChromeLaunch.ReuseExisting`, pid 0 = no relanza) y aplica un tope duro
|
|
de 4 instancias. Handler SIGTERM/SIGINT en main.go llama closeAll (los defers no corren con
|
|
señal). `withConn` retry usa `releaseConn` (suelta solo el WS) en vez de drop. Tests: pool_test.go
|
|
(lógicos) + pool_e2e_test.go (Chrome real, gate BMCP_E2E=1). e2e_checks añadidos.
|
|
- v0.3.0 (2026-06-06) — Cierre del bucle percibir→actuar. Nuevas tools `dom_click_ref`,
|
|
`dom_type_ref`, `dom_hover_ref`: actúan sobre el `#ref` (backendDOMNodeId estable) del
|
|
outline de `page_perceive` con humanización por defecto (Bézier+jitter) y auto-observe
|
|
(devuelven el outline actualizado tras la acción). Refactor: la generación del outline
|
|
se extrajo a `deps.perceiveOutline`/`perceiveOutlineTab`, reusado por `page_perceive` y
|
|
por las tools `*_ref`. 36 → 39 tools.
|
|
- v0.2.0 (2026-06-06) — P0 LLM-readiness. Seguridad: Chrome aislado por defecto (puerto 9333
|
|
+ perfil dedicado `<tmp>/browser_mcp_userdata`), separado del navegador diario en 9222.
|
|
Nuevas tools: `tab_select` (selección determinista de pestaña por id/URL), `page_get_text`
|
|
(lectura compacta de innerText), `page_perceive` (outline AX via `fn run cdp_perceive_outline`).
|
|
33 → 36 tools.
|