package main import ( "context" "encoding/json" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" "fn-registry/functions/browser" ) // registerFrameTools wires frame_list + frame_get_html + frame_get_text (read) // and frame_eval (MUTA). func registerFrameTools(s *server.MCPServer, d *deps) { s.AddTool(frameListTool(), mcp.NewTypedToolHandler(d.handleFrameList)) s.AddTool(frameGetHTMLTool(), mcp.NewTypedToolHandler(d.handleFrameGetHTML)) s.AddTool(frameGetTextTool(), mcp.NewTypedToolHandler(d.handleFrameGetText)) if !d.readOnly { s.AddTool(frameEvalTool(), mcp.NewTypedToolHandler(d.handleFrameEval)) } } // ---- frame_list ---- type frameListArgs struct { Port int `json:"port"` } func frameListTool() mcp.Tool { return mcp.NewTool("frame_list", mcp.WithDescription("List all frames (including iframes) of the current page via Page.getFrameTree. Returns JSON with frame IDs."), mcp.WithNumber("port", mcp.Description("CDP port. Default 9333 (Chrome isolated del MCP); usa 9222 explícito solo para adjuntarte al navegador diario.")), ) } func (d *deps) handleFrameList(_ context.Context, _ mcp.CallToolRequest, a frameListArgs) (*mcp.CallToolResult, error) { var frames []browser.CdpFrame err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error { var e error frames, e = browser.CdpListFrames(c) return e }) if err != nil { return mcp.NewToolResultError(err.Error()), nil } b, _ := json.MarshalIndent(frames, "", " ") return mcp.NewToolResultText(string(b)), nil } // ---- frame_eval (MUTA) ---- type frameEvalArgs struct { Port int `json:"port"` FrameID string `json:"frame_id"` Expression string `json:"expression"` } func frameEvalTool() mcp.Tool { return mcp.NewTool("frame_eval", mcp.WithDescription("Evaluate a JavaScript expression inside a specific frame's execution context. Returns the stringified result."), 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("frame_id", mcp.Required(), mcp.Description("Frame ID (from frame_list).")), mcp.WithString("expression", mcp.Required(), mcp.Description("JavaScript expression to evaluate.")), ) } func (d *deps) handleFrameEval(_ context.Context, _ mcp.CallToolRequest, a frameEvalArgs) (*mcp.CallToolResult, error) { if a.FrameID == "" { return mcp.NewToolResultError("frame_id is required"), nil } if a.Expression == "" { return mcp.NewToolResultError("expression is required"), nil } var res string err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error { var e error res, e = browser.CdpEvalInFrame(c, a.FrameID, a.Expression) return e }) if err != nil { return mcp.NewToolResultError(err.Error()), nil } return mcp.NewToolResultText(truncate(res, htmlMax)), nil } // ---- frame_get_html ---- type frameGetHTMLArgs struct { Port int `json:"port"` FrameID string `json:"frame_id"` } func frameGetHTMLTool() mcp.Tool { return mcp.NewTool("frame_get_html", mcp.WithDescription("Return the serialized HTML of a specific frame. Truncated to 200000 chars."), 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("frame_id", mcp.Required(), mcp.Description("Frame ID (from frame_list).")), ) } func (d *deps) handleFrameGetHTML(_ context.Context, _ mcp.CallToolRequest, a frameGetHTMLArgs) (*mcp.CallToolResult, error) { if a.FrameID == "" { return mcp.NewToolResultError("frame_id is required"), nil } var html string err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error { var e error html, e = browser.CdpGetFrameHTML(c, a.FrameID) return e }) if err != nil { return mcp.NewToolResultError(err.Error()), nil } return mcp.NewToolResultText(truncate(html, htmlMax)), nil } // ---- frame_get_text ---- type frameGetTextArgs struct { Port int `json:"port"` FrameID string `json:"frame_id"` MaxBytes int `json:"max_bytes"` } func frameGetTextTool() mcp.Tool { return mcp.NewTool("frame_get_text", mcp.WithDescription("Return the visible text (innerText) of a specific iframe, truncated to max_bytes. Use this to read content trapped inside an iframe — page_get_text only covers the top-level document. Get the frame_id from frame_list."), 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("frame_id", mcp.Required(), mcp.Description("Frame ID (from frame_list).")), mcp.WithNumber("max_bytes", mcp.Description("Máximo de bytes a devolver. Default 20000. 0 = sin límite.")), ) } func (d *deps) handleFrameGetText(_ context.Context, _ mcp.CallToolRequest, a frameGetTextArgs) (*mcp.CallToolResult, error) { if a.FrameID == "" { return mcp.NewToolResultError("frame_id is required"), nil } maxBytes := a.MaxBytes if maxBytes == 0 { maxBytes = 20000 } var text string err := d.withConn(portOr(a.Port), func(c *browser.CDPConn) error { var e error text, e = browser.CdpGetTextInFrame(c, a.FrameID, maxBytes) return e }) if err != nil { return mcp.NewToolResultError(err.Error()), nil } return mcp.NewToolResultText(text), nil }