Files
egutierrez 618e3b0295 chore: auto-commit (13 archivos)
- 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>
2026-06-06 13:20:36 +02:00

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-dir dedicado. 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 en results/.

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:

  1. page_perceive roto (bug de integración del MCP). Invocaba fn run cdp_perceive_outline --debug-port 9333 ... con flags, pero fn run pasa 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)

  2. cdp_save_storage_state guardaba cookies de todos los dominios. Usaba Network.getAllCookies (global), así que el storage_state arrastraba 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)

  3. cdp_load_storage_state no restauraba la sesión httpOnly. Network.setCookies no aplicaba de forma fiable la cookie de sesión (rack.session, httpOnly) porque a cada cookie le faltaba el campo url. Fix: sintetizar url por cookie a partir de domain/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/cdp inyecta --user-data-dir/--remote-debugging-port globales a todo chromium; el aislamiento dependía de que nuestros flags fueran al final (Chrome usa el último duplicado). Fix: findChrome prefiere 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_launch devuelve 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 — sessionStorage en storage_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_text honesto (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 el nodeId efímero del AX tree). render_ax_outline ahora emite el id del nodo DOM real, estable mientras el nodo viva → el #ref que 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): resuelven backendDOMNodeId → 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_human se refactorizó para usarlo (un solo camino).
  • Auto-observe: cada tool _ref del 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_outline corregidos en la misma edición: guard de ciclo + límite de profundidad (evita RecursionError), se quitó el ljust(60) (gastaba tokens en relleno), y ahora renderiza el value de 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_click ahora 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_text verifica 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_scroll con target explícito (P1.5) y el puente percepción→acción por nodeId (P1.3).

Hallazgos secundarios

  • press_key Enter no 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_clear al 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_state aún no captura sessionStorage (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_type siguen siendo fire-and-forget. En estas pruebas se compensó con dom_wait_element y checks por eval, 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.