Files
browser_mcp/tools_nav.go
T

261 lines
8.1 KiB
Go

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
}