feat: bucle percibir->actuar — dom_click_ref/type_ref/hover_ref por #ref + auto-observe
This commit is contained in:
@@ -2,8 +2,8 @@
|
|||||||
name: browser_mcp
|
name: browser_mcp
|
||||||
lang: go
|
lang: go
|
||||||
domain: infra
|
domain: infra
|
||||||
version: 0.2.0
|
version: 0.3.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."
|
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]
|
tags: [mcp, browser, cdp, automation, scraping]
|
||||||
uses_functions:
|
uses_functions:
|
||||||
- chrome_launch_go_browser
|
- chrome_launch_go_browser
|
||||||
@@ -42,6 +42,10 @@ uses_functions:
|
|||||||
- cdp_get_text_go_browser
|
- cdp_get_text_go_browser
|
||||||
- cdp_connect_target_go_browser
|
- cdp_connect_target_go_browser
|
||||||
- cdp_perceive_outline_py_pipelines
|
- 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: []
|
uses_types: []
|
||||||
framework: ""
|
framework: ""
|
||||||
entry_point: "main.go"
|
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
|
- Para adjuntarte deliberadamente al navegador diario, pasa `port: 9222` explícito en cada
|
||||||
tool. Hazlo solo con cuidado.
|
tool. Hazlo solo con cuidado.
|
||||||
|
|
||||||
## Tools (36)
|
## Tools (39)
|
||||||
|
|
||||||
### Sesión (`tools_session.go`)
|
### Sesión (`tools_session.go`)
|
||||||
- `browser_launch` (MUTA) — lanza Chrome con CDP. args: port, headless, user_data_dir, url.
|
- `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_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_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_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
|
### Input (`tools_input.go`) — todas MUTA
|
||||||
- `press_key` — presiona una tecla nombrada (Enter/Tab/Escape/ArrowDown/...). args: port, key.
|
- `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
|
## 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
|
- v0.2.0 (2026-06-06) — P0 LLM-readiness. Seguridad: Chrome aislado por defecto (puerto 9333
|
||||||
+ perfil dedicado `<tmp>/browser_mcp_userdata`), separado del navegador diario en 9222.
|
+ perfil dedicado `<tmp>/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`
|
Nuevas tools: `tab_select` (selección determinista de pestaña por id/URL), `page_get_text`
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import (
|
|||||||
"fn-registry/functions/browser"
|
"fn-registry/functions/browser"
|
||||||
)
|
)
|
||||||
|
|
||||||
const version = "0.1.0"
|
const version = "0.3.0"
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
httpAddr string
|
httpAddr string
|
||||||
|
|||||||
+100
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mark3labs/mcp-go/mcp"
|
"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(domClickHumanTool(), mcp.NewTypedToolHandler(d.handleDomClickHuman))
|
||||||
s.AddTool(domClickTextTool(), mcp.NewTypedToolHandler(d.handleDomClickText))
|
s.AddTool(domClickTextTool(), mcp.NewTypedToolHandler(d.handleDomClickText))
|
||||||
s.AddTool(domTypeTool(), mcp.NewTypedToolHandler(d.handleDomType))
|
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) ----
|
// ---- dom_click (MUTA) ----
|
||||||
|
|
||||||
type domClickArgs struct {
|
type domClickArgs struct {
|
||||||
|
|||||||
+25
-7
@@ -86,16 +86,34 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa
|
|||||||
maxChars = 20000
|
maxChars = 20000
|
||||||
}
|
}
|
||||||
|
|
||||||
root, err := resolveRoot()
|
outline, err := d.perceiveOutlineTab(port, a.TabID, maxChars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("resolve registry root: " + err.Error()), 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 `<root>/fn run cdp_perceive_outline <port> <tabID> <maxChars>` 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 "", fmt.Errorf("resolve registry root: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
tabID := a.TabID
|
|
||||||
if tabID == "" {
|
if tabID == "" {
|
||||||
tabs, err := browser.CdpListTabs("localhost", port)
|
tabs, err := browser.CdpListTabs("localhost", port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return mcp.NewToolResultError("list tabs: " + err.Error()), nil
|
return "", fmt.Errorf("list tabs: %w", err)
|
||||||
}
|
}
|
||||||
for _, t := range tabs {
|
for _, t := range tabs {
|
||||||
if t.Type == "page" {
|
if t.Type == "page" {
|
||||||
@@ -104,7 +122,7 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tabID == "" {
|
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 == "" {
|
if msg == "" {
|
||||||
msg = err.Error()
|
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 ----
|
// ---- page_get_html ----
|
||||||
|
|||||||
Reference in New Issue
Block a user