merge: adaptación a fixes del registry browser (handle_dialog + find_ref_by_text)
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
name: browser_mcp
|
||||
lang: go
|
||||
domain: infra
|
||||
version: 0.3.0
|
||||
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."
|
||||
version: 0.4.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."
|
||||
tags: [mcp, browser, cdp, automation, scraping]
|
||||
uses_functions:
|
||||
- chrome_launch_go_browser
|
||||
@@ -26,6 +26,7 @@ uses_functions:
|
||||
- cdp_click_text_go_browser
|
||||
- cdp_type_text_go_browser
|
||||
- cdp_find_by_text_go_browser
|
||||
- cdp_find_ref_by_text_go_browser
|
||||
- cdp_wait_element_go_browser
|
||||
- cdp_press_key_go_browser
|
||||
- cdp_scroll_go_browser
|
||||
|
||||
@@ -11,13 +11,18 @@ import (
|
||||
// Una conexión = una sesión viva a una tab "page". Mantenerla evita pagar el
|
||||
// handshake WebSocket en cada tool y preserva estado (event handlers, contexto).
|
||||
type connPool struct {
|
||||
mu sync.Mutex
|
||||
conns map[int]*browser.CDPConn
|
||||
cancels map[int]func() // cancels de handlers persistentes (handle_dialog)
|
||||
mu sync.Mutex
|
||||
conns map[int]*browser.CDPConn
|
||||
cancels map[int]func() // cancels de handlers persistentes (handle_dialog)
|
||||
dialogLogs map[int]*browser.DialogLog // log de diálogos auto-respondidos por puerto
|
||||
}
|
||||
|
||||
func newConnPool() *connPool {
|
||||
return &connPool{conns: map[int]*browser.CDPConn{}, cancels: map[int]func(){}}
|
||||
return &connPool{
|
||||
conns: map[int]*browser.CDPConn{},
|
||||
cancels: map[int]func(){},
|
||||
dialogLogs: map[int]*browser.DialogLog{},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *connPool) get(port int) (*browser.CDPConn, error) {
|
||||
@@ -41,8 +46,11 @@ func (p *connPool) drop(port int) {
|
||||
cancel()
|
||||
delete(p.cancels, port)
|
||||
}
|
||||
delete(p.dialogLogs, port)
|
||||
if c, ok := p.conns[port]; ok && c != nil {
|
||||
_ = browser.CdpClose(c, 0)
|
||||
// CdpDisconnect = cerrar el WebSocket sin matar Chrome (el navegador
|
||||
// sigue vivo; solo soltamos la sesión pooled).
|
||||
_ = browser.CdpDisconnect(c)
|
||||
delete(p.conns, port)
|
||||
}
|
||||
}
|
||||
@@ -62,13 +70,27 @@ func (p *connPool) connectTarget(port int, match string) (*browser.CDPConn, erro
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (p *connPool) setCancel(port int, cancel func()) {
|
||||
// setDialog guarda el cancel y el DialogLog del auto-handler de diálogos del
|
||||
// puerto. Si ya había uno armado, lo cancela primero.
|
||||
func (p *connPool) setDialog(port int, cancel func(), dlog *browser.DialogLog) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if old := p.cancels[port]; old != nil {
|
||||
old()
|
||||
}
|
||||
p.cancels[port] = cancel
|
||||
p.dialogLogs[port] = dlog
|
||||
}
|
||||
|
||||
// dialogSnapshot devuelve el estado del log de diálogos del puerto (0,"","" si
|
||||
// no hay handler armado).
|
||||
func (p *connPool) dialogSnapshot(port int) (int, string, string) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if dl := p.dialogLogs[port]; dl != nil {
|
||||
return dl.Snapshot()
|
||||
}
|
||||
return 0, "", ""
|
||||
}
|
||||
|
||||
func (p *connPool) closeAll() {
|
||||
@@ -79,11 +101,12 @@ func (p *connPool) closeAll() {
|
||||
cancel()
|
||||
}
|
||||
if c != nil {
|
||||
_ = browser.CdpClose(c, 0)
|
||||
_ = browser.CdpDisconnect(c)
|
||||
}
|
||||
}
|
||||
p.conns = map[int]*browser.CDPConn{}
|
||||
p.cancels = map[int]func(){}
|
||||
p.dialogLogs = map[int]*browser.DialogLog{}
|
||||
}
|
||||
|
||||
// isConnErr reconoce errores de conexión CDP muerta para reintentar UNA vez.
|
||||
|
||||
+48
-10
@@ -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 {
|
||||
|
||||
+4
-2
@@ -101,10 +101,12 @@ func (d *deps) handleHandleDialog(_ context.Context, _ mcp.CallToolRequest, a ha
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
cancel, err := browser.CdpHandleDialog(c, a.Accept, a.PromptText)
|
||||
cancel, dlog, err := browser.CdpHandleDialog(c, a.Accept, a.PromptText)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
d.pool.setCancel(port, cancel)
|
||||
// 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
|
||||
}
|
||||
|
||||
+7
-1
@@ -99,6 +99,12 @@ func disconnectTool() mcp.Tool {
|
||||
|
||||
func (d *deps) handleDisconnect(_ context.Context, _ mcp.CallToolRequest, a disconnectArgs) (*mcp.CallToolResult, error) {
|
||||
port := portOr(a.Port)
|
||||
// Leer el log de diálogos ANTES de drop (drop lo limpia).
|
||||
count, lastType, lastMsg := d.pool.dialogSnapshot(port)
|
||||
d.pool.drop(port)
|
||||
return mcp.NewToolResultText(fmt.Sprintf("disconnected port=%d", port)), nil
|
||||
msg := fmt.Sprintf("disconnected port=%d", port)
|
||||
if count > 0 {
|
||||
msg += fmt.Sprintf(" (dialogs auto-handled: %d, last %s: %q)", count, lastType, lastMsg)
|
||||
}
|
||||
return mcp.NewToolResultText(msg), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user