261 lines
8.1 KiB
Go
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
|
|
}
|