- CAPABILITIES_TODO.md - demo_e2e/RESUMEN.md - demo_e2e/results/prueba_1_quotes.json - demo_e2e/results/prueba_2_perceive.json - demo_e2e/results/prueba_3_search.json - demo_e2e/results/prueba_4_login_session.json - demo_e2e/results/prueba_5_books.json - demo_e2e/results/prueba_6_session_storage.json - demo_e2e/results/prueba_7_find_honesto.json - demo_e2e/results/prueba_8_verificacion.json - ... Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.9 KiB
Ejercicio e2e — validación de capacidades del browser_mcp
Fecha: 06/06/2026. Objetivo: comprobar, ejecutando de verdad contra sitios reales, que el servidor
browser_mcp (control de navegador vía CDP) puede hacer tareas distintas de recopilación de datos —
de simples a complejas. No "compila", sino "funciona".
Montaje
- Servidor:
projects/web_scraping/apps/browser_mcp/browser_mcp(36 tools, pool de conexiones). - Navegador: Chrome/Chromium 148 aislado en el puerto CDP 9333 (no el 9222 del navegador diario),
user-data-dirdedicado. Lanzado headless para la batería y luego sin headless (ventana visible) para inspección humana — ambos con 5/5. - Cliente:
mcp_client.py— cliente JSON-RPC stdio secuencial (espera la respuesta de cada tool antes de mandar la siguiente, como hace un cliente MCP real). Un primer intento mandando todos los mensajes de golpe falló por una race: el servidor procesa requests de forma concurrente. - Runner:
run_demo.py— ejecuta las 5 pruebas, guarda pasos/respuestas/veredicto enresults/.
Resultado: 9/9 PASS (headless y con ventana visible)
Las pruebas 1-5 son la batería inicial; 6-9 se añadieron para validar las tandas de fixes (A/D/E, B, y gap #1).
| # | Prueba | Sitio sandbox | Capacidades ejercitadas | Resultado |
|---|---|---|---|---|
| 1 | Extraer citas estructuradas | quotes.toscrape.com | navigate, wait_load, eval → JSON real | PASS — 10 citas {text,author,tags} |
| 2 | Percibir página como agente | the-internet.herokuapp.com | page_perceive (AX outline) | PASS — outline 4021 chars con #ref accionables |
| 3 | Submit de formulario con teclado | the-internet.herokuapp.com/login | dom_click, dom_type, press_key Enter, wait_element | PASS — "You logged into a secure area!" |
| 4 | Login + sesión persistente | the-internet.herokuapp.com | form, storage_save/load, cookie_clear | PASS — sesión restaurada sin re-login |
| 5 | Scraping paginado + dedup | books.toscrape.com | navegación multi-página, eval, composición | PASS — 60 libros únicos (3 páginas) |
| 6 | sessionStorage en storage_state | the-internet.herokuapp.com | set→save→clear→load→get | PASS — clear=null, restore=demo_v (fix D) |
| 7 | find_by_text honesto |
quotes.toscrape.com | dom_find_by_text presente vs inexistente | PASS — texto inexistente devuelve error, no vacío (fix E) |
| 8 | Verificación post-acción | quotes.toscrape.com | dom_click sobre oculto / dom_type sin foco | PASS — ambos devuelven error en vez de actuar al vacío (fix B) |
| 9 | Bucle percibir→actuar por #ref |
the-internet.herokuapp.com/login | page_perceive → dom_type_ref/dom_click_ref + auto-observe | PASS — login solo por #ref (sin selector); cada acción devuelve el outline nuevo (gap #1) |
Cobertura conjunta: lanzar/atar navegador, navegar, esperar carga, evaluar JS con JSON real, percibir (AX outline), leer texto, teclado, formularios, cookies, estado de sesión, y composición multi-página.
Valor del ejercicio: 3 bugs reales encontrados y arreglados
Ejecutar de verdad reveló defectos que "compila" jamás habría detectado:
-
page_perceiveroto (bug de integración del MCP). Invocabafn run cdp_perceive_outline --debug-port 9333 ...con flags, perofn runpasa los argumentos posicionalmente a la función del pipeline →int('--debug-port')reventaba. Toda la percepción AX caía. Fix: argumentos posicionales en el orden de la firma. (tools_read.go) -
cdp_save_storage_stateguardaba cookies de todos los dominios. UsabaNetwork.getAllCookies(global), así que elstorage_statearrastraba cookies de sitios visitados antes en la misma sesión (en la prueba, cookies de Wikipedia contaminaban la sesión de the-internet). Fix: filtrar por el host actual (location.hostname). (cdp_save_storage_state.go) -
cdp_load_storage_stateno restauraba la sesión httpOnly.Network.setCookiesno aplicaba de forma fiable la cookie de sesión (rack.session, httpOnly) porque a cada cookie le faltaba el campourl. Fix: sintetizarurlpor cookie a partir dedomain/secure/path. Con esto el login persistente (la pieza estrella) funciona de verdad. (cdp_load_storage_state.go)
Segunda tanda de fixes (A + D + E) — a partir del análisis de deuda
Tras la primera batería se atacaron tres deudas, cada una validada:
- A — Aislamiento robusto del navegador del agente (
chrome_launch.go). El wrapper del sistema/etc/chromium.d/cdpinyecta--user-data-dir/--remote-debugging-portglobales a todo chromium; el aislamiento dependía de que nuestros flags fueran al final (Chrome usa el último duplicado). Fix:findChromeprefiere el binario real (/usr/lib/chromium/chromium), que al ejecutarse directo no pasa por el wrapper y por tanto no hereda esos flags. Validado por construcción (el binario existe y va primero;browser_launchdevuelve PID correcto); la inspección observable del cmdline choca con el exit-144 del entorno de pruebas al lanzar chromium desde el harness, no con el fix. - D —
sessionStorageenstorage_state(cdp_save_storage_state.go,cdp_load_storage_state.go). Antes solo cookies + localStorage. Ahora también el "cajón temporal". Validado por la prueba 6. - E —
cdp_find_by_texthonesto (cdp_find_by_text.go). Antes devolvía("", nil)cuando no encontraba (el caller creía que había encontrado algo). Ahora devuelve error explícito. Validado por la prueba 7.
Gap #1 — bucle percibir→actuar por #ref (lo que cierra el loop del agente)
El cambio que más sube el nivel del agente. Un solo cambio resolvió tres cosas a la vez:
#ref=backendDOMNodeId(no elnodeIdefímero del AX tree).render_ax_outlineahora emite el id del nodo DOM real, estable mientras el nodo viva → el#refque el LLM lee sigue válido cuando actúa un instante después. Resuelve "ref→acción" y "refs estables" de golpe, sin mantener mapa de estado en el MCP.- Funciones por ref (
cdp_click_ref,cdp_type_ref,cdp_hover_ref): resuelvenbackendDOMNodeId→ bbox (DOM.getBoxModel) → centro → acción humanizada. - Primitivo único
cdp_click_xy_human: click humanizado por coordenadas, compartido por las tres vías (selector,#ref, y futura visión OCR/YOLO).cdp_click_humanse refactorizó para usarlo (un solo camino). - Auto-observe: cada tool
_refdel MCP devuelve el outline AX actualizado tras la acción → el LLM ve el efecto sin pedir otra lectura = verificación implícita (como Playwright MCP). - Defectos de calidad de
render_ax_outlinecorregidos en la misma edición: guard de ciclo + límite de profundidad (evitaRecursionError), se quitó elljust(60)(gastaba tokens en relleno), y ahora renderiza elvaluede los inputs (texto escrito, estado).
Validado por la prueba 9: login completo en the-internet actuando solo por #ref (sin un solo selector CSS).
Pendiente de esta familia: política de humanización por sesión (human/fast/instant) para scraping masivo.
Tercera tanda de fix (B) — verificación post-acción
- B — fin del "fire-and-forget" (
cdp_click.go,cdp_type_text.go).cdp_clickahora verifica que el elemento es visible antes de clicar (display:none / tamaño 0 / opacity 0 → error, en vez de clicar en (0,0) sin efecto).cdp_type_textverifica que hay un campo editable enfocado (input/textarea/ select/contenteditable) antes de escribir (sin foco → error claro "usa CdpClick primero", en vez de escribir a la nada). Validado por la prueba 8. Pendiente de esta familia:cdp_scrollcon target explícito (P1.5) y el puente percepción→acción por nodeId (P1.3).
Hallazgos secundarios
press_key Enterno dispara widgets JS complejos. Contra el buscador de Wikipedia (typeahead Vue del skin Vector) el keyevent sintético se ejecutó sin error pero el widget no reaccionó. La prueba 3 se reorientó a un formulario HTML normal (login de the-internet), donde Enter sí envía el form. Deuda: para widgets JS-driven puede hacer falta disparar el evento del framework o submit explícito.- Las pruebas comparten un mismo Chrome, por lo que el estado (cookies) se acumula entre ellas. Se
añadió
cookie_clearal inicio de las pruebas con login para aislarlas. Un harness más estricto usaría un contexto/perfil por prueba.
Deuda pendiente (no bloqueó el ejercicio)
storage_stateaún no capturasessionStorage(sí cookies + localStorage). Suficiente para sesiones basadas en cookie como the-internet; insuficiente para sitios que guardan el token en sessionStorage.- Verificación post-acción (P1 del análisis LLM-readiness):
dom_click/dom_typesiguen siendo fire-and-forget. En estas pruebas se compensó condom_wait_elementy checks poreval, pero las tools no confirman su efecto por sí mismas.
Cómo reproducir
# 1. Chrome aislado en 9333 (headless)
systemd-run --user -q --unit=browser_demo \
chromium --headless=new --remote-debugging-port=9333 \
--user-data-dir=/tmp/browser_mcp_userdata about:blank
# 1-bis. O con ventana visible (Linux con sesión gráfica):
systemd-run --user -q --unit=browser_demo \
--setenv=DISPLAY=:0 --setenv=XAUTHORITY=$HOME/.Xauthority \
chromium --remote-debugging-port=9333 \
--user-data-dir=/tmp/browser_mcp_visible --start-maximized about:blank
# 2. Compilar el MCP y ejecutar la batería
cd projects/web_scraping/apps/browser_mcp && go build -o browser_mcp .
cd ../../demo_e2e && python3 run_demo.py
# 3. Parar el navegador
systemctl --user stop browser_demo.service
Archivos
mcp_client.py— cliente MCP stdio secuencial (reutilizable).run_demo.py— las 5 pruebas.results/prueba_N_*.json— pasos, respuestas y datos extraídos por prueba.results/run.log— log de la corrida.results/summary.json— veredicto agregado.results/mcp_stderr.log— stderr del servidor MCP.