Merge feat/new-tools-and-richer-list: 4 tools nuevas + browser_list enriquecido

This commit is contained in:
2026-06-16 20:25:35 +02:00
3 changed files with 186 additions and 2 deletions
+65
View File
@@ -27,9 +27,74 @@ func registerDomTools(s *server.MCPServer, d *deps) {
s.AddTool(domTypeRefTool(), mcp.NewTypedToolHandler(d.handleDomTypeRef))
s.AddTool(domHoverRefTool(), mcp.NewTypedToolHandler(d.handleDomHoverRef))
s.AddTool(domClickXYTool(), mcp.NewTypedToolHandler(d.handleDomClickXY))
s.AddTool(domSelectOptionTool(), mcp.NewTypedToolHandler(d.handleDomSelectOption))
s.AddTool(domSetFilesTool(), mcp.NewTypedToolHandler(d.handleDomSetFiles))
}
}
// ---- dom_select_option (MUTA) ----
type domSelectOptionArgs struct {
Port int `json:"port"`
Selector string `json:"selector"`
Value string `json:"value"`
}
func domSelectOptionTool() mcp.Tool {
return mcp.NewTool("dom_select_option",
mcp.WithDescription("Select an <option> in a native <select> element (by CSS selector), matching by option value first, then by visible text, and firing input/change events so React/Vue react. For custom (non-<select>) dropdowns use dom_click_ref on the trigger then on the option instead."),
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 <select> element.")),
mcp.WithString("value", mcp.Required(), mcp.Description("Option value (or visible text if no value matches).")),
)
}
func (d *deps) handleDomSelectOption(_ context.Context, _ mcp.CallToolRequest, a domSelectOptionArgs) (*mcp.CallToolResult, error) {
if a.Selector == "" || a.Value == "" {
return mcp.NewToolResultError("selector and value are required"), nil
}
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
return browser.CdpSelectOption(c, a.Selector, a.Value)
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf("selected %q in %s", a.Value, a.Selector)), nil
}
// ---- dom_set_files (MUTA) ----
type domSetFilesArgs struct {
Port int `json:"port"`
Selector string `json:"selector"`
Paths []string `json:"paths"`
}
func domSetFilesTool() mcp.Tool {
return mcp.NewTool("dom_set_files",
mcp.WithDescription("Upload files to an <input type=\"file\"> (by CSS selector) via DOM.setFileInputFiles, without driving the OS file picker. Paths must be absolute and readable by the Chrome process."),
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 file input element.")),
mcp.WithArray("paths", mcp.Required(), mcp.Description("Absolute file paths to attach."), mcp.Items(map[string]any{"type": "string"})),
)
}
func (d *deps) handleDomSetFiles(_ context.Context, _ mcp.CallToolRequest, a domSetFilesArgs) (*mcp.CallToolResult, error) {
if a.Selector == "" {
return mcp.NewToolResultError("selector is required"), nil
}
if len(a.Paths) == 0 {
return mcp.NewToolResultError("paths is required (at least one file)"), nil
}
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
return browser.CdpSetFileInput(c, a.Selector, a.Paths)
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf("attached %d file(s) to %s", len(a.Paths), a.Selector)), nil
}
// 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
+40 -2
View File
@@ -16,6 +16,8 @@ import (
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
"fn-registry/functions/browser"
)
// registerLifecycleTools wires the per-profile Chromium lifecycle tools:
@@ -55,7 +57,10 @@ type chromiumMaster struct {
UserDataDir string `json:"user_data_dir"` // value of --user-data-dir
CDPPort string `json:"cdp_port"` // value of --remote-debugging-port ("" if none)
HasCDP bool `json:"has_cdp"`
Headless bool `json:"headless"` // true if launched with --headless / --headless=new / --headless=old
Headless bool `json:"headless"` // true if launched with --headless / --headless=new / --headless=old
Pages int `json:"pages"` // count of "page" targets (best-effort via GET /json; 0 if no CDP or unreachable)
ActiveTitle string `json:"active_title,omitempty"` // title of the first "page" target
ActiveURL string `json:"active_url,omitempty"` // URL of the first "page" target
}
// parseCmdline turns the raw bytes of /proc/<pid>/cmdline into argv.
@@ -271,7 +276,7 @@ type browserListArgs struct{}
func browserListTool() mcp.Tool {
return mcp.NewTool("browser_list",
mcp.WithDescription("List the running Chromium MASTER processes (one per user-data-dir master, NOT zygote/gpu/renderer children). For each: pid, profile (--profile-directory value), user_data_dir, cdp_port (--remote-debugging-port value, empty if none), has_cdp, headless (true if launched with --headless). Returns a JSON array. Read-only."),
mcp.WithDescription("List the running Chromium MASTER processes (one per user-data-dir master, NOT zygote/gpu/renderer children). For each: pid, profile (--profile-directory value), user_data_dir, cdp_port (--remote-debugging-port value, empty if none), has_cdp, headless (true if launched with --headless), pages (count of open page targets via GET /json, best-effort), active_title/active_url (first open page). Returns a JSON array. Read-only."),
)
}
@@ -283,10 +288,43 @@ func (d *deps) handleBrowserList(_ context.Context, _ mcp.CallToolRequest, _ bro
if masters == nil {
masters = []chromiumMaster{}
}
// Enriquecer cada master con CDP con su nº de páginas y la primera página
// (título/URL) consultando GET /json. Best-effort: si el puerto no responde,
// se dejan los campos a cero — el listado de procesos nunca falla por esto.
for i := range masters {
enrichMasterTabs(&masters[i])
}
b, _ := json.MarshalIndent(masters, "", " ")
return mcp.NewToolResultText(string(b)), nil
}
// enrichMasterTabs rellena Pages/ActiveTitle/ActiveURL de un master consultando
// sus targets CDP por HTTP. No devuelve error: cualquier fallo (sin CDP, puerto
// caído, timeout) deja los campos en su cero y el master se reporta igual.
func enrichMasterTabs(m *chromiumMaster) {
if m.CDPPort == "" {
return
}
port, err := strconv.Atoi(m.CDPPort)
if err != nil {
return
}
tabs, err := browser.CdpListTabs("localhost", port)
if err != nil {
return
}
for _, t := range tabs {
if t.Type != "page" {
continue
}
m.Pages++
if m.ActiveURL == "" {
m.ActiveTitle = t.Title
m.ActiveURL = t.URL
}
}
}
// ---- browser_launch_profile (MUTA) ----
type launchProfileArgs struct {
+81
View File
@@ -3,6 +3,8 @@ package main
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"github.com/mark3labs/mcp-go/mcp"
@@ -20,12 +22,91 @@ func registerReadTools(s *server.MCPServer, d *deps) {
s.AddTool(pageGetTextTool(), mcp.NewTypedToolHandler(d.handlePageGetText))
s.AddTool(pagePerceiveTool(), mcp.NewTypedToolHandler(d.handlePagePerceive))
s.AddTool(pageScreenshotTool(), mcp.NewTypedToolHandler(d.handlePageScreenshot))
s.AddTool(pageCollectConsoleTool(), mcp.NewTypedToolHandler(d.handlePageCollectConsole))
s.AddTool(pagePDFTool(), mcp.NewTypedToolHandler(d.handlePagePDF))
if !d.readOnly {
s.AddTool(pageEvalJSTool(), mcp.NewTypedToolHandler(d.handlePageEvalJS))
}
}
// ---- page_collect_console ----
type pageCollectConsoleArgs struct {
Port int `json:"port"`
DurationMs int `json:"duration_ms"`
}
func pageCollectConsoleTool() mcp.Tool {
return mcp.NewTool("page_collect_console",
mcp.WithDescription("Capture the page's console output (console.log/info/warn/error), uncaught JS exceptions and browser log entries during a time window, and return them as JSON. It is a SNAPSHOT: it records what happens during duration_ms AFTER the call starts, not past history — so trigger the action you want to observe (reload, click) right before or during the window. Use this to debug why a page misbehaves without flying blind."),
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("duration_ms", mcp.Description("Capture window in milliseconds. Default 1500.")),
)
}
func (d *deps) handlePageCollectConsole(_ context.Context, _ mcp.CallToolRequest, a pageCollectConsoleArgs) (*mcp.CallToolResult, error) {
var entries []browser.ConsoleEntry
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
var e error
entries, e = browser.CdpCollectConsole(c, a.DurationMs)
return e
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if entries == nil {
entries = []browser.ConsoleEntry{}
}
b, _ := json.MarshalIndent(entries, "", " ")
return mcp.NewToolResultText(truncate(string(b), htmlMax)), nil
}
// ---- page_pdf ----
type pagePDFArgs struct {
Port int `json:"port"`
Path string `json:"path"`
Landscape bool `json:"landscape"`
PrintBackground bool `json:"print_background"`
Scale float64 `json:"scale"`
}
func pagePDFTool() mcp.Tool {
return mcp.NewTool("page_pdf",
mcp.WithDescription("Render the current page to a PDF (Page.printToPDF) and write it to a local file path. Use for archiving an article/invoice/report exactly as laid out, when a screenshot is not enough (multi-page, selectable text)."),
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 .pdf file path.")),
mcp.WithBoolean("landscape", mcp.Description("Landscape orientation. Default false (portrait).")),
mcp.WithBoolean("print_background", mcp.Description("Include background graphics/colors. Default false.")),
mcp.WithNumber("scale", mcp.Description("Render scale. Default 1.0.")),
)
}
func (d *deps) handlePagePDF(_ context.Context, _ mcp.CallToolRequest, a pagePDFArgs) (*mcp.CallToolResult, error) {
if a.Path == "" {
return mcp.NewToolResultError("path is required"), nil
}
opts := browser.CdpPrintPDFOpts{
Landscape: a.Landscape,
PrintBackground: a.PrintBackground,
Scale: a.Scale,
}
var data []byte
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
var e error
data, e = browser.CdpPrintPDF(c, opts)
return e
})
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}
if e := os.WriteFile(a.Path, data, 0o644); e != nil {
return mcp.NewToolResultError("saving pdf to " + a.Path + ": " + e.Error()), nil
}
return mcp.NewToolResultText(fmt.Sprintf("pdf saved to %s (%d bytes)", a.Path, len(data))), nil
}
// ---- page_get_text ----
type pageGetTextArgs struct {