feat: browser_mcp — servidor MCP de control de navegador CDP (33 tools + pool de conexiones)
This commit is contained in:
+260
@@ -0,0 +1,260 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
|
||||
"fn-registry/functions/browser"
|
||||
)
|
||||
|
||||
// registerNavTools wires navigation + tab management + page-wait tools.
|
||||
func registerNavTools(s *server.MCPServer, d *deps) {
|
||||
// Tab tools use HTTP /json directly (no pool) — list/activate are read-only.
|
||||
s.AddTool(tabListTool(), mcp.NewTypedToolHandler(d.handleTabList))
|
||||
s.AddTool(tabActivateTool(), mcp.NewTypedToolHandler(d.handleTabActivate))
|
||||
s.AddTool(pageWaitLoadTool(), mcp.NewTypedToolHandler(d.handlePageWaitLoad))
|
||||
s.AddTool(pageWaitIdleTool(), mcp.NewTypedToolHandler(d.handlePageWaitIdle))
|
||||
|
||||
if !d.readOnly {
|
||||
s.AddTool(tabNavigateTool(), mcp.NewTypedToolHandler(d.handleTabNavigate))
|
||||
s.AddTool(tabNewTool(), mcp.NewTypedToolHandler(d.handleTabNew))
|
||||
s.AddTool(tabCloseTool(), mcp.NewTypedToolHandler(d.handleTabClose))
|
||||
s.AddTool(navBackTool(), mcp.NewTypedToolHandler(d.handleNavBack))
|
||||
s.AddTool(navForwardTool(), mcp.NewTypedToolHandler(d.handleNavForward))
|
||||
}
|
||||
}
|
||||
|
||||
// ---- tab_navigate (MUTA) ----
|
||||
|
||||
type tabNavigateArgs struct {
|
||||
Port int `json:"port"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func tabNavigateTool() mcp.Tool {
|
||||
return mcp.NewTool("tab_navigate",
|
||||
mcp.WithDescription("Navigate the connected tab to a URL via Page.navigate."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
mcp.WithString("url", mcp.Required(), mcp.Description("Target URL.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleTabNavigate(_ context.Context, _ mcp.CallToolRequest, a tabNavigateArgs) (*mcp.CallToolResult, error) {
|
||||
if a.URL == "" {
|
||||
return mcp.NewToolResultError("url is required"), nil
|
||||
}
|
||||
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
||||
return browser.CdpNavigate(c, a.URL)
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("navigated to " + a.URL), nil
|
||||
}
|
||||
|
||||
// ---- tab_list ----
|
||||
|
||||
type tabListArgs struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func tabListTool() mcp.Tool {
|
||||
return mcp.NewTool("tab_list",
|
||||
mcp.WithDescription("List all CDP targets (tabs, iframes, workers) via GET /json. Returns JSON."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleTabList(_ context.Context, _ mcp.CallToolRequest, a tabListArgs) (*mcp.CallToolResult, error) {
|
||||
tabs, err := browser.CdpListTabs("localhost", portOr(a.Port))
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
b, _ := json.MarshalIndent(tabs, "", " ")
|
||||
return mcp.NewToolResultText(string(b)), nil
|
||||
}
|
||||
|
||||
// ---- tab_new (MUTA) ----
|
||||
|
||||
type tabNewArgs struct {
|
||||
Port int `json:"port"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
func tabNewTool() mcp.Tool {
|
||||
return mcp.NewTool("tab_new",
|
||||
mcp.WithDescription("Open a new tab via PUT /json/new. Returns the new tab's JSON."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
mcp.WithString("url", mcp.Description("Optional start URL. Empty = about:blank.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleTabNew(_ context.Context, _ mcp.CallToolRequest, a tabNewArgs) (*mcp.CallToolResult, error) {
|
||||
tab, err := browser.CdpNewTab("localhost", portOr(a.Port), a.URL)
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
b, _ := json.MarshalIndent(tab, "", " ")
|
||||
return mcp.NewToolResultText(string(b)), nil
|
||||
}
|
||||
|
||||
// ---- tab_close (MUTA) ----
|
||||
|
||||
type tabCloseArgs struct {
|
||||
Port int `json:"port"`
|
||||
TabID string `json:"tab_id"`
|
||||
}
|
||||
|
||||
func tabCloseTool() mcp.Tool {
|
||||
return mcp.NewTool("tab_close",
|
||||
mcp.WithDescription("Close a tab by its target ID via GET /json/close/<id>."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
mcp.WithString("tab_id", mcp.Required(), mcp.Description("Target ID of the tab to close.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleTabClose(_ context.Context, _ mcp.CallToolRequest, a tabCloseArgs) (*mcp.CallToolResult, error) {
|
||||
if a.TabID == "" {
|
||||
return mcp.NewToolResultError("tab_id is required"), nil
|
||||
}
|
||||
if err := browser.CdpCloseTab("localhost", portOr(a.Port), a.TabID); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("closed tab " + a.TabID), nil
|
||||
}
|
||||
|
||||
// ---- tab_activate ----
|
||||
|
||||
type tabActivateArgs struct {
|
||||
Port int `json:"port"`
|
||||
TabID string `json:"tab_id"`
|
||||
}
|
||||
|
||||
func tabActivateTool() mcp.Tool {
|
||||
return mcp.NewTool("tab_activate",
|
||||
mcp.WithDescription("Bring a tab to the foreground via GET /json/activate/<id>."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
mcp.WithString("tab_id", mcp.Required(), mcp.Description("Target ID of the tab to activate.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleTabActivate(_ context.Context, _ mcp.CallToolRequest, a tabActivateArgs) (*mcp.CallToolResult, error) {
|
||||
if a.TabID == "" {
|
||||
return mcp.NewToolResultError("tab_id is required"), nil
|
||||
}
|
||||
if err := browser.CdpActivateTab("localhost", portOr(a.Port), a.TabID); err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("activated tab " + a.TabID), nil
|
||||
}
|
||||
|
||||
// ---- nav_back (MUTA) ----
|
||||
|
||||
type navBackArgs struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func navBackTool() mcp.Tool {
|
||||
return mcp.NewTool("nav_back",
|
||||
mcp.WithDescription("Navigate back in the connected tab's history."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleNavBack(_ context.Context, _ mcp.CallToolRequest, a navBackArgs) (*mcp.CallToolResult, error) {
|
||||
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
||||
return browser.CdpNavBack(c)
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("navigated back"), nil
|
||||
}
|
||||
|
||||
// ---- nav_forward (MUTA) ----
|
||||
|
||||
type navForwardArgs struct {
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
func navForwardTool() mcp.Tool {
|
||||
return mcp.NewTool("nav_forward",
|
||||
mcp.WithDescription("Navigate forward in the connected tab's history."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handleNavForward(_ context.Context, _ mcp.CallToolRequest, a navForwardArgs) (*mcp.CallToolResult, error) {
|
||||
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
||||
return browser.CdpNavForward(c)
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("navigated forward"), nil
|
||||
}
|
||||
|
||||
// ---- page_wait_load ----
|
||||
|
||||
type pageWaitLoadArgs struct {
|
||||
Port int `json:"port"`
|
||||
TimeoutMs int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
func pageWaitLoadTool() mcp.Tool {
|
||||
return mcp.NewTool("page_wait_load",
|
||||
mcp.WithDescription("Block until the page fires the load event (or timeout)."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
mcp.WithNumber("timeout_ms", mcp.Description("Max wait in ms. Default 10000.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handlePageWaitLoad(_ context.Context, _ mcp.CallToolRequest, a pageWaitLoadArgs) (*mcp.CallToolResult, error) {
|
||||
timeout := a.TimeoutMs
|
||||
if timeout <= 0 {
|
||||
timeout = 10000
|
||||
}
|
||||
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
||||
return browser.CdpWaitLoad(c, time.Duration(timeout)*time.Millisecond)
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("page loaded"), nil
|
||||
}
|
||||
|
||||
// ---- page_wait_idle ----
|
||||
|
||||
type pageWaitIdleArgs struct {
|
||||
Port int `json:"port"`
|
||||
TimeoutMs int `json:"timeout_ms"`
|
||||
}
|
||||
|
||||
func pageWaitIdleTool() mcp.Tool {
|
||||
return mcp.NewTool("page_wait_idle",
|
||||
mcp.WithDescription("Block until network activity quiets down (inflight requests reach 0 for a quiet window) or timeout. Immune to DOM-mutating extensions/animations."),
|
||||
mcp.WithNumber("port", mcp.Description("CDP port. Default 9222.")),
|
||||
mcp.WithNumber("timeout_ms", mcp.Description("Max wait in ms. Default 15000.")),
|
||||
)
|
||||
}
|
||||
|
||||
func (d *deps) handlePageWaitIdle(_ context.Context, _ mcp.CallToolRequest, a pageWaitIdleArgs) (*mcp.CallToolResult, error) {
|
||||
timeout := a.TimeoutMs
|
||||
if timeout <= 0 {
|
||||
timeout = 15000
|
||||
}
|
||||
opts := browser.CdpWaitIdleOpts{
|
||||
Timeout: time.Duration(timeout) * time.Millisecond,
|
||||
}
|
||||
err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error {
|
||||
return browser.CdpWaitIdle(c, opts)
|
||||
})
|
||||
if err != nil {
|
||||
return mcp.NewToolResultError(err.Error()), nil
|
||||
}
|
||||
return mcp.NewToolResultText("network idle"), nil
|
||||
}
|
||||
Reference in New Issue
Block a user