Merge quick/visual-iframe-tools: perceive nativo + iframe data + click XY + screenshot imagen (v0.6.0)

This commit is contained in:
2026-06-06 17:38:42 +02:00
9 changed files with 214 additions and 146 deletions
+62 -23
View File
@@ -2,8 +2,8 @@
name: browser_mcp
lang: go
domain: infra
version: 0.5.0
description: "Servidor MCP que expone control total del navegador via CDP (40 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, incluyendo find-ref-by-text) 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.6.0
description: "Servidor MCP que expone control total del navegador via CDP (42 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, y screenshot devuelto como image content que el LLM ve) 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
@@ -52,8 +52,10 @@ uses_functions:
- cdp_save_storage_state_go_browser
- cdp_load_storage_state_go_browser
- cdp_get_text_go_browser
- cdp_get_text_in_frame_go_browser
- cdp_connect_target_go_browser
- cdp_perceive_outline_py_pipelines
- cdp_get_ax_outline_go_browser
- cdp_screenshot_bytes_go_browser
- cdp_click_ref_go_browser
- cdp_type_ref_go_browser
- cdp_hover_ref_go_browser
@@ -116,7 +118,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 (39)
## Tools (42)
### Sesión (`tools_session.go`)
- `browser_launch` (MUTA) — lanza Chrome con CDP. args: port, headless, user_data_dir, url.
@@ -144,11 +146,17 @@ podría manipular pestañas ajenas del usuario (banca, correo). Para evitarlo:
— no revienta el contexto. args: port, selector (opcional), max_bytes (default 20000).
- `page_perceive` — outline indentado y accionable del árbol de accesibilidad (roles, nombres,
`#ref`): la forma compacta de que el agente "perciba" la página sin reventar el contexto.
Implementado por subprocess (`fn run cdp_perceive_outline`). Si `tab_id` se omite, usa la
primera pestaña page. args: port, tab_id (opcional), max_chars (default 20000).
**Gotcha:** requiere el binario `fn` y el venv de Python del registry disponibles en runtime.
**Nativo en Go** sobre la conexión CDP viva del pool (`cdp_get_ax_outline_go_browser`) — ya
no lanza subprocess `fn run` ni levanta el venv de Python. Para elegir la pestaña usa `tab_select`
ANTES (la conexión del pool ya está fijada a esa pestaña); el campo `tab_id` queda obsoleto y se
ignora (se conserva por compatibilidad). Si se pasa `frame_id`, percibe DENTRO de ese iframe
(obtén el id con `frame_list`). args: port, tab_id (obsoleto), frame_id (opcional), max_chars (default 20000).
- `page_eval_js` (MUTA) — `Runtime.evaluate`. args: port, expression.
- `page_screenshot` — captura a archivo. args: port, path, full_page.
- `page_screenshot` — captura la página y la **devuelve como image content** para que el LLM vea los
píxeles (vía `cdp_screenshot_bytes_go_browser`, sin tocar disco). Si se pasa `path`, además guarda la
imagen en ese archivo; el image content se devuelve siempre. Útil cuando el outline de `page_perceive`
no basta (canvas, mapas, layouts visuales): mira la captura y actúa con `dom_click_xy`. args: port,
path (opcional), full_page.
### DOM (`tools_dom.go`)
- `dom_click` (MUTA) — click por selector. args: port, selector.
@@ -157,17 +165,23 @@ 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_click_ref` (MUTA) — click humanizado por `#ref` (backendDOMNodeId del outline de `page_perceive`) + auto-observe. args: port, ref, mode.
- `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.
- `dom_hover_ref` (MUTA) — hover humanizado por `#ref` + auto-observe. args: port, ref, mode.
- `dom_click_xy` (MUTA) — fallback de click por coordenadas absolutas (x, y en CSS pixels del viewport) con
movimiento humanizado por defecto. Pensado para usarse sobre lo que el agente VE en `page_screenshot`
cuando el outline no basta (canvas, mapas, layouts visuales); prefiere `dom_click_ref` cuando el elemento
aparece en el outline. Devuelve el outline actualizado (auto-observe). args: port, x, y, mode.
#### 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` devuelve un outline accionable (generado de forma nativa en Go
sobre la conexión CDP viva) 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, truncado a 8000 chars), cerrando el bucle
percibir→actuar:
```
page_perceive → outline con #ref de cada elemento
@@ -175,8 +189,14 @@ 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).
Cuando el elemento no aparece en el outline (canvas, mapas, layouts puramente
visuales), el fallback es **mirar** con `page_screenshot` (que devuelve la imagen
al LLM) y **actuar** por coordenadas con `dom_click_xy`, que también devuelve el
outline tras el click.
Las tools `*_ref` y `dom_click_xy` aceptan `mode` (`human` por defecto con
Bézier+jitter anti-bot, `fast` para scraping masivo, `instant` sin movimiento de
ratón). La humanización es el default en todas para no facilitar la detección.
### Input (`tools_input.go`) — todas MUTA
- `press_key` — presiona una tecla nombrada (Enter/Tab/Escape/ArrowDown/...). args: port, key.
@@ -193,6 +213,9 @@ sesión `fast`/`instant` para scraping masivo está pendiente (ver TODO en el c
- `frame_list` — lista frames con sus IDs. args: port.
- `frame_eval` (MUTA) — evalúa JS dentro de un frame. args: port, frame_id, expression.
- `frame_get_html` — HTML de un frame (truncado a 200000). args: port, frame_id.
- `frame_get_text` — texto visible (innerText) de un iframe, truncado a `max_bytes`. Para leer
contenido atrapado dentro de un iframe — `page_get_text` solo cubre el documento de nivel superior.
args: port, frame_id, max_bytes (default 20000).
### Estado de sesión (`tools_storage.go`)
- `storage_save` — guarda cookies + localStorage a JSON. args: port, path.
@@ -218,11 +241,11 @@ Transporte HTTP (Streamable HTTP):
### Flag `--read-only`
Con `--read-only`, el servidor NO registra las tools mutantes (marcadas MUTA arriba):
solo expone las 17 tools de lectura/control (`browser_connect`, `browser_disconnect`, `tab_list`,
solo expone las 19 tools de lectura/control (`browser_connect`, `browser_disconnect`, `tab_list`,
`tab_activate`, `tab_select`, `page_wait_load`, `page_wait_idle`, `page_get_html`, `page_get_text`,
`page_perceive`, `page_screenshot`, `dom_find_by_text`, `dom_wait_element`, `cookie_get`,
`frame_list`, `frame_get_html`, `storage_save`). Útil para sesiones de inspección sin riesgo de
modificar el estado del navegador.
`page_perceive`, `page_screenshot`, `dom_find_by_text`, `dom_find_ref_by_text`, `dom_wait_element`,
`cookie_get`, `frame_list`, `frame_get_html`, `frame_get_text`, `storage_save`). Útil para sesiones
de inspección sin riesgo de modificar el estado del navegador.
## Omitido en v1
@@ -232,8 +255,10 @@ Funciones del dominio `browser` que NO se exponen como tools en esta versión, c
larga duración (registrar handlers + un punto de "stop" que devuelve los datos
acumulados); no encaja en el modelo request/response de una tool MCP simple. Pendiente
de un diseño con tool de start + tool de stop.
- **`cdp_get_ax_tree`** — ya expuesto desde v0.2.0 via la tool `page_perceive`, que invoca
el pipeline `cdp_perceive_outline` por subprocess (`fn run`) en vez de duplicar la lógica aquí.
- **`cdp_get_ax_tree`** — expuesto via la tool `page_perceive`. Desde v0.6.0 el outline se genera
de forma **nativa en Go** (`cdp_get_ax_outline_go_browser`) sobre la conexión CDP viva del pool;
ya no se invoca el pipeline Python `cdp_perceive_outline` por subprocess (`fn run`). El acceso al
árbol AX en bruto sigue sin exponerse: la tool devuelve directamente el outline accionable.
- **Funciones de perfiles Chrome (Bash: create/delete/appearance/reset)** — requieren que
Chrome esté CERRADO para modificar el `Local State` / `Preferences` del perfil; son
incompatibles con un MCP cuyo propósito es controlar un Chrome vivo. Quedan disponibles
@@ -241,6 +266,20 @@ Funciones del dominio `browser` que NO se exponen como tools en esta versión, c
## Capability growth log
- v0.6.0 (2026-06-06) — Percepción visual y de iframes + perceive nativo. (1) `page_perceive` se
generó hasta ahora por subprocess `fn run cdp_perceive_outline` (Python); ahora es **nativo en Go**
sobre la conexión CDP viva del pool (`cdp_get_ax_outline_go_browser`) — mata el subprocess, el venv
y la dependencia del binario `fn` en runtime (se eliminó `resolveRoot`/`exec.Command`). (2) Acceso a
datos dentro de iframes: nueva tool `frame_get_text` (innerText de un iframe, `cdp_get_text_in_frame_go_browser`)
y nuevo parámetro `frame_id` en `page_perceive` para percibir DENTRO de un iframe. (3) Click por
coordenadas absolutas: nueva tool `dom_click_xy` (`cdp_click_xy_human_go_browser`), humanizada por
defecto, pensada para actuar sobre lo que el LLM ve en una captura. (4) `page_screenshot` ahora
**devuelve la imagen como image content** (vía `cdp_screenshot_bytes_go_browser` + `mcp.NewToolResultImage`)
para que el LLM vea los píxeles; `path` pasa a ser opcional (si se da, además guarda a disco). (5) El
auto-observe de las tools `*_ref` subió su truncado de 4000 a 8000 chars (outlines grandes se cortaban).
(6) Fix de seguridad documental: todas las descripciones del parámetro `port` que decían "Default 9222"
(el navegador diario del usuario) corregidas a "Default 9333" (Chrome aislado del MCP); el código ya
usaba 9333, la doc era falsa y podía inducir al modelo a tocar pestañas de banca/correo. 40 → 42 tools.
- v0.5.0 (2026-06-06) — Fix del leak de RAM (chromium huérfanos, apagón 06/06/2026). El pool
ahora registra el PID del Chrome que lanzó por puerto (`pids` map + setPID/getPID/clearPID/
launchedCount). `browser_disconnect` (drop) y el shutdown (closeAll) matan el grupo de proceso
+1 -27
View File
@@ -7,7 +7,6 @@ import (
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
@@ -16,7 +15,7 @@ import (
"fn-registry/functions/browser"
)
const version = "0.3.0"
const version = "0.6.0"
type config struct {
httpAddr string
@@ -198,28 +197,3 @@ func truncate(s string, n int) string {
}
return s[:n] + "\n... [truncated]"
}
// resolveRoot finds the fn_registry root so we can locate the `fn` binary and
// the Python venv at runtime. Mirrors registry_mcp's resolveRoot: honors
// FN_REGISTRY_ROOT, otherwise walks up from cwd looking for registry.db.
func resolveRoot() (string, error) {
if env := os.Getenv("FN_REGISTRY_ROOT"); env != "" {
return filepath.Abs(env)
}
cwd, err := os.Getwd()
if err != nil {
return "", err
}
dir := cwd
for {
if _, err := os.Stat(filepath.Join(dir, "registry.db")); err == nil {
return dir, nil
}
parent := filepath.Dir(dir)
if parent == dir {
break
}
dir = parent
}
return "", fmt.Errorf("registry.db not found upward from %s", cwd)
}
+4 -4
View File
@@ -30,7 +30,7 @@ type cookieGetArgs struct {
func cookieGetTool() mcp.Tool {
return mcp.NewTool("cookie_get",
mcp.WithDescription("Return all browser cookies (Network.getAllCookies) as JSON."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
@@ -62,7 +62,7 @@ type cookieSetArgs struct {
func cookieSetTool() mcp.Tool {
return mcp.NewTool("cookie_set",
mcp.WithDescription("Set a cookie via Network.setCookie."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("name", mcp.Required(), mcp.Description("Cookie name.")),
mcp.WithString("value", mcp.Description("Cookie value.")),
mcp.WithString("domain", mcp.Required(), mcp.Description("Cookie domain.")),
@@ -102,7 +102,7 @@ type cookieDeleteArgs struct {
func cookieDeleteTool() mcp.Tool {
return mcp.NewTool("cookie_delete",
mcp.WithDescription("Delete cookies by name (optionally scoped to a domain) via Network.deleteCookies."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("name", mcp.Required(), mcp.Description("Cookie name to delete.")),
mcp.WithString("domain", mcp.Description("Optional domain scope.")),
)
@@ -130,7 +130,7 @@ type cookieClearArgs struct {
func cookieClearTool() mcp.Tool {
return mcp.NewTool("cookie_clear",
mcp.WithDescription("Clear all browser cookies via Network.clearBrowserCookies."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
+42 -9
View File
@@ -25,6 +25,7 @@ func registerDomTools(s *server.MCPServer, d *deps) {
s.AddTool(domClickRefTool(), mcp.NewTypedToolHandler(d.handleDomClickRef))
s.AddTool(domTypeRefTool(), mcp.NewTypedToolHandler(d.handleDomTypeRef))
s.AddTool(domHoverRefTool(), mcp.NewTypedToolHandler(d.handleDomHoverRef))
s.AddTool(domClickXYTool(), mcp.NewTypedToolHandler(d.handleDomClickXY))
}
}
@@ -58,7 +59,7 @@ func (d *deps) handleDomClickRef(_ context.Context, _ mcp.CallToolRequest, a dom
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
outline, _ := d.perceiveOutline(port, 4000)
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText("clicked ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
}
@@ -92,7 +93,7 @@ func (d *deps) handleDomTypeRef(_ context.Context, _ mcp.CallToolRequest, a domT
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
outline, _ := d.perceiveOutline(port, 4000)
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText("typed into ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
}
@@ -122,10 +123,42 @@ func (d *deps) handleDomHoverRef(_ context.Context, _ mcp.CallToolRequest, a dom
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
outline, _ := d.perceiveOutline(port, 4000)
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText("hovered ref " + fmt.Sprint(a.Ref) + "\n\n" + outline), nil
}
// ---- dom_click_xy (MUTA) — click humanizado por coordenadas absolutas ----
type domClickXYArgs struct {
Port int `json:"port"`
X float64 `json:"x"`
Y float64 `json:"y"`
Mode string `json:"mode"`
}
func domClickXYTool() mcp.Tool {
return mcp.NewTool("dom_click_xy",
mcp.WithDescription("Fallback de click por coordenadas absolutas (x, y) en CSS pixels del viewport, con movimiento de ratón humanizado por defecto. Pensado para usarse sobre lo que el agente VE en page_screenshot cuando el outline de page_perceive no basta (canvas, mapas, layouts visuales). Prefiere dom_click_ref cuando el elemento aparece en el outline. 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("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).")),
)
}
func (d *deps) handleDomClickXY(_ context.Context, _ mcp.CallToolRequest, a domClickXYArgs) (*mcp.CallToolResult, error) {
port := portOr(a.Port)
err := d.withConn(port, func(c *browser.CDPConn) error {
return browser.CdpClickXYHuman(c, a.X, a.Y, browser.MouseProfileForMode(a.Mode))
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
time.Sleep(settleDelay)
outline, _ := d.perceiveOutline(port, 8000)
return mcp.NewToolResultText(fmt.Sprintf("clicked at (%g, %g)\n\n%s", a.X, a.Y, outline)), nil
}
// ---- dom_click (MUTA) ----
type domClickArgs struct {
@@ -136,7 +169,7 @@ type domClickArgs struct {
func domClickTool() mcp.Tool {
return mcp.NewTool("dom_click",
mcp.WithDescription("Click the element matching the CSS selector (synthetic CDP click)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("selector", mcp.Required(), mcp.Description("CSS selector of the element to click.")),
)
}
@@ -164,7 +197,7 @@ type domClickHumanArgs struct {
func domClickHumanTool() mcp.Tool {
return mcp.NewTool("dom_click_human",
mcp.WithDescription("Click the element matching the CSS selector with human-like mouse movement (Bézier path + jitter + press/release pause)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("selector", mcp.Required(), mcp.Description("CSS selector of the element to click.")),
)
}
@@ -192,7 +225,7 @@ type domClickTextArgs struct {
func domClickTextTool() mcp.Tool {
return mcp.NewTool("dom_click_text",
mcp.WithDescription("Find the first element whose visible text matches and click it."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("text", mcp.Required(), mcp.Description("Visible text to match (substring).")),
)
}
@@ -220,7 +253,7 @@ type domTypeArgs struct {
func domTypeTool() mcp.Tool {
return mcp.NewTool("dom_type",
mcp.WithDescription("Type text into the currently focused element (dispatches key events char by char)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("text", mcp.Required(), mcp.Description("Text to type.")),
)
}
@@ -248,7 +281,7 @@ type domFindByTextArgs struct {
func domFindByTextTool() mcp.Tool {
return mcp.NewTool("dom_find_by_text",
mcp.WithDescription("Find the first element whose visible text matches and return a unique CSS selector for it (empty string if none)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("text", mcp.Required(), mcp.Description("Visible text to match (substring).")),
)
}
@@ -315,7 +348,7 @@ type domWaitElementArgs struct {
func domWaitElementTool() mcp.Tool {
return mcp.NewTool("dom_wait_element",
mcp.WithDescription("Block until an element matching the CSS selector appears in the DOM (or timeout)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("selector", mcp.Required(), mcp.Description("CSS selector to wait for.")),
mcp.WithNumber("timeout_ms", mcp.Description("Max wait in ms. Default 10000.")),
)
+43 -4
View File
@@ -10,10 +10,12 @@ import (
"fn-registry/functions/browser"
)
// registerFrameTools wires frame_list + frame_get_html (read) and frame_eval (MUTA).
// registerFrameTools wires frame_list + frame_get_html + frame_get_text (read)
// and frame_eval (MUTA).
func registerFrameTools(s *server.MCPServer, d *deps) {
s.AddTool(frameListTool(), mcp.NewTypedToolHandler(d.handleFrameList))
s.AddTool(frameGetHTMLTool(), mcp.NewTypedToolHandler(d.handleFrameGetHTML))
s.AddTool(frameGetTextTool(), mcp.NewTypedToolHandler(d.handleFrameGetText))
if !d.readOnly {
s.AddTool(frameEvalTool(), mcp.NewTypedToolHandler(d.handleFrameEval))
@@ -29,7 +31,7 @@ type frameListArgs struct {
func frameListTool() mcp.Tool {
return mcp.NewTool("frame_list",
mcp.WithDescription("List all frames (including iframes) of the current page via Page.getFrameTree. Returns JSON with frame IDs."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
@@ -58,7 +60,7 @@ type frameEvalArgs struct {
func frameEvalTool() mcp.Tool {
return mcp.NewTool("frame_eval",
mcp.WithDescription("Evaluate a JavaScript expression inside a specific frame's execution context. Returns the stringified result."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("frame_id", mcp.Required(), mcp.Description("Frame ID (from frame_list).")),
mcp.WithString("expression", mcp.Required(), mcp.Description("JavaScript expression to evaluate.")),
)
@@ -93,7 +95,7 @@ type frameGetHTMLArgs struct {
func frameGetHTMLTool() mcp.Tool {
return mcp.NewTool("frame_get_html",
mcp.WithDescription("Return the serialized HTML of a specific frame. Truncated to 200000 chars."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("frame_id", mcp.Required(), mcp.Description("Frame ID (from frame_list).")),
)
}
@@ -113,3 +115,40 @@ func (d *deps) handleFrameGetHTML(_ context.Context, _ mcp.CallToolRequest, a fr
}
return mcp.NewToolResultText(truncate(html, htmlMax)), nil
}
// ---- frame_get_text ----
type frameGetTextArgs struct {
Port int `json:"port"`
FrameID string `json:"frame_id"`
MaxBytes int `json:"max_bytes"`
}
func frameGetTextTool() mcp.Tool {
return mcp.NewTool("frame_get_text",
mcp.WithDescription("Return the visible text (innerText) of a specific iframe, truncated to max_bytes. Use this to read content trapped inside an iframe — page_get_text only covers the top-level document. Get the frame_id from frame_list."),
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("frame_id", mcp.Required(), mcp.Description("Frame ID (from frame_list).")),
mcp.WithNumber("max_bytes", mcp.Description("Máximo de bytes a devolver. Default 20000. 0 = sin límite.")),
)
}
func (d *deps) handleFrameGetText(_ context.Context, _ mcp.CallToolRequest, a frameGetTextArgs) (*mcp.CallToolResult, error) {
if a.FrameID == "" {
return mcp.NewToolResultError("frame_id is required"), nil
}
maxBytes := a.MaxBytes
if maxBytes == 0 {
maxBytes = 20000
}
var text string
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
var e error
text, e = browser.CdpGetTextInFrame(c, a.FrameID, maxBytes)
return e
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(text), nil
}
+3 -3
View File
@@ -29,7 +29,7 @@ type pressKeyArgs struct {
func pressKeyTool() mcp.Tool {
return mcp.NewTool("press_key",
mcp.WithDescription("Press a named key (Enter, Tab, Escape, ArrowDown, Backspace, ...) on the focused element."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("key", mcp.Required(), mcp.Description("Key name, e.g. Enter, Tab, Escape, ArrowDown.")),
)
}
@@ -58,7 +58,7 @@ type scrollArgs struct {
func scrollTool() mcp.Tool {
return mcp.NewTool("scroll",
mcp.WithDescription("Scroll the page by (delta_x, delta_y) pixels via a synthetic mouse wheel event."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("delta_x", mcp.Description("Horizontal scroll delta in pixels. Default 0.")),
mcp.WithNumber("delta_y", mcp.Description("Vertical scroll delta in pixels. Default 300.")),
)
@@ -89,7 +89,7 @@ type handleDialogArgs struct {
func handleDialogTool() mcp.Tool {
return mcp.NewTool("handle_dialog",
mcp.WithDescription("Arm an auto-handler that responds to every JS dialog (alert/confirm/prompt/beforeunload) on the tab until disconnect. The handler lives in the pooled connection."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
mcp.WithBoolean("accept", mcp.DefaultBool(true), mcp.Description("Whether to accept (true) or dismiss (false) dialogs. Default true.")),
mcp.WithString("prompt_text", mcp.Description("Text to enter for prompt() dialogs.")),
)
+9 -9
View File
@@ -39,7 +39,7 @@ type tabNavigateArgs struct {
func tabNavigateTool() mcp.Tool {
return mcp.NewTool("tab_navigate",
mcp.WithDescription("Navigate the connected tab to a URL via Page.navigate."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("url", mcp.Required(), mcp.Description("Target URL.")),
)
}
@@ -66,7 +66,7 @@ type tabListArgs struct {
func tabListTool() mcp.Tool {
return mcp.NewTool("tab_list",
mcp.WithDescription("List all CDP targets (tabs, iframes, workers) via GET /json. Returns JSON."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
@@ -89,7 +89,7 @@ type tabNewArgs struct {
func tabNewTool() mcp.Tool {
return mcp.NewTool("tab_new",
mcp.WithDescription("Open a new tab via PUT /json/new. Returns the new tab's JSON."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("url", mcp.Description("Optional start URL. Empty = about:blank.")),
)
}
@@ -113,7 +113,7 @@ type tabCloseArgs struct {
func tabCloseTool() mcp.Tool {
return mcp.NewTool("tab_close",
mcp.WithDescription("Close a tab by its target ID via GET /json/close/<id>."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("tab_id", mcp.Required(), mcp.Description("Target ID of the tab to close.")),
)
}
@@ -138,7 +138,7 @@ type tabActivateArgs struct {
func tabActivateTool() mcp.Tool {
return mcp.NewTool("tab_activate",
mcp.WithDescription("Bring a tab to the foreground via GET /json/activate/<id>."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("tab_id", mcp.Required(), mcp.Description("Target ID of the tab to activate.")),
)
}
@@ -184,7 +184,7 @@ type navBackArgs struct {
func navBackTool() mcp.Tool {
return mcp.NewTool("nav_back",
mcp.WithDescription("Navigate back in the connected tab's history."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
@@ -207,7 +207,7 @@ type navForwardArgs struct {
func navForwardTool() mcp.Tool {
return mcp.NewTool("nav_forward",
mcp.WithDescription("Navigate forward in the connected tab's history."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
@@ -231,7 +231,7 @@ type pageWaitLoadArgs struct {
func pageWaitLoadTool() mcp.Tool {
return mcp.NewTool("page_wait_load",
mcp.WithDescription("Block until the page fires the load event (or timeout)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("timeout_ms", mcp.Description("Max wait in ms. Default 10000.")),
)
}
@@ -260,7 +260,7 @@ type pageWaitIdleArgs struct {
func pageWaitIdleTool() mcp.Tool {
return mcp.NewTool("page_wait_idle",
mcp.WithDescription("Block until network activity quiets down (inflight requests reach 0 for a quiet window) or timeout. Immune to DOM-mutating extensions/animations."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("timeout_ms", mcp.Description("Max wait in ms. Default 15000.")),
)
}
+48 -65
View File
@@ -2,10 +2,8 @@ package main
import (
"context"
"fmt"
"os/exec"
"path/filepath"
"strings"
"encoding/base64"
"os"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
@@ -67,14 +65,16 @@ func (d *deps) handlePageGetText(_ context.Context, _ mcp.CallToolRequest, a pag
type pagePerceiveArgs struct {
Port int `json:"port"`
TabID string `json:"tab_id"`
FrameID string `json:"frame_id"`
MaxChars int `json:"max_chars"`
}
func pagePerceiveTool() mcp.Tool {
return mcp.NewTool("page_perceive",
mcp.WithDescription("Devuelve un outline indentado y accionable del árbol de accesibilidad (roles, nombres, #ref) — la forma compacta de que el agente 'perciba' la página sin reventar el contexto. Si tab_id se omite, usa la primera pestaña page. Gotcha: requiere el binario `fn` y el venv de Python del registry disponibles en runtime."),
mcp.WithDescription("Devuelve un outline indentado y accionable del árbol de accesibilidad (roles, nombres, #ref) — la forma compacta de que el agente 'perciba' la página sin reventar el contexto. Generado de forma nativa en Go sobre la conexión CDP viva (sin subprocess ni Python). Para elegir la pestaña, usa tab_select ANTES de percibir (la conexión del pool ya está fijada a esa pestaña). Si frame_id se pasa, percibe DENTRO de ese iframe (obtén el id con frame_list)."),
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("tab_id", mcp.Description("Target id de la pestaña. Vacío = primera pestaña page.")),
mcp.WithString("tab_id", mcp.Description("OBSOLETO: la conexión del pool ya está fijada a una pestaña vía tab_select. Para elegir pestaña usa tab_select primero; este campo se conserva por compatibilidad y se ignora.")),
mcp.WithString("frame_id", mcp.Description("Frame ID (de frame_list) para percibir DENTRO de ese iframe. Vacío = página entera.")),
mcp.WithNumber("max_chars", mcp.Description("Máximo de chars del outline. Default 20000.")),
)
}
@@ -86,66 +86,36 @@ func (d *deps) handlePagePerceive(_ context.Context, _ mcp.CallToolRequest, a pa
maxChars = 20000
}
outline, err := d.perceiveOutlineTab(port, a.TabID, maxChars)
outline, err := d.perceiveOutlineFrame(port, a.FrameID, 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.
// perceiveOutline genera el outline AX accionable de la página entera sobre la
// conexión viva del pool (sin subprocess). Lo usan los auto-observe de las tools
// *_ref tras una acción.
func (d *deps) perceiveOutline(port, maxChars int) (string, error) {
return d.perceiveOutlineTab(port, "", maxChars)
return d.perceiveOutlineFrame(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()
// perceiveOutlineFrame genera el outline AX accionable de forma NATIVA en Go,
// reusando la conexión CDP viva del pool (browser.CdpGetAXOutline). Si frameID
// != "", percibe DENTRO de ese iframe; frameID == "" = página entera. No lanza
// subprocess `fn run` ni levanta el venv de Python — la lógica de poda y render
// del árbol de accesibilidad vive en la función del registry.
func (d *deps) perceiveOutlineFrame(port int, frameID string, maxChars int) (string, error) {
var outline string
err := d.withConn(port, func(c *browser.CDPConn) error {
var e error
outline, e = browser.CdpGetAXOutline(c, frameID, maxChars)
return e
})
if err != nil {
return "", fmt.Errorf("resolve registry root: %w", err)
return "", err
}
if tabID == "" {
tabs, err := browser.CdpListTabs("localhost", port)
if err != nil {
return "", fmt.Errorf("list tabs: %w", err)
}
for _, t := range tabs {
if t.Type == "page" {
tabID = t.ID
break
}
}
if tabID == "" {
return "", fmt.Errorf("no 'page' tab found on port %d", port)
}
}
// `fn run` pasa los argumentos POSICIONALMENTE a la función del pipeline
// (no como flags argparse): el orden debe coincidir con la firma
// cdp_perceive_outline(debug_port, tab_id, max_chars).
cmd := exec.Command(filepath.Join(root, "fn"), "run", "cdp_perceive_outline",
fmt.Sprint(port),
tabID,
fmt.Sprint(maxChars),
)
cmd.Dir = root
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
msg := strings.TrimSpace(stderr.String())
if msg == "" {
msg = err.Error()
}
return "", fmt.Errorf("cdp_perceive_outline failed: %s", msg)
}
return truncate(stdout.String(), htmlMax), nil
return truncate(outline, htmlMax), nil
}
// ---- page_get_html ----
@@ -157,7 +127,7 @@ type pageGetHTMLArgs struct {
func pageGetHTMLTool() mcp.Tool {
return mcp.NewTool("page_get_html",
mcp.WithDescription("Return the current page's full serialized HTML (outerHTML). Truncated to 200000 chars."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")),
)
}
@@ -184,7 +154,7 @@ type pageEvalJSArgs struct {
func pageEvalJSTool() mcp.Tool {
return mcp.NewTool("page_eval_js",
mcp.WithDescription("Evaluate a JavaScript expression in the page context via Runtime.evaluate. Returns the stringified result."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("expression", mcp.Required(), mcp.Description("JavaScript expression to evaluate.")),
)
}
@@ -215,23 +185,36 @@ type pageScreenshotArgs struct {
func pageScreenshotTool() mcp.Tool {
return mcp.NewTool("page_screenshot",
mcp.WithDescription("Capture a screenshot of the current page and write it to a local path (.png/.jpg)."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
mcp.WithString("path", mcp.Required(), mcp.Description("Output file path (.png or .jpg).")),
mcp.WithDescription("Capture a screenshot of the current page and return it as image content so the LLM can actually see the pixels. Optionally also writes it to a local path. Use this when the accessibility outline (page_perceive) is not enough — e.g. canvas/visual layouts — then act with dom_click_xy over what you see."),
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("path", mcp.Description("Optional output file path (.png or .jpg). If given, the image is ALSO saved to disk; the image content is always returned regardless.")),
mcp.WithBoolean("full_page", mcp.Description("Capture the full scroll height instead of just the viewport.")),
)
}
func (d *deps) handlePageScreenshot(_ context.Context, _ mcp.CallToolRequest, a pageScreenshotArgs) (*mcp.CallToolResult, error) {
if a.Path == "" {
return mcp.NewToolResultError("path is required"), nil
}
opts := browser.CdpScreenshotOpts{FullPage: a.FullPage}
var data []byte
var mimeType string
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
return browser.CdpScreenshot(c, a.Path, opts)
var e error
data, mimeType, e = browser.CdpScreenshotBytes(c, opts)
return e
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText("screenshot saved to " + a.Path), nil
text := "screenshot captured"
// Si se pidió un path, persistimos además los bytes capturados (mismo origen
// que la imagen devuelta al LLM, así no se captura dos veces).
if a.Path != "" {
if e := os.WriteFile(a.Path, data, 0o644); e != nil {
return mcp.NewToolResultError("saving screenshot to " + a.Path + ": " + e.Error()), nil
}
text = "screenshot saved to " + a.Path
}
b64 := base64.StdEncoding.EncodeToString(data)
return mcp.NewToolResultImage(text, b64, mimeType), nil
}
+2 -2
View File
@@ -28,7 +28,7 @@ type storageSaveArgs struct {
func storageSaveTool() mcp.Tool {
return mcp.NewTool("storage_save",
mcp.WithDescription("Save the current session storage state (cookies + localStorage) to a JSON file for later reuse."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("path", mcp.Required(), mcp.Description("Output JSON file path.")),
)
}
@@ -56,7 +56,7 @@ type storageLoadArgs struct {
func storageLoadTool() mcp.Tool {
return mcp.NewTool("storage_load",
mcp.WithDescription("Load a previously saved session storage state (cookies + localStorage) from a JSON file into the live browser."),
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
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("path", mcp.Required(), mcp.Description("Input JSON file path.")),
)
}