fa1efe6fd5
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.
423 lines
17 KiB
Go
423 lines
17 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/mark3labs/mcp-go/mcp"
|
|
"github.com/mark3labs/mcp-go/server"
|
|
|
|
"fn-registry/functions/browser"
|
|
)
|
|
|
|
// registerDomTools wires DOM interaction tools. find/wait stay on under --read-only.
|
|
func registerDomTools(s *server.MCPServer, d *deps) {
|
|
s.AddTool(domFindByTextTool(), mcp.NewTypedToolHandler(d.handleDomFindByText))
|
|
s.AddTool(domFindRefByTextTool(), mcp.NewTypedToolHandler(d.handleDomFindRefByText))
|
|
s.AddTool(domWaitElementTool(), mcp.NewTypedToolHandler(d.handleDomWaitElement))
|
|
|
|
if !d.readOnly {
|
|
s.AddTool(domClickTool(), mcp.NewTypedToolHandler(d.handleDomClick))
|
|
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))
|
|
s.AddTool(domClickXYTool(), mcp.NewTypedToolHandler(d.handleDomClickXY))
|
|
}
|
|
}
|
|
|
|
// 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 ----
|
|
|
|
type domClickRefArgs struct {
|
|
Port int `json:"port"`
|
|
Ref int `json:"ref"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
func domClickRefTool() mcp.Tool {
|
|
return mcp.NewTool("dom_click_ref",
|
|
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: '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(mode))
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
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
|
|
}
|
|
|
|
// ---- dom_type_ref (MUTA) — bucle percibir→actuar ----
|
|
|
|
type domTypeRefArgs struct {
|
|
Port int `json:"port"`
|
|
Ref int `json:"ref"`
|
|
Text string `json:"text"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
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.")),
|
|
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'.")),
|
|
)
|
|
}
|
|
|
|
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)
|
|
mode := d.effectiveMode(port, a.Mode)
|
|
err := d.withConn(port, func(c *browser.CDPConn) error {
|
|
// 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
|
|
}
|
|
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
|
|
}
|
|
|
|
// ---- dom_hover_ref (MUTA) — bucle percibir→actuar ----
|
|
|
|
type domHoverRefArgs struct {
|
|
Port int `json:"port"`
|
|
Ref int `json:"ref"`
|
|
Mode string `json:"mode"`
|
|
}
|
|
|
|
func domHoverRefTool() mcp.Tool {
|
|
return mcp.NewTool("dom_hover_ref",
|
|
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: '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(mode))
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
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
|
|
}
|
|
|
|
// ---- 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: '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(mode))
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
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
|
|
}
|
|
|
|
// ---- dom_click (MUTA) ----
|
|
|
|
type domClickArgs struct {
|
|
Port int `json:"port"`
|
|
Selector string `json:"selector"`
|
|
}
|
|
|
|
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 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.")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomClick(_ context.Context, _ mcp.CallToolRequest, a domClickArgs) (*mcp.CallToolResult, error) {
|
|
if a.Selector == "" {
|
|
return mcp.NewToolResultError("selector is required"), nil
|
|
}
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
return browser.CdpClick(c, a.Selector)
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return mcp.NewToolResultText("clicked " + a.Selector), nil
|
|
}
|
|
|
|
// ---- dom_click_human (MUTA) ----
|
|
|
|
type domClickHumanArgs struct {
|
|
Port int `json:"port"`
|
|
Selector string `json:"selector"`
|
|
}
|
|
|
|
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 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.")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomClickHuman(_ context.Context, _ mcp.CallToolRequest, a domClickHumanArgs) (*mcp.CallToolResult, error) {
|
|
if a.Selector == "" {
|
|
return mcp.NewToolResultError("selector is required"), nil
|
|
}
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
return browser.CdpClickHuman(c, a.Selector, browser.MouseHumanOpts{})
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return mcp.NewToolResultText("clicked (human) " + a.Selector), nil
|
|
}
|
|
|
|
// ---- dom_click_text (MUTA) ----
|
|
|
|
type domClickTextArgs struct {
|
|
Port int `json:"port"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
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 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).")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomClickText(_ context.Context, _ mcp.CallToolRequest, a domClickTextArgs) (*mcp.CallToolResult, error) {
|
|
if a.Text == "" {
|
|
return mcp.NewToolResultError("text is required"), nil
|
|
}
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
return browser.CdpClickText(c, a.Text, browser.FindByTextOpts{})
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return mcp.NewToolResultText("clicked text " + a.Text), nil
|
|
}
|
|
|
|
// ---- dom_type (MUTA) ----
|
|
|
|
type domTypeArgs struct {
|
|
Port int `json:"port"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
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 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.")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomType(_ context.Context, _ mcp.CallToolRequest, a domTypeArgs) (*mcp.CallToolResult, error) {
|
|
if a.Text == "" {
|
|
return mcp.NewToolResultError("text is required"), nil
|
|
}
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
return browser.CdpTypeText(c, a.Text)
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return mcp.NewToolResultText("typed text"), nil
|
|
}
|
|
|
|
// ---- dom_find_by_text ----
|
|
|
|
type domFindByTextArgs struct {
|
|
Port int `json:"port"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
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 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).")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomFindByText(_ context.Context, _ mcp.CallToolRequest, a domFindByTextArgs) (*mcp.CallToolResult, error) {
|
|
if a.Text == "" {
|
|
return mcp.NewToolResultError("text is required"), nil
|
|
}
|
|
var sel string
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
var e error
|
|
sel, e = browser.CdpFindByText(c, a.Text, browser.FindByTextOpts{})
|
|
return e
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return mcp.NewToolResultText(sel), nil
|
|
}
|
|
|
|
// ---- dom_find_ref_by_text ----
|
|
|
|
type domFindRefByTextArgs struct {
|
|
Port int `json:"port"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
func domFindRefByTextTool() mcp.Tool {
|
|
return mcp.NewTool("dom_find_ref_by_text",
|
|
mcp.WithDescription("Find the first element whose visible text matches and return its #ref (backendDOMNodeId) ready for dom_click_ref/dom_hover_ref — no fragile CSS selector. Also reports how many elements match (count>1 = ambiguous)."),
|
|
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).")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomFindRefByText(_ context.Context, _ mcp.CallToolRequest, a domFindRefByTextArgs) (*mcp.CallToolResult, error) {
|
|
if a.Text == "" {
|
|
return mcp.NewToolResultError("text is required"), nil
|
|
}
|
|
var ref, count int
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
var e error
|
|
ref, count, e = browser.CdpFindRefByText(c, a.Text, browser.FindByTextOpts{})
|
|
return e
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
msg := fmt.Sprintf("ref=%d count=%d", ref, count)
|
|
if count > 1 {
|
|
msg += " (ambiguous: returning the first match; refine the text to disambiguate)"
|
|
}
|
|
return mcp.NewToolResultText(msg), nil
|
|
}
|
|
|
|
// ---- dom_wait_element ----
|
|
|
|
type domWaitElementArgs struct {
|
|
Port int `json:"port"`
|
|
Selector string `json:"selector"`
|
|
TimeoutMs int `json:"timeout_ms"`
|
|
}
|
|
|
|
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 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.")),
|
|
)
|
|
}
|
|
|
|
func (d *deps) handleDomWaitElement(_ context.Context, _ mcp.CallToolRequest, a domWaitElementArgs) (*mcp.CallToolResult, error) {
|
|
if a.Selector == "" {
|
|
return mcp.NewToolResultError("selector is required"), nil
|
|
}
|
|
timeout := a.TimeoutMs
|
|
if timeout <= 0 {
|
|
timeout = 10000
|
|
}
|
|
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
|
return browser.CdpWaitElement(c, a.Selector, time.Duration(timeout)*time.Millisecond)
|
|
})
|
|
if err != nil {
|
|
return mcp.NewToolResultError(err.Error()), nil
|
|
}
|
|
return mcp.NewToolResultText("element appeared: " + a.Selector), nil
|
|
}
|