Files
browser_mcp/tools_input.go
T
egutierrez fed245a738 feat(browser_mcp): perceive nativo Go, datos de iframe, click XY y screenshot como imagen (v0.6.0)
Capacidades nuevas y cambios (40 -> 42 tools):

- page_perceive ahora se genera de forma NATIVA en Go sobre la conexion CDP
  viva del pool (cdp_get_ax_outline_go_browser). Elimina el subprocess
  `fn run cdp_perceive_outline` (Python), el venv y la dependencia del binario
  `fn` en runtime (se borra resolveRoot/exec.Command). Respeta tab_select.
- page_perceive acepta frame_id para percibir DENTRO de un iframe. El campo
  tab_id queda obsoleto (se ignora; usar tab_select) pero se conserva por
  compatibilidad.
- frame_get_text (nueva, lectura): innerText de un iframe via
  cdp_get_text_in_frame_go_browser. Activa tambien bajo --read-only.
- dom_click_xy (nueva, MUTA): click humanizado por coordenadas absolutas via
  cdp_click_xy_human_go_browser, con mode human/fast/instant y auto-observe.
  Fallback para actuar sobre lo que el LLM ve en page_screenshot.
- page_screenshot devuelve la imagen como image content
  (cdp_screenshot_bytes_go_browser + mcp.NewToolResultImage) para que el LLM
  vea los pixeles; path pasa a ser opcional (si se da, ademas guarda a disco).
- Auto-observe de las tools *_ref sube su truncado de 4000 a 8000 chars.
- Fix de seguridad documental: todas las descripciones del parametro port que
  decian "Default 9222" (navegador diario del usuario) corregidas a
  "Default 9333" (Chrome aislado del MCP). El codigo ya usaba 9333; la doc era
  falsa y podia inducir al modelo a tocar pestanas de banca/correo.

uses_functions del app.md: +cdp_get_ax_outline, +cdp_get_text_in_frame,
+cdp_screenshot_bytes; -cdp_perceive_outline_py_pipelines.

Verificacion: go build OK, go test OK (4 unit pass, 3 e2e skip gated BMCP_E2E=1),
go vet OK, gofmt limpio, sin "Default 9222" en el codigo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 17:35:33 +02:00

113 lines
4.0 KiB
Go

package main
import (
"context"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"fn-registry/functions/browser"
)
// registerInputTools wires press_key, scroll, handle_dialog. All MUTA.
func registerInputTools(s *server.MCPServer, d *deps) {
if d.readOnly {
return
}
s.AddTool(pressKeyTool(), mcp.NewTypedToolHandler(d.handlePressKey))
s.AddTool(scrollTool(), mcp.NewTypedToolHandler(d.handleScroll))
s.AddTool(handleDialogTool(), mcp.NewTypedToolHandler(d.handleHandleDialog))
}
// ---- press_key (MUTA) ----
type pressKeyArgs struct {
Port int `json:"port"`
Key string `json:"key"`
}
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 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.")),
)
}
func (d *deps) handlePressKey(_ context.Context, _ mcp.CallToolRequest, a pressKeyArgs) (*mcp.CallToolResult, error) {
if a.Key == "" {
return mcp.NewToolResultError("key is required"), nil
}
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
return browser.CdpPressKey(c, a.Key)
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText("pressed " + a.Key), nil
}
// ---- scroll (MUTA) ----
type scrollArgs struct {
Port int `json:"port"`
DeltaX float64 `json:"delta_x"`
DeltaY float64 `json:"delta_y"`
}
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 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.")),
)
}
func (d *deps) handleScroll(_ context.Context, _ mcp.CallToolRequest, a scrollArgs) (*mcp.CallToolResult, error) {
deltaY := a.DeltaY
if deltaY == 0 && a.DeltaX == 0 {
deltaY = 300
}
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
return browser.CdpScroll(c, a.DeltaX, deltaY)
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText("scrolled"), nil
}
// ---- handle_dialog (MUTA) ----
type handleDialogArgs struct {
Port int `json:"port"`
Accept bool `json:"accept"`
PromptText string `json:"prompt_text"`
}
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 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.")),
)
}
func (d *deps) handleHandleDialog(_ context.Context, _ mcp.CallToolRequest, a handleDialogArgs) (*mcp.CallToolResult, error) {
port := portOr(a.Port)
c, err := d.pool.get(port)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
cancel, dlog, err := browser.CdpHandleDialog(c, a.Accept, a.PromptText)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
// Guardamos el DialogLog junto al cancel para que browser_disconnect pueda
// reportar cuántos diálogos se auto-respondieron y cuál fue el último.
d.pool.setDialog(port, cancel, dlog)
return mcp.NewToolResultText("dialog auto-handler armed"), nil
}