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(tabSelectTool(), mcp.NewTypedToolHandler(d.handleTabSelect)) 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/."), 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/."), 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 } // ---- tab_select ---- type tabSelectArgs struct { Port int `json:"port"` Match string `json:"match"` } func tabSelectTool() mcp.Tool { return mcp.NewTool("tab_select", mcp.WithDescription("Fija la pestaña sobre la que operan las siguientes tools, eligiéndola por id o por substring de su URL (determinista). Úsala tras tab_list para no operar sobre la pestaña equivocada."), 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("match", mcp.Description("Target id exacto o substring de la URL de la pestaña. Vacío = primera page.")), ) } func (d *deps) handleTabSelect(_ context.Context, _ mcp.CallToolRequest, a tabSelectArgs) (*mcp.CallToolResult, error) { if _, err := d.pool.connectTarget(portOr(a.Port), a.Match); err != nil { return mcp.NewToolResultError(err.Error()), nil } return mcp.NewToolResultText("selected target matching: " + a.Match), 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 }