From ae324562e846730a0953b2026d1467ae72ff964f Mon Sep 17 00:00:00 2001 From: agent Date: Sat, 6 Jun 2026 13:16:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20bucle=20percibir->actuar=20=E2=80=94=20?= =?UTF-8?q?dom=5Fclick=5Fref/type=5Fref/hover=5Fref=20por=20#ref=20+=20aut?= =?UTF-8?q?o-observe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.md | 36 ++++++++++++++++-- main.go | 2 +- tools_dom.go | 100 ++++++++++++++++++++++++++++++++++++++++++++++++++ tools_read.go | 30 ++++++++++++--- 4 files changed, 158 insertions(+), 10 deletions(-) diff --git a/app.md b/app.md index 2e84b27..27687e7 100644 --- a/app.md +++ b/app.md @@ -2,8 +2,8 @@ name: browser_mcp lang: go domain: infra -version: 0.2.0 -description: "Servidor MCP que expone control total del navegador via CDP (36 tools: navegación, DOM, cookies, iframes, teclado/scroll, diálogos, estado de sesión, selección determinista de pestaña + lectura compacta texto/AX) 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.3.0 +description: "Servidor MCP que expone control total del navegador via CDP (39 tools: navegación, DOM, cookies, iframes, teclado/scroll, diálogos, estado de sesión, selección determinista de pestaña, lectura compacta texto/AX + bucle percibir→actuar por #ref con auto-observe) 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] uses_functions: - chrome_launch_go_browser @@ -42,6 +42,10 @@ uses_functions: - cdp_get_text_go_browser - cdp_connect_target_go_browser - cdp_perceive_outline_py_pipelines + - cdp_click_ref_go_browser + - cdp_type_ref_go_browser + - cdp_hover_ref_go_browser + - cdp_click_xy_human_go_browser uses_types: [] framework: "" entry_point: "main.go" @@ -100,7 +104,7 @@ 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 (36) +## Tools (39) ### Sesión (`tools_session.go`) - `browser_launch` (MUTA) — lanza Chrome con CDP. args: port, headless, user_data_dir, url. @@ -141,6 +145,26 @@ podría manipular pestañas ajenas del usuario (banca, correo). Para evitarlo: - `dom_type` (MUTA) — escribe texto en el elemento enfocado. args: port, text. - `dom_find_by_text` — devuelve un selector CSS único para un texto visible. args: port, text. - `dom_wait_element` — espera a que aparezca un selector. args: port, selector, timeout_ms (default 10000). +- `dom_click_ref` (MUTA) — click humanizado por `#ref` (backendDOMNodeId del outline de `page_perceive`) + auto-observe. args: port, ref. +- `dom_type_ref` (MUTA) — enfoca el `#ref` y escribe texto + auto-observe. args: port, ref, text. +- `dom_hover_ref` (MUTA) — hover humanizado por `#ref` + auto-observe. args: port, ref. + +#### Bucle percibir→actuar (por `#ref`) + +`page_perceive` devuelve un outline accionable donde cada elemento lleva un `#ref` +estable (su `backendDOMNodeId`). Las tools `dom_click_ref` / `dom_type_ref` / +`dom_hover_ref` actúan directamente sobre ese `#ref` — no necesitas resolver un +selector CSS. Tras la acción esperan un settle breve (400ms) y **devuelven el +outline actualizado** (auto-observe), cerrando el bucle percibir→actuar: + +``` +page_perceive → outline con #ref de cada elemento +dom_click_ref → click humanizado + outline nuevo tras la acción +dom_type_ref → escribe + outline nuevo +``` + +Las tools `*_ref` usan humanización por defecto (Bézier+jitter). Una política de +sesión `fast`/`instant` para scraping masivo está pendiente (ver TODO en el código). ### Input (`tools_input.go`) — todas MUTA - `press_key` — presiona una tecla nombrada (Enter/Tab/Escape/ArrowDown/...). args: port, key. @@ -205,6 +229,12 @@ Funciones del dominio `browser` que NO se exponen como tools en esta versión, c ## Capability growth log +- v0.3.0 (2026-06-06) — Cierre del bucle percibir→actuar. Nuevas tools `dom_click_ref`, + `dom_type_ref`, `dom_hover_ref`: actúan sobre el `#ref` (backendDOMNodeId estable) del + outline de `page_perceive` con humanización por defecto (Bézier+jitter) y auto-observe + (devuelven el outline actualizado tras la acción). Refactor: la generación del outline + se extrajo a `deps.perceiveOutline`/`perceiveOutlineTab`, reusado por `page_perceive` y + por las tools `*_ref`. 36 → 39 tools. - v0.2.0 (2026-06-06) — P0 LLM-readiness. Seguridad: Chrome aislado por defecto (puerto 9333 + perfil dedicado `/browser_mcp_userdata`), separado del navegador diario en 9222. Nuevas tools: `tab_select` (selección determinista de pestaña por id/URL), `page_get_text` diff --git a/main.go b/main.go index e9212e8..3bb774f 100644 --- a/main.go +++ b/main.go @@ -14,7 +14,7 @@ import ( "fn-registry/functions/browser" ) -const version = "0.1.0" +const version = "0.3.0" type config struct { httpAddr string diff --git a/tools_dom.go b/tools_dom.go index 47236db..f68fba5 100644 --- a/tools_dom.go +++ b/tools_dom.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "time" "github.com/mark3labs/mcp-go/mcp" @@ -20,9 +21,108 @@ func registerDomTools(s *server.MCPServer, d *deps) { s.AddTool(domClickHumanTool(), mcp.NewTypedToolHandler(d.handleDomClickHuman)) s.AddTool(domClickTextTool(), mcp.NewTypedToolHandler(d.handleDomClickText)) s.AddTool(domTypeTool(), mcp.NewTypedToolHandler(d.handleDomType)) + s.AddTool(domClickRefTool(), mcp.NewTypedToolHandler(d.handleDomClickRef)) + s.AddTool(domTypeRefTool(), mcp.NewTypedToolHandler(d.handleDomTypeRef)) + s.AddTool(domHoverRefTool(), mcp.NewTypedToolHandler(d.handleDomHoverRef)) } } +// 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 + +// ---- dom_click_ref (MUTA) — bucle percibir→actuar ---- + +type domClickRefArgs struct { + Port int `json:"port"` + Ref int `json:"ref"` +} + +func domClickRefTool() mcp.Tool { + return mcp.NewTool("dom_click_ref", + mcp.WithDescription("Click humanizado sobre el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable). Usa humanización por defecto (Bézier+jitter). 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.")), + ) +} + +func (d *deps) handleDomClickRef(_ context.Context, _ mcp.CallToolRequest, a domClickRefArgs) (*mcp.CallToolResult, error) { + port := portOr(a.Port) + // TODO: preset de humanización por sesión (human/fast/instant) + err := d.withConn(port, func(c *browser.CDPConn) error { + return browser.CdpClickRef(c, a.Ref, browser.MouseHumanOpts{}) + }) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + time.Sleep(settleDelay) + outline, _ := d.perceiveOutline(port, 4000) + return mcp.NewToolResultText("clicked ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil +} + +// ---- dom_type_ref (MUTA) — bucle percibir→actuar ---- + +type domTypeRefArgs struct { + Port int `json:"port"` + Ref int `json:"ref"` + Text string `json:"text"` +} + +func domTypeRefTool() mcp.Tool { + return mcp.NewTool("dom_type_ref", + mcp.WithDescription("Enfoca el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable) y escribe el texto. 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("text", mcp.Required(), mcp.Description("Texto a escribir en el elemento.")), + ) +} + +func (d *deps) handleDomTypeRef(_ context.Context, _ mcp.CallToolRequest, a domTypeRefArgs) (*mcp.CallToolResult, error) { + if a.Text == "" { + return mcp.NewToolResultError("text is required"), nil + } + port := portOr(a.Port) + // TODO: preset de humanización por sesión (human/fast/instant) + err := d.withConn(port, func(c *browser.CDPConn) error { + return browser.CdpTypeRef(c, a.Ref, a.Text) + }) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + time.Sleep(settleDelay) + outline, _ := d.perceiveOutline(port, 4000) + return mcp.NewToolResultText("typed into ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil +} + +// ---- dom_hover_ref (MUTA) — bucle percibir→actuar ---- + +type domHoverRefArgs struct { + Port int `json:"port"` + Ref int `json:"ref"` +} + +func domHoverRefTool() mcp.Tool { + return mcp.NewTool("dom_hover_ref", + mcp.WithDescription("Hover humanizado sobre el elemento por su #ref del outline de page_perceive (backendDOMNodeId estable). Usa humanización por defecto (Bézier+jitter). 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.")), + ) +} + +func (d *deps) handleDomHoverRef(_ context.Context, _ mcp.CallToolRequest, a domHoverRefArgs) (*mcp.CallToolResult, error) { + port := portOr(a.Port) + // TODO: preset de humanización por sesión (human/fast/instant) + err := d.withConn(port, func(c *browser.CDPConn) error { + return browser.CdpHoverRef(c, a.Ref, browser.MouseHumanOpts{}) + }) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + time.Sleep(settleDelay) + outline, _ := d.perceiveOutline(port, 4000) + return mcp.NewToolResultText("hovered ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil +} + // ---- dom_click (MUTA) ---- type domClickArgs struct { diff --git a/tools_read.go b/tools_read.go index 8460ba7..0b6ddd4 100644 --- a/tools_read.go +++ b/tools_read.go @@ -86,16 +86,34 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa maxChars = 20000 } + outline, err := d.perceiveOutlineTab(port, a.TabID, maxChars) + if err != nil { + return mcp.NewToolResultError(err.Error()), nil + } + return mcp.NewToolResultText(outline), nil +} + +// perceiveOutline genera el outline AX accionable de la pestaña (vía el pipeline +// cdp_perceive_outline). Usa la primera pestaña 'page' del puerto. +func (d *deps) perceiveOutline(port, maxChars int) (string, error) { + return d.perceiveOutlineTab(port, "", maxChars) +} + +// perceiveOutlineTab genera el outline AX accionable de la pestaña indicada (vía +// el pipeline cdp_perceive_outline). Si tabID es "", usa la primera pestaña 'page'. +// Resuelve la raíz del registry para localizar el binario `fn` + el venv de Python +// y ejecuta `/fn run cdp_perceive_outline ` por +// subprocess, devolviendo su stdout truncado a htmlMax. +func (d *deps) perceiveOutlineTab(port int, tabID string, maxChars int) (string, error) { root, err := resolveRoot() if err != nil { - return mcp.NewToolResultError("resolve registry root: " + err.Error()), nil + return "", fmt.Errorf("resolve registry root: %w", err) } - tabID := a.TabID if tabID == "" { tabs, err := browser.CdpListTabs("localhost", port) if err != nil { - return mcp.NewToolResultError("list tabs: " + err.Error()), nil + return "", fmt.Errorf("list tabs: %w", err) } for _, t := range tabs { if t.Type == "page" { @@ -104,7 +122,7 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa } } if tabID == "" { - return mcp.NewToolResultError("no 'page' tab found on port " + fmt.Sprint(port)), nil + return "", fmt.Errorf("no 'page' tab found on port %d", port) } } @@ -125,9 +143,9 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa if msg == "" { msg = err.Error() } - return mcp.NewToolResultError("cdp_perceive_outline failed: " + msg), nil + return "", fmt.Errorf("cdp_perceive_outline failed: %s", msg) } - return mcp.NewToolResultText(truncate(stdout.String(), htmlMax)), nil + return truncate(stdout.String(), htmlMax), nil } // ---- page_get_html ----