agent-browser
agent-browser
Un navegador local endurecido para agentes de IA. Cero dependencias. Conduce el Chrome que ya tienes a través del Protocolo DevTools, extrae Markdown eficiente en tokens y no se marca trivialmente como automatización.
Superficie | Instalación | Úsalo para |
Servidor MCP |
| Claude Code, Cursor, Codex, cualquier cliente MCP |
CLI |
| shells, scripts, CI |
Biblioteca |
| tu propio código Node |
Plugin DSH / Cordis | fila de composición | herramientas nativas en un entorno DSH |
npx -y @truenix/agent-browser markdown https://news.ycombinator.comTodo se ejecuta localmente. Sin cuenta, sin clave API, sin servicio remoto, sin cuota.
Por qué
Alimentar a un agente con HTML crudo desperdicia la mayor parte de su contexto. Medido en páginas reales:
Y un navegador que se anuncia como automatización es bloqueado, degradado o recibe contenido diferente, lo que corrompe silenciosamente lo que el agente haya concluido.
Benchmark de sitios pesados (en vivo, curl vs markdown)
sitio | HTML crudo | markdown |
| ahorro |
| 927 kB | 120 kB | 55 kB | 7.7× / 16.8× |
| 273 kB | 11 kB | 8.7 kB | 23.8× / 31.4× |
| 323 kB | 10 kB | 7.3 kB | 32.0× / 44.2× |
| 648 kB | 69 kB | 13 kB | 9.3× / 48.9× |
| 360 kB | 4.6 kB | 3.5 kB | 78.6× / 102.8× |
| 35 kB | 10 kB | 3.2 kB | 3.4× / 11.0× |
Regenera ambos gráficos y las mediciones subyacentes de esta tabla con
npm run charts — mide en vivo y luego dibuja los PNG usando el propio
agent-browser, por lo que los números de las imágenes no pueden desviarse de lo
que produce el código.
Gastar menos tokens en la misma página
Las URLs son un tercio de la salida en un artículo de Wikipedia y cuatro quintos
en github.com/trending. Cuando el agente está leyendo en lugar de navegar,
elimínalas:
agent-browser markdown https://github.com/trending --links text # 69 kB -> 13 kB
agent-browser markdown https://en.wikipedia.org/wiki/Rust --links relativemodo | renderiza | úsalo cuando |
|
| el agente navegará a continuación |
|
| rastreo del mismo sitio; conserva los destinos, elimina el origen |
|
| lectura, resumen, respuesta a preguntas |

Costo de tokens por chequeo YC (lectura de HN + YC Blog con --links text, 51.1k → 1.5k tokens, +500 de salida) — precios de lista de agosto de 2026: Anthropic Haiku 4.5 $1/$5, Sonnet 5 $3/$15, Opus 5 $5/$25 • OpenAI GPT-4o $2.50/$10, GPT-4o mini $0.15/$0.60
Páginas que se renderizan después de la carga
La navegación espera el evento load, impulsada por eventos del ciclo de vida de
CDP en lugar de un bucle de sondeo y un retraso fijo. En páginas reales, eso es
2–3× más rápido para una salida idéntica byte a byte:
página |
| antiguo asentamiento fijo de 250 ms |
| 7 ms | 258 ms |
| 115 ms | 340 ms |
| 160 ms | 365 ms |
| 218 ms | 466 ms |
Una aplicación renderizada en el cliente cuyo contenido llega después de la carga necesita una señal real, no una suposición mayor — el antiguo asentamiento de 250 ms también perdía ese contenido:
agent-browser markdown https://some-spa.example --wait idlemodo | espera | úsalo cuando |
| el DOM está analizado | solo necesitas el marcado que vino en el HTML |
| el evento load | casi siempre |
| la red se queda en silencio | el resultado parece un caparazón vacío |
Related MCP server: Browser-MCP Navigator
Cualquier otro entorno
Si tu framework habla JSON Schema, puede controlar agent-browser sin que este
proyecto sepa que el framework existe. agent-browser tools imprime el catálogo
en la forma que necesites:
agent-browser tools # MCP: {name, description, inputSchema}
agent-browser tools --format openai # OpenAI: {type:"function", function:{...}}
agent-browser tools --format anthropic # Anthropic: {name, description, input_schema}Cada herramienta se ejecuta de cualquier manera, según lo que tu entorno pueda hacer:
import { findTool, TOOLS } from '@truenix/agent-browser/tools';
import { withBrowser } from '@truenix/agent-browser';
// in-process: the harness can hold a CDP session
const tool = findTool('browser_markdown');
const text = await withBrowser({}, async (session) => {
await session.navigate('https://example.com');
return tool.run(session, { url: 'https://example.com', links: 'text' });
});
// out-of-process: the harness can only run a command (sandboxes, shells)
tool.cli({ url: 'https://example.com', links: 'text' });
// => ['markdown', 'https://example.com', '--links', 'text']Instalación
Como servidor MCP
claude mcp add browser -- npx -y @truenix/agent-browser mcp{
"mcpServers": {
"browser": {
"command": "npx",
"args": ["-y", "@truenix/agent-browser", "mcp"]
}
}
}Herramientas: browser_markdown, browser_text, browser_html, browser_links, browser_screenshot, browser_evaluate, browser_accessibility_tree, browser_pdf, browser_probe.
Cada superficie expone las mismas nueve herramientas de un catálogo compartido, por lo que la CLI, el servidor MCP y el plugin DSH nunca pueden divergir.
Como biblioteca
npm install @truenix/agent-browserimport { withBrowser } from '@truenix/agent-browser';
const md = await withBrowser({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});Como plugin DSH / Cordis
Una línea (recomendado — se conecta automáticamente cuando quieres decir DSH):
npx -y @truenix/agent-browser install # adds the bundle to ~/.dsh/profiles/web/package.json, then pnpm install (the mount ships in the package's own cordis.patch.yml layer)
# npx -y @truenix/agent-browser install --profile web --dry-run # preview
# npx -y @truenix/agent-browser uninstall # remove againReinicia dsh — las nueve herramientas browser_* aparecen como herramientas
nativas. Ningún manejador se ejecuta con un simple npm install; se requiere un
install intencional.
Manual (si prefieres editar la composición tú mismo):
npm i -g @truenix/agent-browser
# or inside the harness checkout: pnpm add @truenix/agent-browserRequiere Node ≥ 18 y una instalación de Chrome/Chromium. Luego añade a la composición del host (el registro de herramientas vive en el host, no por agente):
# ~/.dsh/profiles/web/cordis.patch.yml — persists for every web session
- insert:
- id: agent-browser
name: '@truenix/agent-browser/cordis'
config:
timeoutMs: 180000 # per-tool call budget; default respects AGENT_BROWSER_BIN / ENDPOINT
# cli: 'npx -y @truenix/agent-browser' # override only if neededForma corta (cuando la composición ya envuelve insert):
- '@truenix/agent-browser/cordis':
timeoutMs: 180000Anulaciones de entorno: AGENT_BROWSER_BIN (binario de Chrome),
AGENT_BROWSER_ENDPOINT (conectar a un navegador de larga duración mediante
--endpoint), o config.cli.
CLI
agent-browser <command> [options]
markdown <url> Extract the whole page as Markdown (main content by default)
text <url> Visible text only
html <url> Full serialized DOM after JavaScript runs
links <url> Every anchor as JSON
screenshot <url> PNG/JPEG (-o file, --full)
pdf <url> PDF (-o file)
a11y <url> Filtered accessibility tree
eval <url> <expr> Evaluate JS, return only its value (cheapest)
probe Browser, GPU and capability report
mcp Run as an MCP server on stdio
tools Print tool schemas (--format mcp|openai|anthropic)Opciones: --headful, --no-stealth, --block-images, --gpu/--no-gpu,
--width, --height, --viewport WxH, --main, --raw, --links,
--max-rows, --limit, --wait, --settle, --full, --endpoint <ws>,
--timeout, --json, -o.
markdown para leer, eval para buscar algo
El error más caro que un agente comete con esta herramienta es renderizar una
página entera para responder a una pregunta de una línea. Tres preguntas
específicas en tres páginas pesadas cuestan 235 bytes mediante eval, frente
a 77 kB de Markdown:
agent-browser eval https://github.com/trending \
"JSON.stringify(Array.from(document.querySelectorAll('article h2 a')).slice(0,3).map(a=>a.innerText.trim()))"
# ["openai / codex","mattpocock / skills","affaan-m / ECC"] -> 58 bytesUsa innerText, no textContent. textContent devuelve el espacio en blanco
crudo de la fuente ("openai /\n\n codex"); innerText da lo que
realmente se renderiza ("openai / codex"). Un agente que se equivoque en eso
paga un viaje de ida y vuelta de reintento completo, que cuesta mucho más que
los bytes que ahorró.
Usa markdown cuando realmente necesites leer, resumir o buscar en la página.
Cada invocación lanza su propio navegador (~1.4 s). Para varias búsquedas
seguidas, mantén un navegador vivo y apunta --endpoint a él — las mismas tres
preguntas tardan 5.9 s en tres arranques en frío, 3.2 s contra uno en caliente.
--endpoint se conecta a un navegador ya en ejecución en lugar de lanzar uno —
útil para reutilizar un único navegador de larga duración en muchas llamadas.
Memoria
El mínimo de Chrome es de unos 420 MB de PSS en 18 procesos antes de cargar
nada, y ese mínimo es de Chrome, no de este paquete — el ajuste de banderas lo
mueve ~5%, y la bandera que lo mueve más (--enable-low-end-device-mode)
informa navigator.deviceMemory: 2 junto a 24 núcleos, una máquina imposible y
exactamente el tipo de inconsistencia que hace que un navegador sea marcado. Así
que la palanca es menos navegadores, no más pequeños.
El servidor MCP mantiene un navegador y da a cada llamada de herramienta su propio contexto aislado. Tres páginas pesadas obtenidas simultáneamente:
PSS máximo | procesos | tiempo real | |
un navegador por llamada | 1289 MB | 44 | 2114 ms |
un navegador, 3 contextos | 654 MB | 20 | 2049 ms |
El aislamiento no cambia — el contexto de navegador por llamada era siempre lo
que lo proporcionaba. El navegador se apaga después de 30 s de inactividad
(AGENT_BROWSER_IDLE_MS, 0 para cerrar inmediatamente), por lo que un
servidor de larga duración no se queda con 420 MB entre conversaciones. Una
llamada repetida mientras está caliente se salta el lanzamiento por completo y
se ejecuta aproximadamente el doble de rápido.
La compensación: las tareas comparten un árbol de procesos, por lo que un fallo
a nivel de navegador se lleva todo lo que está en vuelo en lugar de una sola
llamada. Un navegador muerto se detecta y se relanza en la siguiente llamada. Si
necesitas aislamiento de radio de explosión por tarea, usa withBrowser, que
sigue dando a cada llamada su propio navegador.
import { withPooledSession, shutdownPool } from '@truenix/agent-browser/pool';
await withPooledSession({}, async (session) => {
await session.navigate('https://example.com');
return session.markdown();
});
await shutdownPool(); // or let it idle outPara la CLI, cada invocación es su propio proceso, por lo que reutilizar
significa apuntar --endpoint a un navegador que mantengas vivo tú mismo.
El daemon: un navegador para cada llamada de CLI
Cada invocación de CLI es su propio proceso, por lo que por defecto cada una
arranca en frío su propio Chrome — diez llamadas concurrentes significan diez
navegadores. --daemon comparte un navegador residente en su lugar, y cada
llamada sigue teniendo su propio contexto aislado:
agent-browser markdown https://example.com --daemon
agent-browser daemon --status
agent-browser daemon --stoptiempo real | procesos máximos de Chrome | |
3 búsquedas secuenciales, sin daemon | 5330 ms | — |
3 búsquedas secuenciales, | 3254 ms | — |
10 llamadas concurrentes, sin daemon | 1443 ms | 140 |
10 llamadas concurrentes, | 1144 ms | 32 |
Es opcional (--daemon, o AGENT_BROWSER_DAEMON=1) porque iniciar un proceso
en segundo plano que sobrevive a tu comando es un efecto secundario que vale la
pena pedir. Se inicia bajo demanda y sale después de cinco minutos de
inactividad (AGENT_BROWSER_DAEMON_IDLE_MS). Establecer la variable de entorno
también enruta el servidor MCP hacia él, lo que vale la pena hacer cuando varios
clientes MCP comparten una máquina.
--headful, --width, --height, --block-images y --gpu se fijan cuando
un navegador se lanza, por lo que uno compartido no puede respetarlos. Pasar
cualquiera de ellos gana: esa llamada obtiene silenciosamente su propio
navegador privado, y lo dice en stderr.
Cuatro modos de fallo en torno a los que está construido, cada uno verificado por pruebas:
Exactamente un daemon. Diez primeros llamadores simultáneos generan uno; nueve pierden la carrera para vincular el socket y salen antes de lanzar nada. Vincular el socket es el bloqueo, por lo que no hay archivo de bloqueo que pueda quedar obsoleto.
El socket abierto es el contador de referencias. Un cliente mantiene su conexión mientras trabaja, por lo que un cliente eliminado con
SIGKILLaún libera — el kernel cierra el socket. Un mensaje de "por favor libera" habría filtrado una referencia para siempre.Salida por inactividad, para que no se quede con ~420 MB entre conversaciones.
Un daemon eliminado con
SIGKILLno deja nada varado: su marcador de perfil nombra su pid, por lo que el barrido de abajo recupera el navegador en el siguiente lanzamiento.
Limpieza después de sí mismo
Cada lanzamiento escribe un marcador de propietario en su perfil temporal y barre los perfiles cuyo propietario ha muerto, matando el navegador que aún está adjunto a ellos. Combinado con los manejadores de SIGINT/SIGTERM/SIGHUP, eso da:
cómo termina la llamada | procesos huérfanos | después del siguiente lanzamiento |
normalmente | 0 | 0 |
SIGTERM / SIGINT | 0 | 0 |
SIGKILL (incapturable) | 1 navegador | 0 |
Por lo tanto, un cierre forzado cuesta como máximo un navegador abandonado, no uno por llamada interrumpida. Diez llamadas CLI concurrentes, repetidas, no dejan nada atrás. Un navegador vivo nunca se elimina — su proceso propietario sigue en ejecución, y un perfil no atribuible se deja solo hasta que nada lo tenga abierto y haya estado inactivo durante 60 s.
Detección de bots
Pruébalo tú mismo: npm run test:bot. Último resultado:
detector | resultado |
31 aprobados, 0 fallidos | |
6 verdes, 0 rojos, | |
|
Chrome headless puro falla en cuatro filas de sannysoft (HEADCHR_UA, CHR_MEMORY, WebGL SwiftShader, UA antigua) y se informa como bot.
Un detector inalcanzable cuenta como SKIP, nunca como un aprobado, y la puerta requiere tres aprobados alcanzables — por lo que un día en que los sitios de prueba estén caídos no puede confundirse con éxito.
El entorno importa más que los parches
El mismo código, medido en dos lugares:
esta estación de trabajo | runner de GitHub Actions | |
IP | residencial | centro de datos |
GPU | real (NVIDIA) | ninguna → SwiftShader |
| 31 aprobados, 0 fallidos | 30 aprobados, 1 fallido ( |
| 6 verdes, 0 rojos | 6 verdes, 0 rojos |
|
|
|
Cada señal a nivel de CDP permanece limpia en ambos — esa parte es código, y el código es correcto. Lo que cambia el veredicto es el entorno: un ASN de centro de datos más renderizado por software activa un compuesto de señales débiles que ninguna cantidad de parches de huella digital aborda.
Esta es la forma honesta del problema. Endurecer el navegador elimina las señales triviales. Dónde lo ejecutes decide el resto.
Qué hace el endurecimiento y por qué
Cada elemento provino de un detector que nos decía que estábamos equivocados:
Sin
--enable-automation. Esa bandera — que Puppeteer y Playwright añaden — es la que establecenavigator.webdriver = true. El CDP puro no la establece, por lo que permanecefalsesin nada parcheado.Sin
Runtime.enable. Es la señal CDP más ruidosa y alimenta el detector clásico de consola/Error.stack.Runtime.evaluatefunciona bien sin ella.La ventana y la pantalla se mueven juntas.
--window-sizesin--ozone-override-screen-sizedaouterWidth > screen.width, lo cual es físicamente imposible — una señal más fuerte que el headless simple.Sin anulación de métricas de dispositivo por defecto. El habitual 1280×720 es el viewport por defecto de Playwright y los detectores lo marcan por nombre. Establece
--viewportsolo si lo necesitas.GPU real cuando esté disponible, dando un renderizador genuino
ANGLE (NVIDIA …)en lugar de SwiftShader.UA establecida al inicio, no solo a través de CDP.
Emulation.setUserAgentOverrideno llega a los Web Workers, por lo que un worker sigue informando la UA headless mientras la página informa la limpia (hasInconsistentWorkerValues).Sin anulación de
acceptLanguage. CDP derivanavigator.languagesdividiendo ese encabezado, por lo que"en-US,en;q=0.9"se convierte en["en-US","en;q=0.9"]— un valor q donde ninguno puede existir legalmente, y otro desajuste página/worker.--langlo hace correctamente.Client Hints derivados de la propia versión del binario, por lo que
Sec-CH-UAno puede discrepar connavigator.userAgent.Contexto de navegador aislado por sesión, eliminado al cerrar — estado limpio por tarea sin un segundo proceso de navegador.
La lección recurrente: la consistencia supera a la cobertura. Cuatro de esos son casos donde la suplantación parcial hizo la detección más fácil, detectada solo al ejecutar detectores reales.
Por qué la suplantación de WebGL está desactivada por defecto
spoofWebgl existe y está implementado cuidadosamente — un Proxy alrededor de getParameter nativo, por lo que Function.prototype.toString aún informa [native code]. Está desactivado, porque la medición dice que sale mal. De test/webgl-spoof-experiment.mjs:
brazo | renderizador declarado | maxTexture | extensiones | sannysoft | veredicto |
GPU real, sin suplantación | NVIDIA | 32768 | 37 | 0 fallidos |
|
SwiftShader, honesto | SwiftShader | 8192 | 35 | 1 fallido |
|
SwiftShader + suplantación | NVIDIA | 8192 | 35 | 0 fallidos |
|
Reclamar hardware que no tienes arregla una fila cosmética y falla el detector compuesto: el script inyectado no llega a los Web Workers, por lo que el worker aún informa SwiftShader, y MAX_TEXTURE_SIZE permanece en el valor de software mientras la cadena del renderizador afirma una GPU discreta.
SwiftShader honesto pasa. Una mentira convincente no. Dale al navegador una GPU real en su lugar — es gratis.
Lo que esto NO hace
La detección a nivel de huella digital es todo el alcance. No derrota, y no intenta derrotar:
Huella digital TLS/JA3-JA4 y HTTP/2 — decidida antes de que se ejecute cualquier JavaScript
Reputación de IP — centro de datos vs ASN residencial, a menudo el verdadero bloqueador
Análisis de comportamiento — trayectorias del ratón, tiempos, permanencia
Los productos comerciales de desafío se apoyan en eso, por lo que "pasa la puerta" significa no marcado trivialmente como automatización, nunca indetectable. Destinado a tus propios sitios, pruebas, trabajo de accesibilidad y navegación ordinaria de agentes.
Memoria
Una pila de Chrome cuesta aproximadamente 450 MB. La palanca es la arquitectura, no las banderas: ejecuta un navegador y muchos contextos aislados en lugar de un navegador por tarea. Inicia un navegador una vez, luego apunta cada llamada a él con --endpoint / AGENT_BROWSER_ENDPOINT. --block-images ayuda para trabajo de texto.
Cero dependencias
dependencies está vacío, incluido el transporte WebSocket.
El WebSocket global de Node (WHATWG) no puede enviar encabezados de solicitud, que cualquier endpoint CDP autenticado o con proxy necesita, y undici no es importable de forma independiente. Así que src/ws.mjs implementa RFC 6455 directamente sobre node:http(s) — handshake, enmascaramiento, fragmentos de continuación, longitudes de 64 bits, ping/pong — que es todo lo que CDP requiere.
El convertidor de Markdown recorre el DOM con una pila explícita, manteniendo la profundidad de llamadas JS en O(1) independientemente del anidamiento, y usa innerText nativo para nodos en línea a nivel de hoja. Eso lo hace seguro contra recursión en documentos profundamente anidados y notablemente más rápido en páginas grandes.
Entorno
| ruta a un binario de Chrome/Chromium |
| conectar a este endpoint CDP en lugar de lanzar |
Requisitos
Node ≥ 18 y una instalación de Chrome/Chromium. Sin paso de compilación.
Créditos
El endurecimiento de este proyecto se deriva casi por completo de la investigación de detección publicada por otras personas — ver CREDITS.md. Agradecimientos especiales a rebrowser-bot-detector, bot.sannysoft.com, deviceandbrowserinfo.com y Camoufox por mostrar cómo se hace correctamente.
Licencia
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
Browser MCP for logged-in tasks. Uses your Chrome — credentials stay local. Zero-token replay.
Web scraping MCP server: any URL to Markdown or JSON, with JavaScript rendering and batch jobs.
MCP server for Firecrawl — web search, scraping, and biomedical/arXiv paper search.
Stealth web browser for agents: search, fetch, click, download and type in persistent MCP sessions.
Related MCP Servers
- AlicenseAqualityBmaintenanceMCP server for browser automation with anti-detection. Scout pages, find elements, interact with websites, and monitor network traffic from any AI client that supports the Model Context Protocol.211MIT
- FlicenseBqualityCmaintenanceUltra-fast browser automation server over Chrome DevTools Protocol (CDP), exposed as MCP, enabling AI agents to control a real Chrome browser with low latency and minimal token usage.212-
- AlicenseAqualityBmaintenanceA fault-tolerant, stealth-enabled Model Context Protocol (MCP) server for web searching and content fetching. Built for AI Agents (Cursor, Claude Code, OpenCode), it uses a stealth browser engine to fetch pages, dynamically handles SPAs/React, and converts bloat into token-optimized Markdown.213 npm3MIT
- AlicenseAqualityAmaintenanceMCP server that drives your real Chrome/Edge/Opera browser through a Chrome extension and DevTools Protocol, preserving logins and session state, and can also perform OS-level mouse and keyboard input behind approval.5134 PyPI2MIT