diff --git a/mcp/LLM_USAGE_GUIDE.md b/mcp/LLM_USAGE_GUIDE.md index a28180a..d84ae07 100644 --- a/mcp/LLM_USAGE_GUIDE.md +++ b/mcp/LLM_USAGE_GUIDE.md @@ -4,7 +4,7 @@ This guide explains how LLMs can use the cremote MCP (Model Context Protocol) to ## 🎉 Complete Web Automation Platform -The cremote MCP server provides **30 comprehensive web automation tools** organized across 6 enhancement phases: +The cremote MCP server provides **31 comprehensive web automation tools** organized across 6 enhancement phases: - **Core Tools (10)**: Essential web automation capabilities - **Phase 1 (2)**: Element state checking and conditional logic @@ -14,7 +14,7 @@ The cremote MCP server provides **30 comprehensive web automation tools** organi - **Phase 5 (4)**: Enhanced screenshots and file management - **Phase 6 (3)**: Accessibility tree support for semantic understanding -## Available Tools (30 Total) +## Available Tools (31 Total) ### 1. `web_navigate_cremotemcp` Navigate to URLs and optionally take screenshots. @@ -1625,8 +1625,8 @@ get_partial_accessibility_tree_cremotemcp: ## 🎉 Production Ready -This comprehensive web automation platform provides **30 tools** across 6 enhancement phases, optimized specifically for LLM agents and production workflows. All tools include proper error handling, timeout management, and structured responses for reliable automation. +This comprehensive web automation platform provides **31 tools** across 6 enhancement phases, optimized specifically for LLM agents and production workflows. All tools include proper error handling, timeout management, and structured responses for reliable automation. --- -**Ready for Production**: Complete web automation platform with 30 tools, designed for maximum efficiency and reliability in LLM-driven workflows. +**Ready for Production**: Complete web automation platform with 31 tools, designed for maximum efficiency and reliability in LLM-driven workflows. diff --git a/mcp/README.md b/mcp/README.md index ba55746..33b23eb 100644 --- a/mcp/README.md +++ b/mcp/README.md @@ -4,7 +4,7 @@ This is a Model Context Protocol (MCP) server that exposes cremote's web automat ## 🎉 Complete Web Automation Platform -**30 comprehensive tools** across 6 enhancement phases, providing a complete web automation toolkit for LLM agents: +**31 comprehensive tools** across 6 enhancement phases, providing a complete web automation toolkit for LLM agents: ### 🚀 **NEW: Multi-Client Support** @@ -42,7 +42,7 @@ See the [Multi-Client Guide](MULTI_CLIENT_GUIDE.md) for detailed setup and usage **For LLM agents**: See the comprehensive [LLM Usage Guide](LLM_USAGE_GUIDE.md) for detailed usage instructions, examples, and best practices. -## Available Tools (30 Total) +## Available Tools (31 Total) ### Version Information @@ -880,7 +880,7 @@ Query accessibility tree for nodes matching specific criteria. This comprehensive web automation platform is **production ready** with: -- **30 Tools**: Complete coverage of web automation needs +- **31 Tools**: Complete coverage of web automation needs - **6 Enhancement Phases**: Systematic capability building from basic to advanced - **Extensive Testing**: All tools validated and documented - **LLM Optimized**: Designed specifically for AI agent workflows @@ -910,4 +910,4 @@ The server is designed to be easily extensible while maintaining consistency wit --- -**🚀 Ready for Production**: Complete web automation platform with 30 tools across 6 enhancement phases, optimized for LLM agents and production workflows. +**🚀 Ready for Production**: Complete web automation platform with 31 tools across 6 enhancement phases, optimized for LLM agents and production workflows. diff --git a/mcp/main.go b/mcp/main.go index 29e6f92..e321453 100644 --- a/mcp/main.go +++ b/mcp/main.go @@ -2,15 +2,11 @@ package main import ( "context" - "crypto/rand" - "encoding/hex" "encoding/json" "fmt" "log" - "net/http" "os" "strconv" - "sync" "time" "git.teamworkapps.com/shortcut/cremote/client" @@ -20,42 +16,7 @@ import ( const Version = "2.0.0" -// ClientContext provides a unified interface for accessing client state -// This allows the same tool handlers to work with both single-client and multi-client modes -type ClientContext interface { - GetClient() *client.Client - GetCurrentTab() string - SetCurrentTab(tabID string) - AddToTabHistory(tabID string) - GetTabHistory() []string - SetIframeMode(mode bool) - GetIframeMode() bool - AddScreenshot(path string) - GetScreenshots() []string -} - -// ClientSession represents an isolated session for a single MCP client -type ClientSession struct { - ID string - client *client.Client - currentTab string - tabHistory []string - iframeMode bool - screenshots []string - createdAt time.Time - lastUsed time.Time - mu sync.RWMutex -} - -// SessionManager manages multiple client sessions -type SessionManager struct { - sessions map[string]*ClientSession - mu sync.RWMutex - host string - port int -} - -// CremoteServer wraps the cremote client for MCP (legacy single-client mode) +// CremoteServer wraps the cremote client for MCP type CremoteServer struct { client *client.Client currentTab string @@ -64,141 +25,7 @@ type CremoteServer struct { screenshots []string } -// NewSessionManager creates a new session manager -func NewSessionManager(host string, port int) *SessionManager { - return &SessionManager{ - sessions: make(map[string]*ClientSession), - host: host, - port: port, - } -} - -// generateSessionID creates a cryptographically secure session ID -func generateSessionID() string { - bytes := make([]byte, 16) - rand.Read(bytes) - return hex.EncodeToString(bytes) -} - -// CreateSession creates a new client session -func (sm *SessionManager) CreateSession() *ClientSession { - sm.mu.Lock() - defer sm.mu.Unlock() - - session := &ClientSession{ - ID: generateSessionID(), - client: client.NewClient(sm.host, sm.port), - tabHistory: make([]string, 0), - screenshots: make([]string, 0), - createdAt: time.Now(), - lastUsed: time.Now(), - } - - sm.sessions[session.ID] = session - return session -} - -// GetSession retrieves a session by ID -func (sm *SessionManager) GetSession(sessionID string) (*ClientSession, bool) { - sm.mu.RLock() - defer sm.mu.RUnlock() - - session, exists := sm.sessions[sessionID] - if exists { - session.mu.Lock() - session.lastUsed = time.Now() - session.mu.Unlock() - } - return session, exists -} - -// RemoveSession removes a session -func (sm *SessionManager) RemoveSession(sessionID string) { - sm.mu.Lock() - defer sm.mu.Unlock() - delete(sm.sessions, sessionID) -} - -// CleanupExpiredSessions removes sessions that haven't been used recently -func (sm *SessionManager) CleanupExpiredSessions(maxAge time.Duration) { - sm.mu.Lock() - defer sm.mu.Unlock() - - now := time.Now() - for id, session := range sm.sessions { - session.mu.RLock() - if now.Sub(session.lastUsed) > maxAge { - delete(sm.sessions, id) - } - session.mu.RUnlock() - } -} - -// Session-aware methods for ClientSession -func (cs *ClientSession) GetCurrentTab() string { - cs.mu.RLock() - defer cs.mu.RUnlock() - return cs.currentTab -} - -func (cs *ClientSession) SetCurrentTab(tabID string) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.currentTab = tabID - cs.lastUsed = time.Now() -} - -func (cs *ClientSession) AddToTabHistory(tabID string) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.tabHistory = append(cs.tabHistory, tabID) - cs.lastUsed = time.Now() -} - -func (cs *ClientSession) GetTabHistory() []string { - cs.mu.RLock() - defer cs.mu.RUnlock() - // Return a copy to prevent external modification - history := make([]string, len(cs.tabHistory)) - copy(history, cs.tabHistory) - return history -} - -func (cs *ClientSession) SetIframeMode(mode bool) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.iframeMode = mode - cs.lastUsed = time.Now() -} - -func (cs *ClientSession) GetIframeMode() bool { - cs.mu.RLock() - defer cs.mu.RUnlock() - return cs.iframeMode -} - -func (cs *ClientSession) AddScreenshot(path string) { - cs.mu.Lock() - defer cs.mu.Unlock() - cs.screenshots = append(cs.screenshots, path) - cs.lastUsed = time.Now() -} - -func (cs *ClientSession) GetScreenshots() []string { - cs.mu.RLock() - defer cs.mu.RUnlock() - // Return a copy to prevent external modification - screenshots := make([]string, len(cs.screenshots)) - copy(screenshots, cs.screenshots) - return screenshots -} - -// Implement ClientContext interface for ClientSession -func (cs *ClientSession) GetClient() *client.Client { - return cs.client -} - -// NewCremoteServer creates a new cremote MCP server (legacy single-client mode) +// NewCremoteServer creates a new cremote MCP server func NewCremoteServer(host string, port int) *CremoteServer { return &CremoteServer{ client: client.NewClient(host, port), @@ -207,49 +34,6 @@ func NewCremoteServer(host string, port int) *CremoteServer { } } -// Implement ClientContext interface for CremoteServer (legacy mode) -func (cs *CremoteServer) GetClient() *client.Client { - return cs.client -} - -func (cs *CremoteServer) GetCurrentTab() string { - return cs.currentTab -} - -func (cs *CremoteServer) SetCurrentTab(tabID string) { - cs.currentTab = tabID -} - -func (cs *CremoteServer) AddToTabHistory(tabID string) { - cs.tabHistory = append(cs.tabHistory, tabID) -} - -func (cs *CremoteServer) GetTabHistory() []string { - // Return a copy to prevent external modification - history := make([]string, len(cs.tabHistory)) - copy(history, cs.tabHistory) - return history -} - -func (cs *CremoteServer) SetIframeMode(mode bool) { - cs.iframeMode = mode -} - -func (cs *CremoteServer) GetIframeMode() bool { - return cs.iframeMode -} - -func (cs *CremoteServer) AddScreenshot(path string) { - cs.screenshots = append(cs.screenshots, path) -} - -func (cs *CremoteServer) GetScreenshots() []string { - // Return a copy to prevent external modification - screenshots := make([]string, len(cs.screenshots)) - copy(screenshots, cs.screenshots) - return screenshots -} - // Helper functions for parameter extraction func getStringParam(params map[string]any, key, defaultValue string) string { if val, ok := params[key].(string); ok { @@ -275,14 +59,41 @@ func getIntParam(params map[string]any, key string, defaultValue int) int { return defaultValue } -// createToolHandlers creates session-aware tool handlers that work with ClientContext -func createToolHandlers(getContext func() ClientContext) map[string]func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error) { - handlers := make(map[string]func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)) +func main() { + // Get cremote daemon connection settings + cremoteHost := os.Getenv("CREMOTE_HOST") + if cremoteHost == "" { + cremoteHost = "localhost" + } - // Version tool handler - handlers["version_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() - daemonVersion, err := clientCtx.GetClient().GetVersion() + cremotePortStr := os.Getenv("CREMOTE_PORT") + cremotePort := 8989 + if cremotePortStr != "" { + if p, err := strconv.Atoi(cremotePortStr); err == nil { + cremotePort = p + } + } + + log.Printf("Starting cremote MCP server, connecting to cremote daemon at %s:%d", cremoteHost, cremotePort) + + // Create the cremote server + cremoteServer := NewCremoteServer(cremoteHost, cremotePort) + + // Create MCP server + mcpServer := server.NewMCPServer("cremote-mcp", "2.0.0") + + // Register version tool + mcpServer.AddTool(mcp.Tool{ + Name: "version_cremotemcp", + Description: "Get version information for MCP server and daemon", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{}, + Required: []string{}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Get daemon version + daemonVersion, err := cremoteServer.client.GetVersion() if err != nil { daemonVersion = fmt.Sprintf("Unable to connect: %v", err) } @@ -293,11 +104,37 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } + }) - // Navigation tool handler - handlers["web_navigate_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_navigate tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_navigate_cremotemcp", + Description: "Navigate to a URL and optionally take a screenshot", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "url": map[string]any{ + "type": "string", + "description": "URL to navigate to", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + "screenshot": map[string]any{ + "type": "boolean", + "description": "Take screenshot after navigation", + }, + }, + Required: []string{"url"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -308,23 +145,23 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("url parameter is required") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) takeScreenshot := getBoolParam(params, "screenshot", false) // If no tab specified and no current tab, create a new one if tab == "" { - newTab, err := clientCtx.GetClient().OpenTab(timeout) + newTab, err := cremoteServer.client.OpenTab(timeout) if err != nil { return nil, fmt.Errorf("failed to create new tab: %w", err) } tab = newTab - clientCtx.SetCurrentTab(tab) - clientCtx.AddToTabHistory(tab) + cremoteServer.currentTab = tab + cremoteServer.tabHistory = append(cremoteServer.tabHistory, tab) } // Load the URL - err := clientCtx.GetClient().LoadURL(tab, url, timeout) + err := cremoteServer.client.LoadURL(tab, url, timeout) if err != nil { return nil, fmt.Errorf("failed to load URL: %w", err) } @@ -334,9 +171,9 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context // Take screenshot if requested if takeScreenshot { screenshotPath := fmt.Sprintf("/tmp/navigate-%d.png", time.Now().Unix()) - err = clientCtx.GetClient().TakeScreenshot(tab, screenshotPath, false, timeout) + err = cremoteServer.client.TakeScreenshot(tab, screenshotPath, false, timeout) if err == nil { - clientCtx.AddScreenshot(screenshotPath) + cremoteServer.screenshots = append(cremoteServer.screenshots, screenshotPath) message += fmt.Sprintf(" (screenshot saved to %s)", screenshotPath) } } @@ -347,14 +184,42 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } + }) - // Legacy navigation tool handler (for backward compatibility) - handlers["web_navigate"] = handlers["web_navigate_cremotemcp"] - - // Web interaction tool handler - handlers["web_interact"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_interact tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_interact_cremotemcp", + Description: "Interact with web elements (click, fill, submit)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "action": map[string]any{ + "type": "string", + "description": "Action to perform", + "enum": []any{"click", "fill", "submit", "upload", "select"}, + }, + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the element", + }, + "value": map[string]any{ + "type": "string", + "description": "Value to fill (for fill/upload actions)", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"action", "selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -362,7 +227,8 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context action := getStringParam(params, "action", "") selector := getStringParam(params, "selector", "") - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + value := getStringParam(params, "value", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) if action == "" { @@ -371,33 +237,41 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context if selector == "" { return nil, fmt.Errorf("selector parameter is required") } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } var err error var message string switch action { case "click": - err = clientCtx.GetClient().ClickElement(tab, selector, timeout) - message = fmt.Sprintf("Clicked element: %s", selector) + err = cremoteServer.client.ClickElement(tab, selector, timeout) + message = fmt.Sprintf("Clicked element %s", selector) case "fill": - value := getStringParam(params, "value", "") if value == "" { return nil, fmt.Errorf("value parameter is required for fill action") } - err = clientCtx.GetClient().FillFormField(tab, selector, value, timeout) - message = fmt.Sprintf("Filled element %s with value: %s", selector, value) + err = cremoteServer.client.FillFormField(tab, selector, value, timeout) + message = fmt.Sprintf("Filled element %s with value", selector) case "submit": - err = clientCtx.GetClient().SubmitForm(tab, selector, timeout) - message = fmt.Sprintf("Submitted form: %s", selector) + err = cremoteServer.client.SubmitForm(tab, selector, timeout) + message = fmt.Sprintf("Submitted form %s", selector) + + case "upload": + if value == "" { + return nil, fmt.Errorf("value parameter (file path) is required for upload action") + } + err = cremoteServer.client.UploadFile(tab, selector, value, timeout) + message = fmt.Sprintf("Uploaded file %s to element %s", value, selector) case "select": - value := getStringParam(params, "value", "") if value == "" { return nil, fmt.Errorf("value parameter is required for select action") } - err = clientCtx.GetClient().SelectElement(tab, selector, value, timeout) + err = cremoteServer.client.SelectElement(tab, selector, value, timeout) message = fmt.Sprintf("Selected option %s in element %s", value, selector) default: @@ -405,7 +279,7 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context } if err != nil { - return nil, fmt.Errorf("failed to %s: %w", action, err) + return nil, fmt.Errorf("failed to %s element: %w", action, err) } return &mcp.CallToolResult{ @@ -414,42 +288,79 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } + }) - // Web extract tool handler - handlers["web_extract"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_extract tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_extract_cremotemcp", + Description: "Extract data from the page (source, element HTML, or execute JavaScript)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "type": map[string]any{ + "type": "string", + "description": "Type of extraction", + "enum": []any{"source", "element", "javascript"}, + }, + "selector": map[string]any{ + "type": "string", + "description": "CSS selector (for element type)", + }, + "code": map[string]any{ + "type": "string", + "description": "JavaScript code (for javascript type)", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"type"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } extractType := getStringParam(params, "type", "") - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + selector := getStringParam(params, "selector", "") + code := getStringParam(params, "code", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) if extractType == "" { return nil, fmt.Errorf("type parameter is required") } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } - var result interface{} + var data string var err error switch extractType { case "source": - result, err = clientCtx.GetClient().GetPageSource(tab, timeout) + data, err = cremoteServer.client.GetPageSource(tab, timeout) + case "element": - selector := getStringParam(params, "selector", "") if selector == "" { return nil, fmt.Errorf("selector parameter is required for element extraction") } - result, err = clientCtx.GetClient().GetElementHTML(tab, selector, timeout) + data, err = cremoteServer.client.GetElementHTML(tab, selector, timeout) + case "javascript": - code := getStringParam(params, "code", "") if code == "" { return nil, fmt.Errorf("code parameter is required for javascript extraction") } - result, err = clientCtx.GetClient().EvalJS(tab, code, timeout) + data, err = cremoteServer.client.EvalJS(tab, code, timeout) + default: return nil, fmt.Errorf("unknown extraction type: %s", extractType) } @@ -458,54 +369,102 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("failed to extract %s: %w", extractType, err) } - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal result: %w", err) - } - return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Extracted %s data: %s", extractType, data)), }, IsError: false, }, nil - } + }) - // Web screenshot tool handler - handlers["web_screenshot"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_screenshot tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_screenshot_cremotemcp", + Description: "Take a screenshot of the current page", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "output": map[string]any{ + "type": "string", + "description": "Output file path", + }, + "full_page": map[string]any{ + "type": "boolean", + "description": "Capture full page", + "default": false, + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"output"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } output := getStringParam(params, "output", "") + fullPage := getBoolParam(params, "full_page", false) + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + if output == "" { return nil, fmt.Errorf("output parameter is required") } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) - timeout := getIntParam(params, "timeout", 5) - fullPage := getBoolParam(params, "full_page", false) - - err := clientCtx.GetClient().TakeScreenshot(tab, output, fullPage, timeout) + err := cremoteServer.client.TakeScreenshot(tab, output, fullPage, timeout) if err != nil { return nil, fmt.Errorf("failed to take screenshot: %w", err) } - clientCtx.AddScreenshot(output) + cremoteServer.screenshots = append(cremoteServer.screenshots, output) return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(fmt.Sprintf("Screenshot saved to: %s", output)), + mcp.NewTextContent(fmt.Sprintf("Screenshot saved to %s", output)), }, IsError: false, }, nil - } + }) - // Web manage tabs tool handler - handlers["web_manage_tabs"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_manage_tabs tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_manage_tabs_cremotemcp", + Description: "Manage browser tabs (open, close, list, switch)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "action": map[string]any{ + "type": "string", + "description": "Action to perform", + "enum": []any{"open", "close", "list", "switch"}, + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (for close/switch actions)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"action"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -524,33 +483,40 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context switch action { case "open": - newTab, err := clientCtx.GetClient().OpenTab(timeout) + newTab, err := cremoteServer.client.OpenTab(timeout) if err != nil { return nil, fmt.Errorf("failed to open new tab: %w", err) } - clientCtx.SetCurrentTab(newTab) - clientCtx.AddToTabHistory(newTab) + cremoteServer.currentTab = newTab + cremoteServer.tabHistory = append(cremoteServer.tabHistory, newTab) message = fmt.Sprintf("Opened new tab: %s", newTab) case "close": if tab == "" { - tab = clientCtx.GetCurrentTab() + return nil, fmt.Errorf("tab parameter is required for close action") } - if tab == "" { - return nil, fmt.Errorf("no tab to close") - } - err = clientCtx.GetClient().CloseTab(tab, timeout) + err = cremoteServer.client.CloseTab(tab, timeout) if err != nil { return nil, fmt.Errorf("failed to close tab: %w", err) } - // Clear current tab if we closed it - if tab == clientCtx.GetCurrentTab() { - clientCtx.SetCurrentTab("") + // Remove from history and update current tab + for i, id := range cremoteServer.tabHistory { + if id == tab { + cremoteServer.tabHistory = append(cremoteServer.tabHistory[:i], cremoteServer.tabHistory[i+1:]...) + break + } + } + if cremoteServer.currentTab == tab { + if len(cremoteServer.tabHistory) > 0 { + cremoteServer.currentTab = cremoteServer.tabHistory[len(cremoteServer.tabHistory)-1] + } else { + cremoteServer.currentTab = "" + } } message = fmt.Sprintf("Closed tab: %s", tab) case "list": - tabs, err := clientCtx.GetClient().ListTabs() + tabs, err := cremoteServer.client.ListTabs() if err != nil { return nil, fmt.Errorf("failed to list tabs: %w", err) } @@ -560,58 +526,94 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context if tab == "" { return nil, fmt.Errorf("tab parameter is required for switch action") } - clientCtx.SetCurrentTab(tab) - clientCtx.AddToTabHistory(tab) + cremoteServer.currentTab = tab + // Add to history if not already there + found := false + for _, t := range cremoteServer.tabHistory { + if t == tab { + found = true + break + } + } + if !found { + cremoteServer.tabHistory = append(cremoteServer.tabHistory, tab) + } message = fmt.Sprintf("Switched to tab: %s", tab) default: return nil, fmt.Errorf("unknown tab action: %s", action) } - if err != nil { - return nil, fmt.Errorf("failed to %s tab: %w", action, err) - } - return &mcp.CallToolResult{ Content: []mcp.Content{ mcp.NewTextContent(message), }, IsError: false, }, nil - } + }) - // Web iframe tool handler - handlers["web_iframe"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_iframe tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_iframe_cremotemcp", + Description: "Switch iframe context for subsequent operations", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "action": map[string]any{ + "type": "string", + "description": "Action to perform", + "enum": []any{"enter", "exit"}, + }, + "selector": map[string]any{ + "type": "string", + "description": "Iframe CSS selector (for enter action)", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"action"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } action := getStringParam(params, "action", "") - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + selector := getStringParam(params, "selector", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) if action == "" { return nil, fmt.Errorf("action parameter is required") } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } var err error var message string switch action { case "enter": - selector := getStringParam(params, "selector", "") if selector == "" { return nil, fmt.Errorf("selector parameter is required for enter action") } - err = clientCtx.GetClient().SwitchToIframe(tab, selector, timeout) - clientCtx.SetIframeMode(true) + err = cremoteServer.client.SwitchToIframe(tab, selector, timeout) + cremoteServer.iframeMode = true message = fmt.Sprintf("Entered iframe: %s", selector) case "exit": - err = clientCtx.GetClient().SwitchToMain(tab, timeout) - clientCtx.SetIframeMode(false) + err = cremoteServer.client.SwitchToMain(tab, timeout) + cremoteServer.iframeMode = false message = "Exited iframe context" default: @@ -628,74 +630,28 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } + }) - // Console logs tool handler - handlers["console_logs"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() - params, ok := request.Params.Arguments.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid arguments format") - } - - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) - clear := getBoolParam(params, "clear", false) - - logs, err := clientCtx.GetClient().GetConsoleLogs(tab, clear) - if err != nil { - return nil, fmt.Errorf("failed to get console logs: %w", err) - } - - logsJSON, err := json.MarshalIndent(logs, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal logs: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(logsJSON)), + // Register file_upload tool + mcpServer.AddTool(mcp.Tool{ + Name: "file_upload_cremotemcp", + Description: "Upload a file from the client to the container for use in form uploads", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "local_path": map[string]any{ + "type": "string", + "description": "Path to the file on the client machine", + }, + "container_path": map[string]any{ + "type": "string", + "description": "Optional path where to store the file in the container (defaults to /tmp/filename)", + }, }, - IsError: false, - }, nil - } - - // Console command tool handler - handlers["console_command"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() - params, ok := request.Params.Arguments.(map[string]any) - if !ok { - return nil, fmt.Errorf("invalid arguments format") - } - - command := getStringParam(params, "command", "") - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) - timeout := getIntParam(params, "timeout", 5) - - if command == "" { - return nil, fmt.Errorf("command parameter is required") - } - - result, err := clientCtx.GetClient().EvalJS(tab, command, timeout) - if err != nil { - return nil, fmt.Errorf("failed to execute console command: %w", err) - } - - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal result: %w", err) - } - - return &mcp.CallToolResult{ - Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), - }, - IsError: false, - }, nil - } - - // File upload tool handler - handlers["file_upload"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + Required: []string{"local_path"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -708,22 +664,40 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("local_path parameter is required") } - result, err := clientCtx.GetClient().UploadFileToContainer(localPath, containerPath) + // Upload the file to the container + targetPath, err := cremoteServer.client.UploadFileToContainer(localPath, containerPath) if err != nil { return nil, fmt.Errorf("failed to upload file: %w", err) } return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(fmt.Sprintf("File uploaded successfully to: %s", result)), + mcp.NewTextContent(fmt.Sprintf("File uploaded successfully to container at: %s", targetPath)), }, IsError: false, }, nil - } + }) - // File download tool handler - handlers["file_download"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register file_download tool + mcpServer.AddTool(mcp.Tool{ + Name: "file_download_cremotemcp", + Description: "Download a file from the container to the client (e.g., downloaded files from browser)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "container_path": map[string]any{ + "type": "string", + "description": "Path to the file in the container", + }, + "local_path": map[string]any{ + "type": "string", + "description": "Path where to save the file on the client machine", + }, + }, + Required: []string{"container_path", "local_path"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -739,170 +713,546 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("local_path parameter is required") } - err := clientCtx.GetClient().DownloadFileFromContainer(containerPath, localPath) + // Download the file from the container + err := cremoteServer.client.DownloadFileFromContainer(containerPath, localPath) if err != nil { return nil, fmt.Errorf("failed to download file: %w", err) } return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(fmt.Sprintf("File downloaded successfully to: %s", localPath)), + mcp.NewTextContent(fmt.Sprintf("File downloaded successfully from container to: %s", localPath)), }, IsError: false, }, nil - } + }) - // Enhanced tools with _cremotemcp suffix + // Register console_logs tool + mcpServer.AddTool(mcp.Tool{ + Name: "console_logs_cremotemcp", + Description: "Get console logs from the browser tab", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "clear": map[string]any{ + "type": "boolean", + "description": "Clear logs after retrieval (default: false)", + "default": false, + }, + }, + Required: []string{}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } - // Web element check tool handler - handlers["web_element_check_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + tab := getStringParam(params, "tab", cremoteServer.currentTab) + clear := getBoolParam(params, "clear", false) + + // Get console logs + logs, err := cremoteServer.client.GetConsoleLogs(tab, clear) + if err != nil { + return nil, fmt.Errorf("failed to get console logs: %w", err) + } + + // Format logs for display + var logText string + if len(logs) == 0 { + logText = "No console logs found." + } else { + logText = fmt.Sprintf("Found %d console log entries:\n\n", len(logs)) + for i, log := range logs { + level := log["level"].(string) + message := log["message"].(string) + timestamp := log["timestamp"].(string) + logText += fmt.Sprintf("[%d] %s [%s]: %s\n", i+1, timestamp, level, message) + } + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(logText), + }, + IsError: false, + }, nil + }) + + // Register console_command tool + mcpServer.AddTool(mcp.Tool{ + Name: "console_command_cremotemcp", + Description: "Execute a command in the browser console", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "command": map[string]any{ + "type": "string", + "description": "JavaScript command to execute in console", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + "default": 5, + }, + }, + Required: []string{"command"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + command := getStringParam(params, "command", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + + if command == "" { + return nil, fmt.Errorf("command parameter is required") + } + + // Execute console command using existing EvalJS functionality + result, err := cremoteServer.client.EvalJS(tab, command, timeout) + if err != nil { + return nil, fmt.Errorf("failed to execute console command: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Console command executed successfully.\nCommand: %s\nResult: %s", command, result)), + }, + IsError: false, + }, nil + }) + + // Register web_element_check tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_element_check_cremotemcp", + Description: "Check existence, visibility, enabled state, count elements", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the element(s)", + }, + "check_type": map[string]any{ + "type": "string", + "description": "Type of check to perform", + "enum": []any{"exists", "visible", "enabled", "focused", "selected", "all"}, + "default": "exists", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } selector := getStringParam(params, "selector", "") + checkType := getStringParam(params, "check_type", "exists") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + if selector == "" { return nil, fmt.Errorf("selector parameter is required") } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) - timeout := getIntParam(params, "timeout", 5) - checkType := getStringParam(params, "check_type", "exists") - - result, err := clientCtx.GetClient().CheckElement(tab, selector, checkType, timeout) + result, err := cremoteServer.client.CheckElement(tab, selector, checkType, timeout) if err != nil { return nil, fmt.Errorf("failed to check element: %w", err) } - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal result: %w", err) - } + // Format result as JSON string for display + resultJSON, _ := json.Marshal(result) return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Element check result: %s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // Web element attributes tool handler - handlers["web_element_attributes_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_element_attributes tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_element_attributes_cremotemcp", + Description: "Get attributes, properties, computed styles of an element", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the element", + }, + "attributes": map[string]any{ + "type": "string", + "description": "Comma-separated list of attributes or 'all' for common attributes. Use 'style_' prefix for computed styles, 'prop_' for JavaScript properties", + "default": "all", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } selector := getStringParam(params, "selector", "") + attributes := getStringParam(params, "attributes", "all") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + if selector == "" { return nil, fmt.Errorf("selector parameter is required") } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) - timeout := getIntParam(params, "timeout", 5) - attributes := getStringParam(params, "attributes", "all") - - result, err := clientCtx.GetClient().GetElementAttributes(tab, selector, attributes, timeout) + result, err := cremoteServer.client.GetElementAttributes(tab, selector, attributes, timeout) if err != nil { return nil, fmt.Errorf("failed to get element attributes: %w", err) } - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal result: %w", err) - } + // Format result as JSON string for display + resultJSON, _ := json.Marshal(result) return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Element attributes: %s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // Web extract multiple tool handler - handlers["web_extract_multiple_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_extract_multiple tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_extract_multiple_cremotemcp", + Description: "Extract from multiple selectors in one call", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selectors": map[string]any{ + "type": "object", + "description": "Object with keys as labels and values as CSS selectors", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selectors"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } - selectors, ok := params["selectors"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("selectors parameter is required and must be an object") - } - - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + selectorsParam := params["selectors"] + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) + if selectorsParam == nil { + return nil, fmt.Errorf("selectors parameter is required") + } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } + // Convert selectors to map[string]string selectorsMap := make(map[string]string) - for key, value := range selectors { - if strValue, ok := value.(string); ok { - selectorsMap[key] = strValue + if selectorsObj, ok := selectorsParam.(map[string]any); ok { + for key, value := range selectorsObj { + if selector, ok := value.(string); ok { + selectorsMap[key] = selector + } } + } else { + return nil, fmt.Errorf("selectors must be an object with string values") } - result, err := clientCtx.GetClient().ExtractMultiple(tab, selectorsMap, timeout) + result, err := cremoteServer.client.ExtractMultiple(tab, selectorsMap, timeout) if err != nil { - return nil, fmt.Errorf("failed to extract multiple elements: %w", err) + return nil, fmt.Errorf("failed to extract multiple: %w", err) } - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal result: %w", err) - } + // Format result as JSON string for display + resultJSON, _ := json.Marshal(result) return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Multiple extraction result: %s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // Add remaining key tools - - // Web extract links tool handler - handlers["web_extract_links_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_extract_links tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_extract_links_cremotemcp", + Description: "Extract all links with filtering options", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "container_selector": map[string]any{ + "type": "string", + "description": "Optional CSS selector to limit search to a container", + }, + "href_pattern": map[string]any{ + "type": "string", + "description": "Optional regex pattern to filter links by href", + }, + "text_pattern": map[string]any{ + "type": "string", + "description": "Optional regex pattern to filter links by text content", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) - timeout := getIntParam(params, "timeout", 5) containerSelector := getStringParam(params, "container_selector", "") hrefPattern := getStringParam(params, "href_pattern", "") textPattern := getStringParam(params, "text_pattern", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) - result, err := clientCtx.GetClient().ExtractLinks(tab, containerSelector, hrefPattern, textPattern, timeout) + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } + + result, err := cremoteServer.client.ExtractLinks(tab, containerSelector, hrefPattern, textPattern, timeout) if err != nil { return nil, fmt.Errorf("failed to extract links: %w", err) } - resultJSON, err := json.MarshalIndent(result, "", " ") - if err != nil { - return nil, fmt.Errorf("failed to marshal result: %w", err) - } + // Format result as JSON string for display + resultJSON, _ := json.Marshal(result) return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Links extraction result: %s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // Web extract table tool handler - handlers["web_extract_table_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_extract_table tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_extract_table_cremotemcp", + Description: "Extract table data as structured JSON", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the table element", + }, + "include_headers": map[string]any{ + "type": "boolean", + "description": "Whether to extract and use headers for structured data", + "default": true, + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + selector := getStringParam(params, "selector", "") + includeHeaders := getBoolParam(params, "include_headers", true) + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + + if selector == "" { + return nil, fmt.Errorf("selector parameter is required") + } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } + + result, err := cremoteServer.client.ExtractTable(tab, selector, includeHeaders, timeout) + if err != nil { + return nil, fmt.Errorf("failed to extract table: %w", err) + } + + // Format result as JSON string for display + resultJSON, _ := json.Marshal(result) + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Table extraction result: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Register web_extract_text tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_extract_text_cremotemcp", + Description: "Extract text with pattern matching", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for elements to extract text from", + }, + "pattern": map[string]any{ + "type": "string", + "description": "Optional regex pattern to match within text", + }, + "extract_type": map[string]any{ + "type": "string", + "description": "Type of text extraction", + "enum": []any{"text", "innerText", "textContent"}, + "default": "textContent", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + selector := getStringParam(params, "selector", "") + pattern := getStringParam(params, "pattern", "") + extractType := getStringParam(params, "extract_type", "textContent") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + + if selector == "" { + return nil, fmt.Errorf("selector parameter is required") + } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } + + result, err := cremoteServer.client.ExtractText(tab, selector, pattern, extractType, timeout) + if err != nil { + return nil, fmt.Errorf("failed to extract text: %w", err) + } + + // Format result as JSON string for display + resultJSON, _ := json.Marshal(result) + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Text extraction result: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Register web_form_analyze tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_form_analyze_cremotemcp", + Description: "Analyze forms completely", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the form element", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -913,13 +1263,12 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("selector parameter is required") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) - includeHeaders := getBoolParam(params, "include_headers", true) - result, err := clientCtx.GetClient().ExtractTable(tab, selector, includeHeaders, timeout) + result, err := cremoteServer.client.AnalyzeForm(tab, selector, timeout) if err != nil { - return nil, fmt.Errorf("failed to extract table: %w", err) + return nil, fmt.Errorf("failed to analyze form: %w", err) } resultJSON, err := json.MarshalIndent(result, "", " ") @@ -929,26 +1278,101 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Form analysis result: %s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // Web page info tool handler - handlers["web_page_info_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_interact_multiple tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_interact_multiple_cremotemcp", + Description: "Batch interactions", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "interactions": map[string]any{ + "type": "array", + "description": "Array of interactions to perform", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the element", + }, + "action": map[string]any{ + "type": "string", + "description": "Action to perform", + "enum": []any{"click", "fill", "select", "check", "uncheck"}, + }, + "value": map[string]any{ + "type": "string", + "description": "Value for the action (required for fill, select)", + }, + }, + "required": []string{"selector", "action"}, + }, + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"interactions"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + interactionsData, ok := params["interactions"].([]any) + if !ok { + return nil, fmt.Errorf("interactions parameter is required and must be an array") + } + + // Parse interactions + var interactions []client.InteractionItem + for _, interactionData := range interactionsData { + interactionMap, ok := interactionData.(map[string]any) + if !ok { + return nil, fmt.Errorf("each interaction must be an object") + } + + interaction := client.InteractionItem{} + + if selector, ok := interactionMap["selector"].(string); ok { + interaction.Selector = selector + } else { + return nil, fmt.Errorf("each interaction must have a selector") + } + + if action, ok := interactionMap["action"].(string); ok { + interaction.Action = action + } else { + return nil, fmt.Errorf("each interaction must have an action") + } + + if value, ok := interactionMap["value"].(string); ok { + interaction.Value = value + } + + interactions = append(interactions, interaction) + } + + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) - result, err := clientCtx.GetClient().GetPageInfo(tab, timeout) + result, err := cremoteServer.client.InteractMultiple(tab, interactions, timeout) if err != nil { - return nil, fmt.Errorf("failed to get page info: %w", err) + return nil, fmt.Errorf("failed to perform multiple interactions: %w", err) } resultJSON, err := json.MarshalIndent(result, "", " ") @@ -958,15 +1382,574 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("Multiple interactions result: %s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // File management tool handler - handlers["file_management_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Register web_form_fill_bulk tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_form_fill_bulk_cremotemcp", + Description: "Fill entire forms with key-value pairs", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "fields": map[string]any{ + "type": "object", + "description": "Map of field names/selectors to values", + }, + "form_selector": map[string]any{ + "type": "string", + "description": "CSS selector for the form element (optional)", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"fields"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + fieldsData, ok := params["fields"].(map[string]any) + if !ok { + return nil, fmt.Errorf("fields parameter is required and must be an object") + } + + // Convert fields to map[string]string + fields := make(map[string]string) + for key, value := range fieldsData { + if strValue, ok := value.(string); ok { + fields[key] = strValue + } else { + return nil, fmt.Errorf("all field values must be strings") + } + } + + formSelector := getStringParam(params, "form_selector", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + + result, err := cremoteServer.client.FillFormBulk(tab, formSelector, fields, timeout) + if err != nil { + return nil, fmt.Errorf("failed to fill form bulk: %w", err) + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Form bulk fill result: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Register web_page_info tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_page_info_cremotemcp", + Description: "Get comprehensive page metadata and state information", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab if not specified)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + "default": 5, + }, + }, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + tabID := getStringParam(params, "tab", "") + timeout := getIntParam(params, "timeout", 5) + + result, err := cremoteServer.client.GetPageInfo(tabID, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error getting page info: %v", err)), + }, + IsError: true, + }, nil + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Page info: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Register web_viewport_info tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_viewport_info_cremotemcp", + Description: "Get viewport and scroll information", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab if not specified)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + "default": 5, + }, + }, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + tabID := getStringParam(params, "tab", "") + timeout := getIntParam(params, "timeout", 5) + + result, err := cremoteServer.client.GetViewportInfo(tabID, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error getting viewport info: %v", err)), + }, + IsError: true, + }, nil + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Viewport info: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Register web_performance_metrics tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_performance_metrics_cremotemcp", + Description: "Get page performance metrics", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab if not specified)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + "default": 5, + }, + }, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + tabID := getStringParam(params, "tab", "") + timeout := getIntParam(params, "timeout", 5) + + result, err := cremoteServer.client.GetPerformance(tabID, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error getting performance metrics: %v", err)), + }, + IsError: true, + }, nil + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Performance metrics: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Register web_content_check tool + mcpServer.AddTool(mcp.Tool{ + Name: "web_content_check_cremotemcp", + Description: "Check for specific content types and loading states", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "type": map[string]any{ + "type": "string", + "description": "Content type to check", + "enum": []any{"images", "scripts", "styles", "forms", "links", "iframes", "errors"}, + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab if not specified)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + "default": 5, + }, + }, + Required: []string{"type"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + contentType := getStringParam(params, "type", "") + if contentType == "" { + return nil, fmt.Errorf("type parameter is required") + } + + tabID := getStringParam(params, "tab", "") + timeout := getIntParam(params, "timeout", 5) + + result, err := cremoteServer.client.CheckContent(tabID, contentType, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error checking content: %v", err)), + }, + IsError: true, + }, nil + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Content check result: %s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // Phase 5: Enhanced Screenshot and File Management Tools + + // web_screenshot_element_cremotemcp - Screenshot specific elements + mcpServer.AddTool(mcp.Tool{ + Name: "web_screenshot_element_cremotemcp", + Description: "Take a screenshot of a specific element on the page", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the element to screenshot", + }, + "output": map[string]any{ + "type": "string", + "description": "Path where to save the screenshot", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab if not specified)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + }, + }, + Required: []string{"selector", "output"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + selector := getStringParam(params, "selector", "") + output := getStringParam(params, "output", "") + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + + if selector == "" { + return nil, fmt.Errorf("selector parameter is required") + } + if output == "" { + return nil, fmt.Errorf("output parameter is required") + } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } + + err := cremoteServer.client.ScreenshotElement(tab, selector, output, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error taking element screenshot: %v", err)), + }, + IsError: true, + }, nil + } + + cremoteServer.screenshots = append(cremoteServer.screenshots, output) + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Element screenshot saved to: %s", output)), + }, + IsError: false, + }, nil + }) + + // web_screenshot_enhanced_cremotemcp - Enhanced screenshots with metadata + mcpServer.AddTool(mcp.Tool{ + Name: "web_screenshot_enhanced_cremotemcp", + Description: "Take an enhanced screenshot with metadata (timestamp, viewport size, URL)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "output": map[string]any{ + "type": "string", + "description": "Path where to save the screenshot", + }, + "full_page": map[string]any{ + "type": "boolean", + "description": "Capture full page (default: false)", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab if not specified)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 5)", + }, + }, + Required: []string{"output"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + output := getStringParam(params, "output", "") + fullPage := getBoolParam(params, "full_page", false) + tab := getStringParam(params, "tab", cremoteServer.currentTab) + timeout := getIntParam(params, "timeout", 5) + + if output == "" { + return nil, fmt.Errorf("output parameter is required") + } + if tab == "" { + return nil, fmt.Errorf("no tab available - navigate to a page first") + } + + metadata, err := cremoteServer.client.ScreenshotEnhanced(tab, output, fullPage, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error taking enhanced screenshot: %v", err)), + }, + IsError: true, + }, nil + } + + cremoteServer.screenshots = append(cremoteServer.screenshots, output) + + metadataJSON, err := json.MarshalIndent(metadata, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal metadata: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Enhanced screenshot saved with metadata:\n%s", string(metadataJSON))), + }, + IsError: false, + }, nil + }) + + // file_operations_bulk_cremotemcp - Bulk file operations + mcpServer.AddTool(mcp.Tool{ + Name: "file_operations_bulk_cremotemcp", + Description: "Perform bulk file operations (upload/download multiple files)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "operation": map[string]any{ + "type": "string", + "description": "Operation type", + "enum": []any{"upload", "download"}, + }, + "files": map[string]any{ + "type": "array", + "description": "Array of file operations", + "items": map[string]any{ + "type": "object", + "properties": map[string]any{ + "local_path": map[string]any{ + "type": "string", + "description": "Path on client machine", + }, + "container_path": map[string]any{ + "type": "string", + "description": "Path in container", + }, + "operation": map[string]any{ + "type": "string", + "description": "Override operation type for this file", + "enum": []any{"upload", "download"}, + }, + }, + "required": []any{"local_path", "container_path"}, + }, + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds (default: 30)", + }, + }, + Required: []string{"operation", "files"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map + params, ok := request.Params.Arguments.(map[string]any) + if !ok { + return nil, fmt.Errorf("invalid arguments format") + } + + operation := getStringParam(params, "operation", "") + filesParam := params["files"] + timeout := getIntParam(params, "timeout", 30) + + if operation == "" { + return nil, fmt.Errorf("operation parameter is required") + } + if filesParam == nil { + return nil, fmt.Errorf("files parameter is required") + } + + // Convert files parameter to FileOperation slice + filesArray, ok := filesParam.([]any) + if !ok { + return nil, fmt.Errorf("files must be an array") + } + + var operations []client.FileOperation + for _, fileItem := range filesArray { + fileMap, ok := fileItem.(map[string]any) + if !ok { + return nil, fmt.Errorf("each file must be an object") + } + + localPath := getStringParam(fileMap, "local_path", "") + containerPath := getStringParam(fileMap, "container_path", "") + fileOperation := getStringParam(fileMap, "operation", operation) + + if localPath == "" || containerPath == "" { + return nil, fmt.Errorf("local_path and container_path are required for each file") + } + + operations = append(operations, client.FileOperation{ + LocalPath: localPath, + ContainerPath: containerPath, + Operation: fileOperation, + }) + } + + result, err := cremoteServer.client.BulkFiles(operation, operations, timeout) + if err != nil { + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error performing bulk file operations: %v", err)), + }, + IsError: true, + }, nil + } + + resultJSON, err := json.MarshalIndent(result, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal result: %w", err) + } + + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Bulk file operations completed:\n%s", string(resultJSON))), + }, + IsError: false, + }, nil + }) + + // file_management_cremotemcp - File management operations + mcpServer.AddTool(mcp.Tool{ + Name: "file_management_cremotemcp", + Description: "Manage files (cleanup, list, get info)", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "operation": map[string]any{ + "type": "string", + "description": "Management operation", + "enum": []any{"cleanup", "list", "info"}, + }, + "pattern": map[string]any{ + "type": "string", + "description": "File pattern for cleanup/list, or file path for info", + }, + "max_age": map[string]any{ + "type": "string", + "description": "Max age in hours for cleanup (default: 24)", + }, + }, + Required: []string{"operation"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + // Convert arguments to map params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -980,9 +1963,14 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("operation parameter is required") } - result, err := clientCtx.GetClient().ManageFiles(operation, pattern, maxAge) + result, err := cremoteServer.client.ManageFiles(operation, pattern, maxAge) if err != nil { - return nil, fmt.Errorf("failed to manage files: %w", err) + return &mcp.CallToolResult{ + Content: []mcp.Content{ + mcp.NewTextContent(fmt.Sprintf("Error managing files: %v", err)), + }, + IsError: true, + }, nil } resultJSON, err := json.MarshalIndent(result, "", " ") @@ -992,23 +1980,43 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return &mcp.CallToolResult{ Content: []mcp.Content{ - mcp.NewTextContent(string(resultJSON)), + mcp.NewTextContent(fmt.Sprintf("File management result:\n%s", string(resultJSON))), }, IsError: false, }, nil - } + }) - // Accessibility tree tool handlers - - // Get full accessibility tree - handlers["get_accessibility_tree_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + // Accessibility tree tools + mcpServer.AddTool(mcp.Tool{ + Name: "get_accessibility_tree_cremotemcp", + Description: "Get the full accessibility tree for a page or with limited depth", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "depth": map[string]any{ + "type": "integer", + "description": "Maximum depth to retrieve (optional, omit for full tree)", + "minimum": 0, + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) // Parse depth parameter @@ -1017,7 +2025,7 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context depth = &depthParam } - result, err := clientCtx.GetClient().GetAccessibilityTree(tab, depth, timeout) + result, err := cremoteServer.client.GetAccessibilityTree(tab, depth, timeout) if err != nil { return nil, fmt.Errorf("failed to get accessibility tree: %w", err) } @@ -1033,11 +2041,36 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } + }) - // Get partial accessibility tree for specific element - handlers["get_partial_accessibility_tree_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + mcpServer.AddTool(mcp.Tool{ + Name: "get_partial_accessibility_tree_cremotemcp", + Description: "Get accessibility tree for a specific element and its relatives", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "selector": map[string]any{ + "type": "string", + "description": "CSS selector for the target element", + }, + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "fetch_relatives": map[string]any{ + "type": "boolean", + "description": "Whether to fetch ancestors, siblings, and children", + "default": true, + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{"selector"}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") @@ -1048,11 +2081,11 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("selector parameter is required") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) fetchRelatives := getBoolParam(params, "fetch_relatives", true) - result, err := clientCtx.GetClient().GetPartialAccessibilityTree(tab, selector, fetchRelatives, timeout) + result, err := cremoteServer.client.GetPartialAccessibilityTree(tab, selector, fetchRelatives, timeout) if err != nil { return nil, fmt.Errorf("failed to get partial accessibility tree: %w", err) } @@ -1068,17 +2101,45 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } + }) - // Query accessibility tree for nodes matching criteria - handlers["query_accessibility_tree_cremotemcp"] = func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - clientCtx := getContext() + mcpServer.AddTool(mcp.Tool{ + Name: "query_accessibility_tree_cremotemcp", + Description: "Query accessibility tree for nodes matching specific criteria", + InputSchema: mcp.ToolInputSchema{ + Type: "object", + Properties: map[string]any{ + "tab": map[string]any{ + "type": "string", + "description": "Tab ID (optional, uses current tab)", + }, + "selector": map[string]any{ + "type": "string", + "description": "CSS selector to limit search scope (optional)", + }, + "accessible_name": map[string]any{ + "type": "string", + "description": "Accessible name to match (optional)", + }, + "role": map[string]any{ + "type": "string", + "description": "ARIA role to match (optional)", + }, + "timeout": map[string]any{ + "type": "integer", + "description": "Timeout in seconds", + "default": 5, + }, + }, + Required: []string{}, + }, + }, func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { params, ok := request.Params.Arguments.(map[string]any) if !ok { return nil, fmt.Errorf("invalid arguments format") } - tab := getStringParam(params, "tab", clientCtx.GetCurrentTab()) + tab := getStringParam(params, "tab", cremoteServer.currentTab) timeout := getIntParam(params, "timeout", 5) selector := getStringParam(params, "selector", "") accessibleName := getStringParam(params, "accessible_name", "") @@ -1089,7 +2150,7 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context return nil, fmt.Errorf("at least one search criteria (selector, accessible_name, or role) must be provided") } - result, err := clientCtx.GetClient().QueryAccessibilityTree(tab, selector, accessibleName, role, timeout) + result, err := cremoteServer.client.QueryAccessibilityTree(tab, selector, accessibleName, role, timeout) if err != nil { return nil, fmt.Errorf("failed to query accessibility tree: %w", err) } @@ -1105,477 +2166,11 @@ func createToolHandlers(getContext func() ClientContext) map[string]func(context }, IsError: false, }, nil - } - - return handlers -} - -// registerAllTools registers all tools with their schemas -func registerAllTools(mcpServer *server.MCPServer, handlers map[string]func(context.Context, mcp.CallToolRequest) (*mcp.CallToolResult, error)) { - // Version tool - mcpServer.AddTool(mcp.Tool{ - Name: "version_cremotemcp", - Description: "Get version information for MCP server and daemon", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{}, - Required: []string{}, - }, - }, handlers["version_cremotemcp"]) - - // Navigation tools - mcpServer.AddTool(mcp.Tool{ - Name: "web_navigate_cremotemcp", - Description: "Navigate to a URL and optionally take a screenshot", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "url": map[string]any{ - "type": "string", - "description": "URL to navigate to", - }, - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional, uses current tab)", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds (default: 5)", - "default": 5, - }, - "screenshot": map[string]any{ - "type": "boolean", - "description": "Take screenshot after navigation", - }, - }, - Required: []string{"url"}, - }, - }, handlers["web_navigate_cremotemcp"]) - - // Legacy navigation tool - mcpServer.AddTool(mcp.Tool{ - Name: "web_navigate", - Description: "Navigate to a URL and optionally take a screenshot", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "url": map[string]any{ - "type": "string", - "description": "URL to navigate to", - }, - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional, uses current tab)", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds (default: 5)", - "default": 5, - }, - "screenshot": map[string]any{ - "type": "boolean", - "description": "Take screenshot after navigation", - }, - }, - Required: []string{"url"}, - }, - }, handlers["web_navigate"]) - - // Interaction tool - mcpServer.AddTool(mcp.Tool{ - Name: "web_interact", - Description: "Interact with web elements (click, fill, submit)", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "action": map[string]any{ - "type": "string", - "description": "Action to perform", - "enum": []string{"click", "fill", "submit", "select"}, - }, - "selector": map[string]any{ - "type": "string", - "description": "CSS selector for the element", - }, - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional)", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds", - "default": 5, - }, - "value": map[string]any{ - "type": "string", - "description": "Value to fill (for fill/select actions)", - }, - }, - Required: []string{"action", "selector"}, - }, - }, handlers["web_interact"]) - - // Add more key tools - if handler, exists := handlers["web_extract"]; exists { - mcpServer.AddTool(mcp.Tool{ - Name: "web_extract", - Description: "Extract data from the page (source, element HTML, or execute JavaScript)", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "type": map[string]any{ - "type": "string", - "description": "Type of extraction", - "enum": []string{"source", "element", "javascript"}, - }, - "selector": map[string]any{ - "type": "string", - "description": "CSS selector (for element type)", - }, - "code": map[string]any{ - "type": "string", - "description": "JavaScript code (for javascript type)", - }, - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional)", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds", - "default": 5, - }, - }, - Required: []string{"type"}, - }, - }, handler) - } - - if handler, exists := handlers["web_screenshot"]; exists { - mcpServer.AddTool(mcp.Tool{ - Name: "web_screenshot", - Description: "Take a screenshot of the current page", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "output": map[string]any{ - "type": "string", - "description": "Output file path", - }, - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional)", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds", - "default": 5, - }, - "full_page": map[string]any{ - "type": "boolean", - "description": "Capture full page", - "default": false, - }, - }, - Required: []string{"output"}, - }, - }, handler) - } - - // Accessibility tree tools - if handler, exists := handlers["get_accessibility_tree_cremotemcp"]; exists { - mcpServer.AddTool(mcp.Tool{ - Name: "get_accessibility_tree_cremotemcp", - Description: "Get the full accessibility tree for a page or with limited depth", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional, uses current tab)", - }, - "depth": map[string]any{ - "type": "integer", - "description": "Maximum depth to retrieve (optional, omit for full tree)", - "minimum": 0, - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds", - "default": 5, - }, - }, - Required: []string{}, - }, - }, handler) - } - - if handler, exists := handlers["get_partial_accessibility_tree_cremotemcp"]; exists { - mcpServer.AddTool(mcp.Tool{ - Name: "get_partial_accessibility_tree_cremotemcp", - Description: "Get accessibility tree for a specific element and its relatives", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "selector": map[string]any{ - "type": "string", - "description": "CSS selector for the target element", - }, - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional, uses current tab)", - }, - "fetch_relatives": map[string]any{ - "type": "boolean", - "description": "Whether to fetch ancestors, siblings, and children", - "default": true, - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds", - "default": 5, - }, - }, - Required: []string{"selector"}, - }, - }, handler) - } - - if handler, exists := handlers["query_accessibility_tree_cremotemcp"]; exists { - mcpServer.AddTool(mcp.Tool{ - Name: "query_accessibility_tree_cremotemcp", - Description: "Query accessibility tree for nodes matching specific criteria", - InputSchema: mcp.ToolInputSchema{ - Type: "object", - Properties: map[string]any{ - "tab": map[string]any{ - "type": "string", - "description": "Tab ID (optional, uses current tab)", - }, - "selector": map[string]any{ - "type": "string", - "description": "CSS selector to limit search scope (optional)", - }, - "accessible_name": map[string]any{ - "type": "string", - "description": "Accessible name to match (optional)", - }, - "role": map[string]any{ - "type": "string", - "description": "ARIA role to match (optional)", - }, - "timeout": map[string]any{ - "type": "integer", - "description": "Timeout in seconds", - "default": 5, - }, - }, - Required: []string{}, - }, - }, handler) - } -} - -func main() { - // Get cremote daemon connection settings - cremoteHost := os.Getenv("CREMOTE_HOST") - if cremoteHost == "" { - cremoteHost = "localhost" - } - - cremotePortStr := os.Getenv("CREMOTE_PORT") - cremotePort := 8989 - if cremotePortStr != "" { - if p, err := strconv.Atoi(cremotePortStr); err == nil { - cremotePort = p - } - } - - // Get transport mode (stdio or http) - transportMode := os.Getenv("CREMOTE_TRANSPORT") - if transportMode == "" { - transportMode = "stdio" // Default to stdio for backward compatibility - } - - log.Printf("Starting cremote MCP server, transport: %s, connecting to cremote daemon at %s:%d", transportMode, cremoteHost, cremotePort) - - if transportMode == "http" { - // Multi-client HTTP mode - sessionManager := NewSessionManager(cremoteHost, cremotePort) - startHTTPServer(sessionManager) - } else { - // Single-client stdio mode (legacy) - cremoteServer := NewCremoteServer(cremoteHost, cremotePort) - startStdioServer(cremoteServer) - } -} - -func startStdioServer(cremoteServer *CremoteServer) { - // Create MCP server - mcpServer := server.NewMCPServer("cremote-mcp", "2.0.0") - - // Create session-aware tool handlers - toolHandlers := createToolHandlers(func() ClientContext { - return cremoteServer }) - // Register all tools - registerAllTools(mcpServer, toolHandlers) - // Start the server - log.Printf("Cremote MCP server ready (stdio mode)") + log.Printf("Cremote MCP server ready") if err := server.ServeStdio(mcpServer); err != nil { log.Fatalf("Server error: %v", err) } } - -func startHTTPServer(sessionManager *SessionManager) { - // Start session cleanup routine - go func() { - ticker := time.NewTicker(5 * time.Minute) - defer ticker.Stop() - for { - select { - case <-ticker.C: - sessionManager.CleanupExpiredSessions(30 * time.Minute) - } - } - }() - - // Get HTTP server configuration - httpHost := os.Getenv("CREMOTE_HTTP_HOST") - if httpHost == "" { - httpHost = "localhost" - } - - httpPortStr := os.Getenv("CREMOTE_HTTP_PORT") - httpPort := 8990 - if httpPortStr != "" { - if p, err := strconv.Atoi(httpPortStr); err == nil { - httpPort = p - } - } - - // Create HTTP server - mux := http.NewServeMux() - mux.HandleFunc("/mcp", func(w http.ResponseWriter, r *http.Request) { - handleMCPRequest(w, r, sessionManager) - }) - - server := &http.Server{ - Addr: fmt.Sprintf("%s:%d", httpHost, httpPort), - Handler: mux, - } - - log.Printf("Cremote MCP HTTP server ready on %s", server.Addr) - if err := server.ListenAndServe(); err != nil { - log.Fatalf("HTTP server error: %v", err) - } -} - -// handleMCPRequest handles HTTP MCP requests with session management -func handleMCPRequest(w http.ResponseWriter, r *http.Request, sessionManager *SessionManager) { - // Set CORS headers - w.Header().Set("Access-Control-Allow-Origin", "*") - w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS") - w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id, MCP-Protocol-Version") - - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - - // Get or create session - sessionID := r.Header.Get("Mcp-Session-Id") - var session *ClientSession - var isNewSession bool - - if sessionID == "" { - // Create new session for initialization - session = sessionManager.CreateSession() - isNewSession = true - w.Header().Set("Mcp-Session-Id", session.ID) - } else { - // Get existing session - var exists bool - session, exists = sessionManager.GetSession(sessionID) - if !exists { - http.Error(w, "Session not found", http.StatusNotFound) - return - } - } - - // Handle different HTTP methods according to MCP Streamable HTTP spec - switch r.Method { - case "POST": - handleMCPPost(w, r, session, isNewSession) - case "GET": - handleMCPGet(w, r, session) - case "DELETE": - handleMCPDelete(w, r, sessionManager, sessionID) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } -} - -// handleMCPPost handles POST requests (JSON-RPC messages) -func handleMCPPost(w http.ResponseWriter, r *http.Request, session *ClientSession, isNewSession bool) { - // Read request body - var jsonRPCRequest map[string]interface{} - if err := json.NewDecoder(r.Body).Decode(&jsonRPCRequest); err != nil { - http.Error(w, "Invalid JSON", http.StatusBadRequest) - return - } - - // For now, implement a basic response for initialization - // TODO: Implement full JSON-RPC handling with session-aware tools - method, ok := jsonRPCRequest["method"].(string) - if !ok { - http.Error(w, "Missing method", http.StatusBadRequest) - return - } - - if method == "initialize" { - // Handle initialization - response := map[string]interface{}{ - "jsonrpc": "2.0", - "id": jsonRPCRequest["id"], - "result": map[string]interface{}{ - "protocolVersion": "2025-06-18", - "capabilities": map[string]interface{}{ - "tools": map[string]interface{}{}, - }, - "serverInfo": map[string]interface{}{ - "name": "cremote-mcp", - "version": Version, - }, - }, - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(response) - return - } - - // For other methods, return not implemented for now - w.WriteHeader(http.StatusNotImplemented) - w.Write([]byte("Method not yet implemented in HTTP transport")) -} - -// handleMCPGet handles GET requests (SSE streams) -func handleMCPGet(w http.ResponseWriter, r *http.Request, session *ClientSession) { - // For now, return method not allowed as we don't implement SSE yet - http.Error(w, "SSE not yet implemented", http.StatusMethodNotAllowed) -} - -// handleMCPDelete handles DELETE requests (session termination) -func handleMCPDelete(w http.ResponseWriter, r *http.Request, sessionManager *SessionManager, sessionID string) { - if sessionID == "" { - http.Error(w, "No session to delete", http.StatusBadRequest) - return - } - - sessionManager.RemoveSession(sessionID) - w.WriteHeader(http.StatusOK) - w.Write([]byte("Session terminated")) -} diff --git a/tests/test_accessibility b/tests/test_accessibility new file mode 100755 index 0000000..bcb0cb8 Binary files /dev/null and b/tests/test_accessibility differ diff --git a/tools.txt b/tools.txt new file mode 100644 index 0000000..9558691 --- /dev/null +++ b/tools.txt @@ -0,0 +1,52 @@ + +Cremote MCP Tools (27 Web Automation Tools): + +Core Web Automation (10 tools): + + * console_command_cremotemcp - Execute JavaScript commands in browser +console + * console_logs_cremotemcp - Get console logs from browser tab + * file_download_cremotemcp - Download files from container to client + * file_upload_cremotemcp - Upload files from client to container + * web_extract_cremotemcp - Extract data from pages (source, element, +JavaScript) + * web_interact_cremotemcp - Interact with elements (click, fill, +submit, upload, select) + * web_iframe_cremotemcp - Switch iframe context + * web_manage_tabs_cremotemcp - Manage browser tabs (open, close, list, +switch) + * web_navigate_cremotemcp - Navigate to URLs with optional screenshots + * web_screenshot_cremotemcp - Take screenshots of pages + +Enhanced Tools (17 additional tools): + + * file_management_cremotemcp_cremotemcp - File management (cleanup, +list, info) + * file_operations_bulk_cremotemcp_cremotemcp - Bulk file operations + * version_cremotemcp_cremotemcp - Get MCP server version info + * web_content_check_cremotemcp_cremotemcp - Check content types and +loading states + * web_element_attributes_cremotemcp_cremotemcp - Get element +attributes and properties + * web_element_check_cremotemcp_cremotemcp - Check element existence, +visibility, state + * web_extract_links_cremotemcp_cremotemcp - Extract all links with +filtering + * web_extract_multiple_cremotemcp_cremotemcp - Extract from multiple +selectors + * web_extract_table_cremotemcp_cremotemcp - Extract table data as JSON + * web_extract_text_cremotemcp_cremotemcp - Extract text with pattern +matching + * web_form_analyze_cremotemcp_cremotemcp - Analyze forms completely + * web_form_fill_bulk_cremotemcp_cremotemcp - Fill entire forms with +key-value pairs + * web_interact_multiple_cremotemcp_cremotemcp - Batch interactions + * web_page_info_cremotemcp_cremotemcp - Get page metadata and state + * web_performance_metrics_cremotemcp_cremotemcp - Get performance +metrics + * web_screenshot_element_cremotemcp_cremotemcp - Screenshot specific +elements + * web_screenshot_enhanced_cremotemcp_cremotemcp - Enhanced screenshots +with metadata + * web_viewport_info_cremotemcp_cremotemcp - Get viewport and scroll +information