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 --noEmitpasó — MDX no es TypeScript.astro checkpasó — 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 buildpasó — 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.mjsypnpm audit:prodpara el chequeo barato y rápido del markup público.- Un audit doc en
docs/audits/2026-06-10-prod-phase-11-validation.mdcon 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ó.