Compare commits

..

3 Commits

Author SHA1 Message Date
egutierrez 1c5b81f711 merge: quick/cdp-speed-modes — modo de velocidad de sesión + aceleraciones CDP (v0.8.0) 2026-06-13 14:28:20 +02:00
egutierrez a48e262371 docs: app.md v0.8.0 — browser_set_mode + aceleraciones CDP
Documenta los cambios de la v0.8.0 en el app.md del browser_mcp:

- Bump de versión 0.7.0 -> 0.8.0 y descripción (45 -> 46 tools, mención del modo de velocidad de sesión).
- Sección Tools (46): añade browser_set_mode en el grupo Sesión.
- Capability growth log: entrada v0.8.0 detallando el flag de velocidad, el settle adaptativo, la escritura insertText en auto, el poll del puerto en launch_profile, los enable cacheados, wait_load por evento, el timeout de sendCDP y las nuevas CdpInsertText/CdpTypeRefFast, con los números del smoke contra Chrome 9333.
2026-06-13 14:28:03 +02:00
egutierrez fa1efe6fd5 feat: modo de velocidad de sesión (browser_set_mode) + acciones más rápidas en auto
Añade un flag de velocidad por sesión para que el manejo del navegador sea muy rápido por defecto, conservando un modo sigiloso para cuando haya detección anti-bot fuerte.

- Nueva tool browser_set_mode (tools_session.go): fija el modo de la sesión por puerto en el pool. 'auto' (default del MCP) = rápido; 'human' = sigiloso anti-detección; también admite 'fast'/'instant'. Cada tool de acción puede overridearlo con su arg mode.
- pool.go: estado de modo por puerto (modes map + setMode/getMode), limpiado en drop y closeAll.
- tools_dom.go: effectiveMode resuelve el modo (arg de la llamada > modo de sesión > 'auto'). settleForMode reemplaza el sleep ciego fijo de 400ms tras cada acción mutante: 60ms en auto/fast, aleatorio 250-650ms en human (ritmo no-máquina), 0 en instant. dom_type_ref gana arg mode y rutea a CdpTypeRefFast (insertText, un round-trip) en auto o CdpTypeRef (carácter a carácter) en human. Descripciones del arg mode actualizadas (el default ya no es human).
- tools_lifecycle.go: browser_launch_profile reemplaza el sleep(1s) ciego por un poll del puerto CDP (waitCDPPort).
- .gitignore: ignora registry.db/operations.db (no deben vivir en la app; regla db_locations).

Doctrina invertida respecto a la anterior 'humanizado siempre': ahora rápido por defecto, sigiloso bajo demanda.
2026-06-13 14:27:56 +02:00
6 changed files with 160 additions and 26 deletions
+4
View File
@@ -1,2 +1,6 @@
/browser_mcp
*.log
# registry.db sólo existe en la raíz del repo (regla db_locations). Si un test o el
# binario lo crea aquí por un path relativo, es basura: ignorarlo evita trackearlo.
registry.db
operations.db*
+22 -3
View File
@@ -2,8 +2,8 @@
name: browser_mcp
lang: go
domain: infra
version: 0.7.0
description: "Servidor MCP que expone control total del navegador via CDP (45 tools: navegación, DOM, cookies, iframes, teclado/scroll, diálogos, estado de sesión, selección determinista de pestaña, lectura compacta texto/AX nativa + bucle percibir→actuar por #ref con auto-observe, percepción y lectura de texto dentro de iframes, click por coordenadas, screenshot devuelto como image content que el LLM ve, y gestión del ciclo de vida de Chromium por perfil: listar masters en ejecución, lanzar un perfil concreto con o sin CDP, y cerrar limpio) reusando funciones del dominio browser del registry con un pool de conexiones CDP vivas. Por defecto opera sobre un Chrome aislado (puerto 9333) separado del navegador diario."
version: 0.8.0
description: "Servidor MCP que expone control total del navegador via CDP (46 tools: navegación, DOM, cookies, iframes, teclado/scroll, diálogos, estado de sesión, selección determinista de pestaña, modo de velocidad de sesión (browser_set_mode: 'auto' rápido por defecto / 'human' sigiloso anti-detección), lectura compacta texto/AX nativa + bucle percibir→actuar por #ref con auto-observe, percepción y lectura de texto dentro de iframes, click por coordenadas, screenshot devuelto como image content que el LLM ve, y gestión del ciclo de vida de Chromium por perfil: listar masters en ejecución, lanzar un perfil concreto con o sin CDP, y cerrar limpio) reusando funciones del dominio browser del registry con un pool de conexiones CDP vivas. Por defecto opera sobre un Chrome aislado (puerto 9333) separado del navegador diario."
tags: [mcp, browser, cdp, automation, scraping]
e2e_checks:
- id: build
@@ -118,12 +118,13 @@ podría manipular pestañas ajenas del usuario (banca, correo). Para evitarlo:
- Para adjuntarte deliberadamente al navegador diario, pasa `port: 9222` explícito en cada
tool. Hazlo solo con cuidado.
## Tools (45)
## Tools (46)
### Sesión (`tools_session.go`)
- `browser_launch` (MUTA) — lanza Chrome con CDP. args: port, headless, user_data_dir, url.
- `browser_connect` — abre/poolea la conexión CDP del puerto. args: port.
- `browser_disconnect` — cierra y descarta la conexión del puerto (no mata Chrome). args: port.
- `browser_set_mode` — fija el modo de velocidad de sesión del puerto: `auto` (default, rápido) o `human` (sigiloso anti-detección). args: port, mode. Cada tool de acción puede overridearlo con su arg `mode`.
### Ciclo de vida por perfil (`tools_lifecycle.go`)
Gestionan los Chromium del USUARIO por perfil (`Personal`, `Work`, ...), distintos del Chrome
@@ -286,6 +287,24 @@ Funciones del dominio `browser` que NO se exponen como tools en esta versión, c
## Capability growth log
- v0.8.0 (2026-06-13) — Aceleración del manejo del navegador via CDP + flag de velocidad de
sesión. (1) Nueva tool `browser_set_mode` (45 → 46 tools): fija el modo de velocidad por puerto
en el pool — `auto` (default del MCP, rápido) vs `human` (sigiloso anti-detección). El modo se
resuelve por acción con `effectiveMode`: arg `mode` de la tool > modo de sesión > `auto`. (2) Settle
adaptativo: el sleep ciego fijo de 400ms tras cada acción mutante (`dom_click_ref`/`dom_type_ref`/
`dom_hover_ref`/`dom_click_xy`) pasa a `settleForMode` — 60ms en `auto`, aleatorio 250-650ms en
`human` (ritmo no-máquina), 0 en `instant`. (3) `dom_type_ref` ahora tiene arg `mode`: en `auto`
usa `CdpTypeRefFast` (`Input.insertText`, un solo round-trip) y en `human` teclea carácter a
carácter (`CdpTypeRef`) con pausas aleatorias. (4) `browser_launch_profile` reemplaza el `sleep(1s)`
ciego por un poll del puerto CDP (`waitCDPPort`). Cambios en el dominio `browser` del registry que
aprovecha el MCP: `Accessibility.enable`/`Network.enable`/`Page.enable` cacheados por conexión
(`ensureAX`/`ensureNetwork`/`ensurePage` en `CDPConn`) — se eliminan round-trips redundantes en cada
percepción/espera; `cdp_wait_load` pasa de polling de `document.readyState` cada 200ms a esperar el
evento `Page.loadEventFired` (fast path si ya está `complete`); `sendCDP` adquiere timeout
(`cdpCmdTimeout` 30s) para no colgar el tool indefinidamente; nuevas `CdpInsertText` y
`CdpTypeRefFast` (camino rápido de escritura); el modo `auto` se añade al perfil de ratón
(`MouseProfileForMode`) como alias rápido de `fast`. Smoke contra Chrome 9333: percepción #2 con
enable cacheado 1.7ms (vs 3.7ms la #1), `wait_load` fast-path 245µs (vs ≥200ms del polling previo).
- v0.7.0 (2026-06-10) — Ciclo de vida de Chromium por perfil (`tools_lifecycle.go`). Tres tools
nuevas: `browser_list` (enumera los procesos master de Chromium leyendo `/proc/*/cmdline`,
filtrando por `--user-data-dir` presente y `--type=` ausente), `browser_launch_profile` (lanza un
+19
View File
@@ -23,6 +23,7 @@ type connPool struct {
pids map[int]int // puerto -> PID del Chrome lanzado por el MCP (solo los SUYOS)
cancels map[int]func() // cancels de handlers persistentes (handle_dialog)
dialogLogs map[int]*browser.DialogLog // log de diálogos auto-respondidos por puerto
modes map[int]string // puerto -> modo de velocidad de sesión ("auto"|"human"|...)
}
func newConnPool() *connPool {
@@ -31,9 +32,25 @@ func newConnPool() *connPool {
pids: map[int]int{},
cancels: map[int]func(){},
dialogLogs: map[int]*browser.DialogLog{},
modes: map[int]string{},
}
}
// setMode fija el modo de velocidad de sesión para un puerto (lo lee
// effectiveMode cuando una tool de acción no trae su propio arg `mode`).
func (p *connPool) setMode(port int, mode string) {
p.mu.Lock()
defer p.mu.Unlock()
p.modes[port] = mode
}
// getMode devuelve el modo de sesión del puerto ("" si no se fijó ninguno).
func (p *connPool) getMode(port int) string {
p.mu.Lock()
defer p.mu.Unlock()
return p.modes[port]
}
func (p *connPool) get(port int) (*browser.CDPConn, error) {
p.mu.Lock()
defer p.mu.Unlock()
@@ -121,6 +138,7 @@ func (p *connPool) drop(port int) {
_ = browser.CdpClose(c, pid)
delete(p.conns, port)
delete(p.pids, port)
delete(p.modes, port)
}
// connectTarget descarta la conexión actual del puerto y reconecta a un target
@@ -188,6 +206,7 @@ func (p *connPool) closeAll() {
p.pids = map[int]int{}
p.cancels = map[int]func(){}
p.dialogLogs = map[int]*browser.DialogLog{}
p.modes = map[int]string{}
}
// isConnErr reconoce errores de conexión CDP muerta para reintentar UNA vez.
+65 -15
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"fmt"
"math/rand"
"time"
"github.com/mark3labs/mcp-go/mcp"
@@ -29,9 +30,40 @@ func registerDomTools(s *server.MCPServer, d *deps) {
}
}
// settleDelay es la espera breve tras una acción mutante antes de re-percibir,
// dando tiempo a que el DOM se asiente (navegación, focus, repaint).
const settleDelay = 400 * time.Millisecond
// defaultMode es el modo de velocidad cuando ni la llamada ni la sesión fijan uno.
// "auto" = rápido (movimiento de ratón mínimo, escritura en un solo evento, settle
// breve) — el modo por defecto del MCP. "human" (Bézier + esperas aleatorias) se
// activa explícitamente vía browser_set_mode o el arg `mode` cuando un sitio
// aplique detección anti-bot fuerte.
const defaultMode = "auto"
// effectiveMode resuelve el modo de velocidad de una acción: el arg de la llamada
// gana; si está vacío, el modo de sesión fijado por browser_set_mode; si tampoco
// hay, defaultMode.
func (d *deps) effectiveMode(port int, callMode string) string {
if callMode != "" {
return callMode
}
if m := d.pool.getMode(port); m != "" {
return m
}
return defaultMode
}
// settleForMode es la espera tras una acción mutante antes de re-percibir, dando
// tiempo a que el DOM se asiente (navegación, focus, repaint). En "human" es
// ALEATORIA (250-650ms) para no exhibir un ritmo de máquina; en auto/fast es breve
// y fija (60ms); en "instant" es nula.
func settleForMode(mode string) time.Duration {
switch mode {
case "human", "":
return time.Duration(250+rand.Intn(401)) * time.Millisecond // 250..650
case "instant":
return 0
default: // auto, fast
return 60 * time.Millisecond
}
}
// ---- dom_click_ref (MUTA) — bucle percibir→actuar ----
@@ -46,19 +78,22 @@ func domClickRefTool() mcp.Tool {
mcp.WithDescription("Click sobre el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable). Devuelve el outline actualizado tras la acción (auto-observe)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
mcp.WithNumber("ref", mcp.Required(), mcp.Description("#ref del elemento (backendDOMNodeId) leído del outline de page_perceive.")),
mcp.WithString("mode", mcp.Description("Velocidad: 'human' (default, Bézier+jitter anti-bot), 'fast' (movimiento reducido, scraping masivo), 'instant' (element.click() JS, sin eventos de ratón; también fallback si el elemento no tiene geometría).")),
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión: movimiento de ratón reducido, rápido), 'human' (Bézier+jitter+pausas aleatorias anti-bot, para detección fuerte), 'instant' (element.click() JS, sin eventos de ratón; también fallback si el elemento no tiene geometría). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
)
}
func (d *deps) handleDomClickRef(_ context.Context, _ mcp.CallToolRequest, a domClickRefArgs) (*mcp.CallToolResult, error) {
port := portOr(a.Port)
mode := d.effectiveMode(port, a.Mode)
err := d.withConn(port, func(c *browser.CDPConn) error {
return browser.CdpClickRef(c, a.Ref, browser.MouseProfileForMode(a.Mode))
return browser.CdpClickRef(c, a.Ref, browser.MouseProfileForMode(mode))
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
if dl := settleForMode(mode); dl > 0 {
time.Sleep(dl)
}
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText("clicked ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
}
@@ -69,6 +104,7 @@ type domTypeRefArgs struct {
Port int `json:"port"`
Ref int `json:"ref"`
Text string `json:"text"`
Mode string `json:"mode"`
}
func domTypeRefTool() mcp.Tool {
@@ -77,6 +113,7 @@ func domTypeRefTool() mcp.Tool {
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
mcp.WithNumber("ref", mcp.Required(), mcp.Description("#ref del elemento (backendDOMNodeId) leído del outline de page_perceive.")),
mcp.WithString("text", mcp.Required(), mcp.Description("Texto a escribir en el elemento.")),
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión, escribe en un solo evento Input.insertText — rápido) o 'human' (caracter a caracter con pausas aleatorias, anti-detección). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
)
}
@@ -85,14 +122,21 @@ func (d *deps) handleDomTypeRef(_ context.Context, _ mcp.CallToolRequest, a domT
return mcp.NewToolResultError("text is required"), nil
}
port := portOr(a.Port)
// TODO: preset de humanización por sesión (human/fast/instant)
mode := d.effectiveMode(port, a.Mode)
err := d.withConn(port, func(c *browser.CDPConn) error {
return browser.CdpTypeRef(c, a.Ref, a.Text)
// human => teclea caracter a caracter (eventos de tecla reales + ritmo
// irregular). auto/fast/instant => inserta todo en un solo round-trip.
if mode == "human" {
return browser.CdpTypeRef(c, a.Ref, a.Text)
}
return browser.CdpTypeRefFast(c, a.Ref, a.Text)
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
if dl := settleForMode(mode); dl > 0 {
time.Sleep(dl)
}
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText("typed into ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
}
@@ -110,19 +154,22 @@ func domHoverRefTool() mcp.Tool {
mcp.WithDescription("Hover sobre el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable). Devuelve el outline actualizado tras la acción (auto-observe)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
mcp.WithNumber("ref", mcp.Required(), mcp.Description("#ref del elemento (backendDOMNodeId) leído del outline de page_perceive.")),
mcp.WithString("mode", mcp.Description("Velocidad: 'human' (default, Bézier+jitter), 'fast' (movimiento reducido), 'instant' (sin movimiento de ratón).")),
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión: movimiento reducido, rápido), 'human' (Bézier+jitter+pausas aleatorias anti-bot), 'instant' (sin movimiento de ratón). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
)
}
func (d *deps) handleDomHoverRef(_ context.Context, _ mcp.CallToolRequest, a domHoverRefArgs) (*mcp.CallToolResult, error) {
port := portOr(a.Port)
mode := d.effectiveMode(port, a.Mode)
err := d.withConn(port, func(c *browser.CDPConn) error {
return browser.CdpHoverRef(c, a.Ref, browser.MouseProfileForMode(a.Mode))
return browser.CdpHoverRef(c, a.Ref, browser.MouseProfileForMode(mode))
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
if dl := settleForMode(mode); dl > 0 {
time.Sleep(dl)
}
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText("hovered ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
}
@@ -142,19 +189,22 @@ func domClickXYTool() mcp.Tool {
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
mcp.WithNumber("x", mcp.Required(), mcp.Description("Coordenada X absoluta en CSS pixels del viewport.")),
mcp.WithNumber("y", mcp.Required(), mcp.Description("Coordenada Y absoluta en CSS pixels del viewport.")),
mcp.WithString("mode", mcp.Description("Velocidad: 'human' (default, Bézier+jitter anti-bot), 'fast' (movimiento reducido, scraping masivo), 'instant' (sin movimiento de ratón).")),
mcp.WithString("mode", mcp.Description("Velocidad: 'auto' (default de sesión: movimiento reducido, rápido), 'human' (Bézier+jitter+pausas aleatorias anti-bot), 'instant' (sin movimiento de ratón). Vacío = modo de sesión (browser_set_mode) o 'auto'.")),
)
}
func (d *deps) handleDomClickXY(_ context.Context, _ mcp.CallToolRequest, a domClickXYArgs) (*mcp.CallToolResult, error) {
port := portOr(a.Port)
mode := d.effectiveMode(port, a.Mode)
err := d.withConn(port, func(c *browser.CDPConn) error {
return browser.CdpClickXYHuman(c, a.X, a.Y, browser.MouseProfileForMode(a.Mode))
return browser.CdpClickXYHuman(c, a.X, a.Y, browser.MouseProfileForMode(mode))
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
if dl := settleForMode(mode); dl > 0 {
time.Sleep(dl)
}
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText(fmt.Sprintf("clicked at (%g, %g)\n\n%s", a.X, a.Y, outline)), nil
}
+20 -7
View File
@@ -324,16 +324,16 @@ func (d *deps) handleBrowserLaunchProfile(_ context.Context, _ mcp.CallToolReque
pid := cmd.Process.Pid
_ = cmd.Process.Release()
// Give Chromium a moment to come up. If it forwarded to an existing master the
// child exits fast; the launched pid is still informative.
time.Sleep(1 * time.Second)
// When cdp=true, opportunistically confirm the port responds (best-effort: a
// forwarded launch may not bind the port if the master had no CDP).
// Give Chromium a moment to come up. With CDP we poll the port instead of a
// blind 1s sleep: we return as soon as it responds (best-effort: a forwarded
// launch may not bind the port if the master had no CDP). Without CDP there's
// no port to poll, so we give the window a short margin to appear / forward.
if a.CDP && note == "" {
if !cdpPortResponds(cdpPort) {
if !waitCDPPort(cdpPort, 5*time.Second) {
note = "cdp port not confirmed listening yet"
}
} else {
time.Sleep(300 * time.Millisecond)
}
out := map[string]any{
@@ -452,6 +452,19 @@ func processAlive(pid int) bool {
return err == nil
}
// waitCDPPort polls the CDP port until it accepts a TCP connection or the timeout
// elapses. Replaces a blind sleep: returns as soon as Chromium binds the port.
func waitCDPPort(port int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if cdpPortResponds(port) {
return true
}
time.Sleep(100 * time.Millisecond)
}
return cdpPortResponds(port)
}
// cdpPortResponds reports whether something is listening on the CDP port on
// 127.0.0.1. Single TCP dial with a short timeout; best-effort confirmation only.
func cdpPortResponds(port int) bool {
+30 -1
View File
@@ -12,13 +12,15 @@ import (
"fn-registry/functions/browser"
)
// registerSessionTools wires browser_launch (MUTA), browser_connect, browser_disconnect.
// registerSessionTools wires browser_launch (MUTA), browser_connect, browser_disconnect,
// browser_set_mode.
func registerSessionTools(s *server.MCPServer, d *deps) {
if !d.readOnly {
s.AddTool(launchTool(), mcp.NewTypedToolHandler(d.handleLaunch))
}
s.AddTool(connectTool(), mcp.NewTypedToolHandler(d.handleConnect))
s.AddTool(disconnectTool(), mcp.NewTypedToolHandler(d.handleDisconnect))
s.AddTool(setModeTool(), mcp.NewTypedToolHandler(d.handleSetMode))
}
// maxLaunchedChromes es el tope duro de instancias Chrome que el MCP puede tener
@@ -142,3 +144,30 @@ func (d *deps) handleDisconnect(_ context.Context, _ mcp.CallToolRequest, a disc
}
return mcp.NewToolResultText(msg), nil
}
// ---- browser_set_mode ----
type setModeArgs struct {
Port int `json:"port"`
Mode string `json:"mode"`
}
func setModeTool() mcp.Tool {
return mcp.NewTool("browser_set_mode",
mcp.WithDescription("Fija el modo de velocidad de SESIÓN de las acciones del navegador en este puerto. 'auto' (default del MCP) = rápido: movimiento de ratón mínimo, escritura en un solo evento (Input.insertText) y esperas breves — para scraping y automatización propia. 'human' = sigiloso anti-detección: trayectoria de ratón Bézier con jitter, escritura carácter a carácter y esperas ALEATORIAS entre acción y percepción — actívalo cuando un sitio aplique detección anti-bot fuerte. El arg 'mode' de cada tool de acción (dom_click_ref, dom_type_ref, dom_hover_ref, dom_click_xy) sigue ganando puntualmente sobre este ajuste de sesión."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
mcp.WithString("mode", mcp.Required(), mcp.Description("'auto' (rápido, default) o 'human' (sigiloso, anti-detección). También admite 'fast' (alias de auto) e 'instant' (sin movimiento de ratón) para casos puntuales.")),
)
}
func (d *deps) handleSetMode(_ context.Context, _ mcp.CallToolRequest, a setModeArgs) (*mcp.CallToolResult, error) {
switch a.Mode {
case "auto", "human", "fast", "instant":
// válido
default:
return mcp.NewToolResultError("mode debe ser 'auto' o 'human' (también 'fast'/'instant')"), nil
}
port := portOr(a.Port)
d.pool.setMode(port, a.Mode)
return mcp.NewToolResultText(fmt.Sprintf("session mode set to %q for port=%d (cada tool de acción puede overridearlo con su arg mode)", a.Mode, port)), nil
}