From 71bc7ab8d88ce16728f659a7826f28c40a35518f Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 6 Jun 2026 15:33:18 +0200 Subject: [PATCH] feat: tool dom_find_ref_by_text (click-by-text por #ref) + mode en click_ref/hover_ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dom_find_ref_by_text usa la nueva CdpFindRefByText del registry: encuentra por texto y devuelve el #ref (backendDOMNodeId) listo para dom_click_ref, sin selector CSS frágil; reporta count para ambigüedad. Incluye WIP pre-existente ya estable: dom_click_ref/dom_hover_ref exponen 'mode' (human/fast/instant) vía MouseProfileForMode. Compila + 9 e2e verdes. --- tools_dom.go | 58 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/tools_dom.go b/tools_dom.go index f68fba5..9dec50d 100644 --- a/tools_dom.go +++ b/tools_dom.go @@ -14,6 +14,7 @@ import ( // 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 { @@ -34,23 +35,24 @@ const settleDelay = 400 * time.Millisecond // ---- dom_click_ref (MUTA) — bucle percibir→actuar ---- type domClickRefArgs struct { - Port int `json:"port"` - Ref int `json:"ref"` + 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 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.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: 'human' (default, Bézier+jitter anti-bot), 'fast' (movimiento reducido, scraping masivo), 'instant' (element.click() JS, sin eventos de ratón; también fallback si el elemento no tiene geometría).")), ) } 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{}) + return browser.CdpClickRef(c, a.Ref, browser.MouseProfileForMode(a.Mode)) }) if err != nil { return mcp.NewToolResultError(err.Error()), nil @@ -97,23 +99,24 @@ func (d *deps) handleDomTypeRef(_ context.Context, _ mcp.CallToolRequest, a domT // ---- dom_hover_ref (MUTA) — bucle percibir→actuar ---- type domHoverRefArgs struct { - Port int `json:"port"` - Ref int `json:"ref"` + 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 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.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: 'human' (default, Bézier+jitter), 'fast' (movimiento reducido), 'instant' (sin movimiento de ratón).")), ) } 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{}) + return browser.CdpHoverRef(c, a.Ref, browser.MouseProfileForMode(a.Mode)) }) if err != nil { return mcp.NewToolResultError(err.Error()), nil @@ -266,6 +269,41 @@ func (d *deps) handleDomFindByText(_ context.Context, _ mcp.CallToolRequest, a d 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 {