Saltar al contenido
← Todos los posts
Blog Meta · Tooling

Cómo validamos los deploys con MCP browser tools en Claude Code

Un bug de MDX truncó el HTML sin warning. Para que no vuelva a pasar, montamos validación post-deploy en dos capas — un script estático y MCP browser tools con Chrome real. Esto es el arco completo del proceso, con los hallazgos reales.

claude-code mcp observability audit tooling

Un build verde no garantiza un render verde. Lo aprendimos cuando un bloque MDX con un identificador entre llaves silenciosamente truncó el HTML del primer post del blog a la mitad — sin warnings, sin tests rojos, sin que Lighthouse en local protestara. Phase 10 mergeó el cambio y prod sirvió HTML inválido durante unas horas hasta que lo notamos manualmente. Ese incidente disparó dos cosas: un validador específico de MDX en pnpm check, y la decisión de montar validación post-deploy en serio. Este post documenta cómo llegamos a esa segunda parte, qué cubre cada capa, y los hallazgos reales del primer pase contra producción.

El bug que no apareció en ningún check

El primer post del blog (how-this-site-is-built) tenía una tabla que mencionaba el campo de madurez de cada producto del laboratorio. En la prosa, accidentalmente quedó `{Maturity}` sin backticks. MDX trata {algo} como una expresión JSX — intenta evaluarla, no encuentra el binding Maturity en scope, y en vez de fallar ruidosamente, el compilador trunca el render a partir de ahí y emite un HTML válido hasta donde llegó. El sitio levantó, el post quedó publicado, y la mitad de abajo simplemente no existía. Cero alertas.

Lo que hace al bug interesante es que ningún check estándar lo agarró:

  • tsc --noEmit pasó — MDX no es TypeScript.
  • astro check pasó — Astro parseó el MDX sin error porque la expresión es válida sintácticamente.
  • Vitest pasó — no había test para el contenido renderizado del post.
  • Lighthouse local pasó — Lighthouse audita el HTML que el server devuelve, y el HTML devuelto era estructuralmente válido, solo incompleto.
  • pnpm build pasó — Astro generó el archivo sin warnings.

Cuatro capas de validación verdes y producción rota. El hotfix (PR #20) fue trivial: wrappear el identificador en backticks. La parte difícil fue convencerse de que el siguiente `{algo}` no escrito todavía no iba a pasar por el mismo embudo.

El multi-agent audit que encontró las grietas

Antes de escribir tooling nueva, paramos a entender el alcance del problema. ¿Cuántos otros checks no estábamos haciendo? ¿Qué otras categorías de bugs podían pasar por debajo del radar de la misma forma?

Para esto delegamos un audit multi-agente desde Claude Code: un revisor de código senior auditando los MDX existentes contra patrones JSX-trigger, un agente SRE auditando la observabilidad post-deploy (logs, alertas, sintéticos), y un agente general mapeando qué validadores externos podían correr contra el sitio publicado (Lighthouse, validadores de OG, sitemap, JSON-LD).

El patrón importante acá no es la lista concreta de hallazgos — esos viven en docs/audits/2026-06-10-prod-phase-11-validation.md — sino la forma de organizar la búsqueda. Cuando el espacio del problema es ancho (“qué otros bugs como este pueden pasar”), un solo modelo razonando linealmente tiende a sesgarse hacia las primeras ideas. Tres agentes con prompts distintos, cada uno revisando su área en paralelo, devuelven un diff más útil del estado del sistema porque cubren ángulos que no se interfieren. Claude Code orquesta esto sin que el usuario tenga que hilar el contexto a mano.

El audit produjo dos outputs concretos: una lista de 11 mejoras de SEO y distribución que se aterrizaron en Phase 11 (RSS bilingüe, og:type=article, JSON-LD para servicios, sitemap con lastmod, skip-link a11y, entre otras), y un mapa de qué chequeos quedaban sin cubrir incluso después de Phase 11 — la motivación directa para Capa 1 y Capa 2 de la validación post-deploy.

Capa 1: el script que corre en 1.8 segundos

scripts/audit-prod.mjs es un script Node 22 standalone — sin dependencias más allá de fetch, regex y JSON nativos — que ejecuta 66 checks contra https://www.culturetech.cl. Cubre cinco categorías: social cards (og:type, og:image alcanzable y con dimensiones correctas, twitter:card, article:* meta), RSS feeds (XML válido, items con campos requeridos, auto-discovery en el <head>), JSON-LD (presencia del @type esperado por tipo de página, BlogPosting con wordCount e image como ImageObject), skip-link a11y (anchor con clases sr-only focus:not-sr-only y target <main id="main">), y sitemap (sitemap-index.xml reachable, lastmod por URL, sample de URLs verificadas, robots.txt referenciando el sitemap).

Cada check tiene severidad (BLOCKER, HIGH, MEDIUM, LOW). Exit code 1 si falla algún BLOCKER o HIGH. Se invoca con pnpm audit:prod y termina en 1.8 segundos contra producción.

La virtud del script es que no necesita un browser. Hace HTTP requests, parsea HTML con regex acotada al markup que importa, valida JSON-LD como JSON, y verifica que las URLs anunciadas en el sitemap devuelvan 200. Esto lo hace barato de correr — se puede poner en cron, se puede correr antes de cada push, no hace falta mantener Chromium ni manejar timeouts de render.

La virtud es también su límite. El script verifica el HTML que el server devuelve, no lo que el browser renderiza. Si un script de terceros inyecta <noindex> después del load, el script no se entera. Si el OG image llega 200 pero está visualmente roto, el script no se entera. Si el Tab key del navegador real no sigue el orden DOM por algún CSS raro, el script no se entera. Para esos casos hace falta Capa 2.

Capa 2: cuando el HTML estático no alcanza

Hay validaciones que sólo se pueden hacer con un browser real ejecutando el HTML. Lighthouse SEO score depende de que el browser parsee el documento, ejecute el JS, y evalúe heurísticas que mezclan markup con render. El preview de un card social depende de que un scraper externo (X, LinkedIn, Facebook) lea las meta-etiquetas exactamente como las leen sus propios crawlers. Tab key salta entre elementos focusables en el orden que el browser determina, que combina DOM con tabindex, display, visibility y pointer-events. Performance metrics como LCP y CLS sólo existen como artefactos del proceso de render bajo condiciones de red y CPU específicas.

Para todo eso, montamos un par de MCP servers en Claude Code:

{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "chrome-devtools-mcp@latest"],
    },
  },
}

chrome-devtools-mcp levanta una instancia de Chrome real — no Chromium headless de testing — y expone tools como mcp__chrome-devtools__lighthouse_audit, mcp__chrome-devtools__navigate_page, mcp__chrome-devtools__take_screenshot, mcp__chrome-devtools__press_key y mcp__chrome-devtools__performance_start_trace. Esto es deliberado: las validaciones visuales y de performance tienen que reflejar el browser que usan los usuarios, no uno de testing con un engine subset. Playwright está también instalado como fallback, pero en general se prefiere Chrome cuando hay alternativa.

El primer pase de Capa 2 contra Phase 11 cubrió cuatro dimensiones:

Lighthouse mobile sobre tres URLs representativas — un blog post (BlogPosting), un servicio (Service) y un vertical (WebPage). Las tres devolvieron 100/100/100/100 (Accessibility, Best Practices, SEO, Agentic Browsing), con cero failures sobre 54 checks. Importante: en mi primer intento navegué a una URL incorrecta (/es-CL/verticales/banca/ directo, cuando la URL real está bajo /es-CL/servicios/verticales/banca/). Lighthouse devolvió 0/0/0/0 porque Cloudflare sirvió la 404 page de Astro sin estructura SEO. Esto confirma el valor de Capa 2: una URL mal escrita pasa silenciosamente por Capa 1, que valida URLs que se le pasan pero no descubre cuáles son las URLs correctas.

Screenshots de cards sociales vía opengraph.xyz — un validador sin login que renderiza Facebook, X, LinkedIn, WhatsApp y Discord lado a lado leyendo las mismas meta-tags que cada plataforma. El meta-tag inspector devolvió 11 Good, 2 Warning (og:description de 135 chars que algunos clientes mobile truncan a ~125; OG image sin CTA visual), 0 Errors. Las tres screenshots quedaron versionadas en docs/audits/assets/ como evidencia visual.

Tab key real desde la home. La primera pulsación de Tab movió el foco al skip-link (Saltar al contenido, href="#main"), confirmando que WCAG 2.1 SC 2.4.1 (Bypass Blocks) no es sólo markup válido sino comportamiento real. La segunda pulsación lo movió al logo del banner — orden DOM sano. Capa 1 había verificado que las clases sr-only focus:not-sr-only y el target <main id="main"> existían. Capa 2 confirmó que el navegador efectivamente respeta ese tab order.

Performance trace mobile con throttling Slow 4G + 4x CPU slowdown. LCP 871 ms (target good < 2,500 ms), CLS 0.00, TTFB 12 ms. El trace raw quedó comprimido en .json.gz (654 KB) e importable a Chrome DevTools → Performance para análisis fino. Toda la latencia es render delay, no red — esperable para un blog post con tipografía variable custom y poco JS.

El patrón: auditoría como código

Lo que importa de todo esto no es la lista de checks ni los scores particulares. Es que el sitio queda con dos artefactos versionados que cualquier futura sesión de Claude Code — o cualquier dev — puede correr sin coordinación adicional:

  • scripts/audit-prod.mjs y pnpm audit:prod para el chequeo barato y rápido del markup público.
  • Un audit doc en docs/audits/2026-06-10-prod-phase-11-validation.md con resultados de Capa 1 y Capa 2, evidencia visual (screenshots, perf trace) y findings priorizados. Linkeable desde cualquier PR futuro que necesite contexto.

El anti-patrón obvio acá es la auditoría como captura de pantalla en Google Drive. Una validación que vive afuera del repo se desactualiza el día que el dueño se va, o el día que cambia la URL del documento, o simplemente el día que nadie recuerda que existía. Una validación que vive en scripts/ y docs/audits/ del propio repo vive el tiempo que vive el repo. Y, mientras tanto, queda como referencia ejecutable para la próxima vez que alguien quiera verificar que el deploy de Phase N no regresó algo que Phase N-1 dejaba funcionando.

Claude Code refuerza este patrón sin pedirlo: en cada sesión carga el CLAUDE.md del proyecto, lee los audits anteriores cuando son relevantes, y propone correr pnpm audit:prod automáticamente después de un merge a main. La validación post-deploy deja de ser un ritual manual que se hace si alguien se acuerda. Pasa a ser una sugerencia natural del entorno de trabajo.

Lo que sigue

Capa 2 todavía depende de que un humano (o Claude Code en modo interactivo) ejecute la sesión. Falta integrarla a CI — un job en GitHub Actions que corra pnpm audit:prod automático después del deploy, y un canal escalable para Capa 2 (lighthouse-ci sirve para el subset Lighthouse; el resto sigue siendo manual). Ese es el siguiente PR.

Falta también lectura real con screen reader. NVDA en Windows o VoiceOver en macOS no son automatizables desde MCP — habría que construir una capa de validación distinta, probablemente con un agente de testing manual asistido. Por ahora, queda documentado como gap conocido en el audit doc.

Y falta cobertura más fina del browser real bajo carga: 100 URLs en lugar de 3, throttling distinto, perfiles de red más realistas (no todos los usuarios son Slow 4G). El trade-off ahí es claro — más cobertura cuesta más wall-clock y más complejidad de parsing de outputs. Lo que hicimos en esta iteración es suficiente para validar Phase 11. Cuando el sitio tenga tráfico real y haya incentivo para optimizar por percentil, el trade-off cambia.

Mientras tanto, el patrón está montado, el bug {Maturity} no puede volver a pasar (lo bloquea scripts/validate-mdx.mjs ejecutado en pnpm check), y el próximo deploy a producción tiene dos checks atrás suyo en vez de cero. Eso es lo que cambió.

¿Te resonó el post?

Si esto te interesa, hay más en los playbooks y los productos del laboratorio. O agenda una conversación si quieres discutir algo de lo escrito acá.