# 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: 8/8 PASS (headless y con ventana visible) Las pruebas 1-5 son la batería inicial; 6-8 se añadieron para validar las tandas de fixes (A/D/E y B). | # | 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) | 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. ## 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 ```bash # 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.