package main import ( "context" "encoding/base64" "encoding/json" "fmt" "log" "net/http" "sync" "forge.lthn.ai/core/gui/pkg/display" "forge.lthn.ai/core/gui/pkg/mcp" "forge.lthn.ai/core/gui/pkg/webview" "forge.lthn.ai/core/gui/pkg/ws" "github.com/wailsapp/wails/v3/pkg/application" ) // MCPBridge wires together MCP, WebView, Display and WebSocket services // and starts the MCP HTTP server after Wails initializes. type MCPBridge struct { mcpService *mcp.Service webview *webview.Service display *display.Service wsHub *ws.Hub claudeBridge *ClaudeBridge app *application.App port int running bool mu sync.Mutex } // NewMCPBridge creates a new MCP bridge with all services wired up. func NewMCPBridge(port int, displaySvc *display.Service) *MCPBridge { wv := webview.New() hub := ws.NewHub() mcpSvc := mcp.NewStandaloneWithPort(port) mcpSvc.SetWebView(wv) mcpSvc.SetDisplay(displaySvc) // Create Claude bridge to forward messages to MCP core on port 9876 claudeBridge := NewClaudeBridge("ws://localhost:9876/ws") return &MCPBridge{ mcpService: mcpSvc, webview: wv, display: displaySvc, wsHub: hub, claudeBridge: claudeBridge, port: port, } } // ServiceStartup is called by Wails when the app starts. // This wires up the app reference and starts the HTTP server. func (b *MCPBridge) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { b.mu.Lock() defer b.mu.Unlock() // Get the Wails app reference b.app = application.Get() if b.app == nil { return fmt.Errorf("failed to get Wails app reference") } // Wire up the WebView service with the app b.webview.SetApp(b.app) // Set up console listener b.webview.SetupConsoleListener() // Inject console capture into all windows after a short delay // (windows may not be created yet) go b.injectConsoleCapture() // Start the HTTP server for MCP go b.startHTTPServer() log.Printf("MCP Bridge started on port %d", b.port) return nil } // injectConsoleCapture injects the console capture script into windows. func (b *MCPBridge) injectConsoleCapture() { // Wait a bit for windows to be created // In production, you'd use events to detect window creation windows := b.webview.ListWindows() for _, w := range windows { if err := b.webview.InjectConsoleCapture(w.Name); err != nil { log.Printf("Failed to inject console capture in %s: %v", w.Name, err) } } } // startHTTPServer starts the HTTP server for MCP and WebSocket. func (b *MCPBridge) startHTTPServer() { b.running = true // Start the WebSocket hub hubCtx := context.Background() go b.wsHub.Run(hubCtx) // Claude bridge disabled - port 9876 is not an MCP WebSocket server // b.claudeBridge.Start() mux := http.NewServeMux() // WebSocket endpoint for GUI clients mux.HandleFunc("/ws", b.wsHub.HandleWebSocket) // WebSocket endpoint for real-time display events mux.HandleFunc("/events", b.handleEventsWebSocket) // MCP info endpoint mux.HandleFunc("/mcp", b.handleMCPInfo) // MCP tools endpoint (simple HTTP for now, SSE later) mux.HandleFunc("/mcp/tools", b.handleMCPTools) mux.HandleFunc("/mcp/call", b.handleMCPCall) // Health check mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]any{ "status": "ok", "mcp": true, "webview": b.webview != nil, "display": b.display != nil, }) }) addr := fmt.Sprintf(":%d", b.port) log.Printf("MCP HTTP server listening on %s", addr) if err := http.ListenAndServe(addr, mux); err != nil { log.Printf("MCP HTTP server error: %v", err) } } // handleMCPInfo returns MCP server information. func (b *MCPBridge) handleMCPInfo(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") info := map[string]any{ "name": "core", "version": "0.1.0", "capabilities": map[string]any{ "webview": true, "display": b.display != nil, "windowControl": b.display != nil, "screenControl": b.display != nil, "websocket": fmt.Sprintf("ws://localhost:%d/ws", b.port), "events": fmt.Sprintf("ws://localhost:%d/events", b.port), }, } json.NewEncoder(w).Encode(info) } // handleMCPTools returns the list of available tools. func (b *MCPBridge) handleMCPTools(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") // Return tool list - grouped by category tools := []map[string]string{ // File operations {"name": "file_read", "description": "Read the contents of a file"}, {"name": "file_write", "description": "Write content to a file"}, {"name": "file_edit", "description": "Edit a file by replacing text"}, {"name": "file_delete", "description": "Delete a file"}, {"name": "file_exists", "description": "Check if file exists"}, {"name": "file_rename", "description": "Rename or move a file"}, {"name": "dir_list", "description": "List directory contents"}, {"name": "dir_create", "description": "Create a directory"}, {"name": "lang_detect", "description": "Detect file language"}, {"name": "lang_list", "description": "List supported languages"}, // Process management {"name": "process_start", "description": "Start a process"}, {"name": "process_stop", "description": "Stop a process"}, {"name": "process_kill", "description": "Kill a process"}, {"name": "process_list", "description": "List processes"}, {"name": "process_output", "description": "Get process output"}, {"name": "process_input", "description": "Send input to process"}, // WebSocket streaming {"name": "ws_start", "description": "Start WebSocket server"}, {"name": "ws_info", "description": "Get WebSocket info"}, // WebView interaction (JS runtime, console, DOM) {"name": "webview_list", "description": "List windows"}, {"name": "webview_eval", "description": "Execute JavaScript"}, {"name": "webview_console", "description": "Get console messages"}, {"name": "webview_console_clear", "description": "Clear console buffer"}, {"name": "webview_click", "description": "Click element"}, {"name": "webview_type", "description": "Type into element"}, {"name": "webview_query", "description": "Query DOM elements"}, {"name": "webview_navigate", "description": "Navigate to URL"}, {"name": "webview_source", "description": "Get page source"}, {"name": "webview_url", "description": "Get current page URL"}, {"name": "webview_title", "description": "Get current page title"}, {"name": "webview_screenshot", "description": "Capture page as base64 PNG"}, {"name": "webview_screenshot_element", "description": "Capture specific element as PNG"}, {"name": "webview_scroll", "description": "Scroll to element or position"}, {"name": "webview_hover", "description": "Hover over element"}, {"name": "webview_select", "description": "Select option in dropdown"}, {"name": "webview_check", "description": "Check/uncheck checkbox or radio"}, {"name": "webview_element_info", "description": "Get detailed info about element"}, {"name": "webview_computed_style", "description": "Get computed styles for element"}, {"name": "webview_highlight", "description": "Visually highlight element"}, {"name": "webview_dom_tree", "description": "Get DOM tree structure"}, {"name": "webview_errors", "description": "Get captured error messages"}, {"name": "webview_performance", "description": "Get performance metrics"}, {"name": "webview_resources", "description": "List loaded resources"}, {"name": "webview_network", "description": "Get network requests log"}, {"name": "webview_network_clear", "description": "Clear network request log"}, {"name": "webview_network_inject", "description": "Inject network interceptor for detailed logging"}, {"name": "webview_pdf", "description": "Export page as PDF (base64 data URI)"}, {"name": "webview_print", "description": "Open print dialog for window"}, // Window/Display control (native app control) {"name": "window_list", "description": "List all windows with positions"}, {"name": "window_get", "description": "Get info about a specific window"}, {"name": "window_create", "description": "Create a new window at specific position"}, {"name": "window_close", "description": "Close a window by name"}, {"name": "window_position", "description": "Move a window to specific coordinates"}, {"name": "window_size", "description": "Resize a window"}, {"name": "window_bounds", "description": "Set position and size in one call"}, {"name": "window_maximize", "description": "Maximize a window"}, {"name": "window_minimize", "description": "Minimize a window"}, {"name": "window_restore", "description": "Restore from maximized/minimized"}, {"name": "window_focus", "description": "Bring window to front"}, {"name": "window_focused", "description": "Get currently focused window"}, {"name": "window_visibility", "description": "Show or hide a window"}, {"name": "window_always_on_top", "description": "Pin window above others"}, {"name": "window_title", "description": "Change window title"}, {"name": "window_title_get", "description": "Get current window title"}, {"name": "window_fullscreen", "description": "Toggle fullscreen mode"}, {"name": "screen_list", "description": "List all screens/monitors"}, {"name": "screen_get", "description": "Get specific screen by ID"}, {"name": "screen_primary", "description": "Get primary screen info"}, {"name": "screen_at_point", "description": "Get screen containing a point"}, {"name": "screen_for_window", "description": "Get screen a window is on"}, {"name": "screen_work_areas", "description": "Get usable screen space (excluding dock/menubar)"}, // Layout management {"name": "layout_save", "description": "Save current window arrangement with a name"}, {"name": "layout_restore", "description": "Restore a saved layout by name"}, {"name": "layout_list", "description": "List all saved layouts"}, {"name": "layout_delete", "description": "Delete a saved layout"}, {"name": "layout_get", "description": "Get details of a specific layout"}, {"name": "layout_tile", "description": "Auto-tile windows (left/right/grid/quadrants)"}, {"name": "layout_snap", "description": "Snap window to screen edge/corner"}, {"name": "layout_stack", "description": "Stack windows in cascade pattern"}, {"name": "layout_workflow", "description": "Apply preset workflow layout (coding/debugging/presenting)"}, // System tray {"name": "tray_set_icon", "description": "Set system tray icon"}, {"name": "tray_set_tooltip", "description": "Set system tray tooltip"}, {"name": "tray_set_label", "description": "Set system tray label"}, {"name": "tray_set_menu", "description": "Set system tray menu items"}, {"name": "tray_info", "description": "Get system tray info"}, // Window background colour (for transparency) {"name": "window_background_colour", "description": "Set window background colour with alpha"}, // System integration {"name": "clipboard_read", "description": "Read text from system clipboard"}, {"name": "clipboard_write", "description": "Write text to system clipboard"}, {"name": "clipboard_has", "description": "Check if clipboard has content"}, {"name": "clipboard_clear", "description": "Clear the clipboard"}, {"name": "notification_show", "description": "Show native system notification"}, {"name": "notification_permission_request", "description": "Request notification permission"}, {"name": "notification_permission_check", "description": "Check notification permission status"}, {"name": "theme_get", "description": "Get current system theme (dark/light)"}, {"name": "theme_system", "description": "Get system theme preference"}, {"name": "focus_set", "description": "Set focus to specific window"}, // Dialogs {"name": "dialog_open_file", "description": "Show file open dialog"}, {"name": "dialog_save_file", "description": "Show file save dialog"}, {"name": "dialog_open_directory", "description": "Show directory picker"}, {"name": "dialog_confirm", "description": "Show confirmation dialog (yes/no)"}, {"name": "dialog_prompt", "description": "Show input prompt dialog (not supported natively)"}, // Event subscriptions (WebSocket) {"name": "event_info", "description": "Get WebSocket event server info and connected clients"}, } json.NewEncoder(w).Encode(map[string]any{"tools": tools}) } // handleMCPCall handles tool calls via HTTP POST. // This provides a REST bridge for display/window tools. func (b *MCPBridge) handleMCPCall(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } if r.Method != "POST" { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var req struct { Tool string `json:"tool"` Params map[string]any `json:"params"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } // Execute tools based on prefix var result map[string]any if len(req.Tool) > 8 && req.Tool[:8] == "webview_" { result = b.executeWebviewTool(req.Tool, req.Params) } else { result = b.executeDisplayTool(req.Tool, req.Params) } json.NewEncoder(w).Encode(result) } // executeDisplayTool handles window and screen tool execution. func (b *MCPBridge) executeDisplayTool(tool string, params map[string]any) map[string]any { if b.display == nil { return map[string]any{"error": "display service not available"} } switch tool { case "window_list": windows := b.display.ListWindowInfos() return map[string]any{"windows": windows} case "window_get": name, _ := params["name"].(string) info, err := b.display.GetWindowInfo(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"window": info} case "window_position": name, _ := params["name"].(string) x, _ := params["x"].(float64) y, _ := params["y"].(float64) err := b.display.SetWindowPosition(name, int(x), int(y)) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "name": name, "x": int(x), "y": int(y)} case "window_size": name, _ := params["name"].(string) width, _ := params["width"].(float64) height, _ := params["height"].(float64) err := b.display.SetWindowSize(name, int(width), int(height)) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "name": name, "width": int(width), "height": int(height)} case "window_bounds": name, _ := params["name"].(string) x, _ := params["x"].(float64) y, _ := params["y"].(float64) width, _ := params["width"].(float64) height, _ := params["height"].(float64) err := b.display.SetWindowBounds(name, int(x), int(y), int(width), int(height)) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "name": name, "x": int(x), "y": int(y), "width": int(width), "height": int(height)} case "window_maximize": name, _ := params["name"].(string) err := b.display.MaximizeWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "action": "maximize"} case "window_minimize": name, _ := params["name"].(string) err := b.display.MinimizeWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "action": "minimize"} case "window_restore": name, _ := params["name"].(string) err := b.display.RestoreWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "action": "restore"} case "window_focus": name, _ := params["name"].(string) err := b.display.FocusWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "action": "focus"} case "screen_list": screens := b.display.GetScreens() return map[string]any{"screens": screens} case "screen_get": id := getStringParam(params, "id") screen, err := b.display.GetScreen(id) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"screen": screen} case "screen_primary": screen, err := b.display.GetPrimaryScreen() if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"screen": screen} case "screen_at_point": x := getIntParam(params, "x") y := getIntParam(params, "y") screen, err := b.display.GetScreenAtPoint(x, y) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"screen": screen} case "screen_for_window": name := getStringParam(params, "name") screen, err := b.display.GetScreenForWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"screen": screen} case "window_create": opts := display.CreateWindowOptions{ Name: getStringParam(params, "name"), Title: getStringParam(params, "title"), URL: getStringParam(params, "url"), X: getIntParam(params, "x"), Y: getIntParam(params, "y"), Width: getIntParam(params, "width"), Height: getIntParam(params, "height"), } info, err := b.display.CreateWindow(opts) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "window": info} case "window_close": name, _ := params["name"].(string) err := b.display.CloseWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "action": "close"} case "window_visibility": name, _ := params["name"].(string) visible, _ := params["visible"].(bool) err := b.display.SetWindowVisibility(name, visible) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "visible": visible} case "window_always_on_top": name, _ := params["name"].(string) onTop, _ := params["onTop"].(bool) err := b.display.SetWindowAlwaysOnTop(name, onTop) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "alwaysOnTop": onTop} case "window_title": name, _ := params["name"].(string) title, _ := params["title"].(string) err := b.display.SetWindowTitle(name, title) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "title": title} case "window_title_get": name := getStringParam(params, "name") title, err := b.display.GetWindowTitle(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"title": title} case "window_fullscreen": name, _ := params["name"].(string) fullscreen, _ := params["fullscreen"].(bool) err := b.display.SetWindowFullscreen(name, fullscreen) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "fullscreen": fullscreen} case "screen_work_areas": areas := b.display.GetWorkAreas() return map[string]any{"workAreas": areas} case "window_focused": name := b.display.GetFocusedWindow() return map[string]any{"focused": name} case "layout_save": name, _ := params["name"].(string) err := b.display.SaveLayout(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "name": name} case "layout_restore": name, _ := params["name"].(string) err := b.display.RestoreLayout(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "name": name} case "layout_list": layouts := b.display.ListLayouts() return map[string]any{"layouts": layouts} case "layout_delete": name, _ := params["name"].(string) err := b.display.DeleteLayout(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "name": name} case "layout_get": name, _ := params["name"].(string) layout := b.display.GetLayout(name) if layout == nil { return map[string]any{"error": "layout not found", "name": name} } return map[string]any{"layout": layout} case "layout_tile": mode := getStringParam(params, "mode") var windowNames []string if names, ok := params["windows"].([]any); ok { for _, n := range names { if s, ok := n.(string); ok { windowNames = append(windowNames, s) } } } err := b.display.TileWindows(display.TileMode(mode), windowNames) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "mode": mode} case "layout_snap": name := getStringParam(params, "name") position := getStringParam(params, "position") err := b.display.SnapWindow(name, display.SnapPosition(position)) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "position": position} case "layout_stack": var windowNames []string if names, ok := params["windows"].([]any); ok { for _, n := range names { if s, ok := n.(string); ok { windowNames = append(windowNames, s) } } } offsetX := getIntParam(params, "offsetX") offsetY := getIntParam(params, "offsetY") err := b.display.StackWindows(windowNames, offsetX, offsetY) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "layout_workflow": workflow := getStringParam(params, "workflow") err := b.display.ApplyWorkflowLayout(display.WorkflowType(workflow)) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true, "workflow": workflow} case "tray_set_tooltip": tooltip := getStringParam(params, "tooltip") err := b.display.SetTrayTooltip(tooltip) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "tray_set_label": label := getStringParam(params, "label") err := b.display.SetTrayLabel(label) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "tray_set_icon": // Icon data as base64 encoded PNG iconBase64 := getStringParam(params, "icon") if iconBase64 == "" { return map[string]any{"error": "icon data required"} } // Decode base64 iconData, err := base64.StdEncoding.DecodeString(iconBase64) if err != nil { return map[string]any{"error": "invalid base64 icon data: " + err.Error()} } err = b.display.SetTrayIcon(iconData) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "tray_set_menu": // Menu items as JSON array var items []display.TrayMenuItem if menuData, ok := params["menu"].([]any); ok { menuJSON, _ := json.Marshal(menuData) json.Unmarshal(menuJSON, &items) } err := b.display.SetTrayMenu(items) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "tray_info": info := b.display.GetTrayInfo() return info case "window_background_colour": name := getStringParam(params, "name") r := uint8(getIntParam(params, "r")) g := uint8(getIntParam(params, "g")) b_val := uint8(getIntParam(params, "b")) a := uint8(getIntParam(params, "a")) if a == 0 { a = 255 // Default to opaque } err := b.display.SetWindowBackgroundColour(name, r, g, b_val, a) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "clipboard_read": text, err := b.display.ReadClipboard() if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"text": text} case "clipboard_write": text, _ := params["text"].(string) err := b.display.WriteClipboard(text) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "clipboard_has": has := b.display.HasClipboard() return map[string]any{"hasContent": has} case "clipboard_clear": err := b.display.ClearClipboard() if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "notification_show": title := getStringParam(params, "title") message := getStringParam(params, "message") subtitle := getStringParam(params, "subtitle") id := getStringParam(params, "id") err := b.display.ShowNotification(display.NotificationOptions{ ID: id, Title: title, Message: message, Subtitle: subtitle, }) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "notification_permission_request": granted, err := b.display.RequestNotificationPermission() if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"granted": granted} case "notification_permission_check": authorized, err := b.display.CheckNotificationPermission() if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"authorized": authorized} case "theme_get": theme := b.display.GetTheme() return map[string]any{"theme": theme} case "theme_system": theme := b.display.GetSystemTheme() return map[string]any{"theme": theme} case "focus_set": name := getStringParam(params, "name") err := b.display.FocusWindow(name) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "dialog_open_file": title := getStringParam(params, "title") defaultDir := getStringParam(params, "defaultDirectory") multiple, _ := params["allowMultiple"].(bool) opts := display.OpenFileOptions{ Title: title, DefaultDirectory: defaultDir, AllowMultiple: multiple, } if multiple { paths, err := b.display.OpenFileDialog(opts) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"paths": paths} } path, err := b.display.OpenSingleFileDialog(opts) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"path": path} case "dialog_save_file": title := getStringParam(params, "title") defaultDir := getStringParam(params, "defaultDirectory") defaultFilename := getStringParam(params, "defaultFilename") path, err := b.display.SaveFileDialog(display.SaveFileOptions{ Title: title, DefaultDirectory: defaultDir, DefaultFilename: defaultFilename, }) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"path": path} case "dialog_open_directory": title := getStringParam(params, "title") defaultDir := getStringParam(params, "defaultDirectory") path, err := b.display.OpenDirectoryDialog(display.OpenDirectoryOptions{ Title: title, DefaultDirectory: defaultDir, }) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"path": path} case "dialog_confirm": title := getStringParam(params, "title") message := getStringParam(params, "message") confirmed, err := b.display.ConfirmDialog(title, message) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"confirmed": confirmed} case "dialog_prompt": title := getStringParam(params, "title") message := getStringParam(params, "message") result, ok, err := b.display.PromptDialog(title, message) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"result": result, "ok": ok} case "event_info": eventMgr := b.display.GetEventManager() if eventMgr == nil { return map[string]any{"error": "event manager not available"} } return map[string]any{ "endpoint": fmt.Sprintf("ws://localhost:%d/events", b.port), "connectedClients": eventMgr.ConnectedClients(), "eventTypes": []string{ "window.focus", "window.blur", "window.move", "window.resize", "window.close", "window.create", "theme.change", "screen.change", }, } default: return map[string]any{"error": "unknown tool", "tool": tool} } } // executeWebviewTool handles webview/JS tool execution. func (b *MCPBridge) executeWebviewTool(tool string, params map[string]any) map[string]any { if b.webview == nil { return map[string]any{"error": "webview service not available"} } switch tool { case "webview_list": windows := b.webview.ListWindows() return map[string]any{"windows": windows} case "webview_eval": windowName := getStringParam(params, "window") code := getStringParam(params, "code") result, err := b.webview.ExecJS(windowName, code) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"result": result} case "webview_console": level := getStringParam(params, "level") limit := getIntParam(params, "limit") if limit == 0 { limit = 100 } messages := b.webview.GetConsoleMessages(level, limit) return map[string]any{"messages": messages} case "webview_console_clear": b.webview.ClearConsole() return map[string]any{"success": true} case "webview_click": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") err := b.webview.Click(windowName, selector) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_type": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") text := getStringParam(params, "text") err := b.webview.Type(windowName, selector, text) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_query": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") result, err := b.webview.QuerySelector(windowName, selector) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"elements": result} case "webview_navigate": windowName := getStringParam(params, "window") url := getStringParam(params, "url") err := b.webview.Navigate(windowName, url) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_source": windowName := getStringParam(params, "window") result, err := b.webview.GetPageSource(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"source": result} case "webview_url": windowName := getStringParam(params, "window") result, err := b.webview.GetURL(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"url": result} case "webview_title": windowName := getStringParam(params, "window") result, err := b.webview.GetTitle(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"title": result} case "webview_screenshot": windowName := getStringParam(params, "window") data, err := b.webview.Screenshot(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"data": data} case "webview_screenshot_element": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") data, err := b.webview.ScreenshotElement(windowName, selector) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"data": data} case "webview_scroll": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") x := getIntParam(params, "x") y := getIntParam(params, "y") err := b.webview.Scroll(windowName, selector, x, y) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_hover": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") err := b.webview.Hover(windowName, selector) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_select": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") value := getStringParam(params, "value") err := b.webview.Select(windowName, selector, value) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_check": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") checked, _ := params["checked"].(bool) err := b.webview.Check(windowName, selector, checked) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_element_info": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") result, err := b.webview.GetElementInfo(windowName, selector) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"element": result} case "webview_computed_style": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") var properties []string if props, ok := params["properties"].([]any); ok { for _, p := range props { if s, ok := p.(string); ok { properties = append(properties, s) } } } result, err := b.webview.GetComputedStyle(windowName, selector, properties) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"styles": result} case "webview_highlight": windowName := getStringParam(params, "window") selector := getStringParam(params, "selector") duration := getIntParam(params, "duration") err := b.webview.Highlight(windowName, selector, duration) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_dom_tree": windowName := getStringParam(params, "window") maxDepth := getIntParam(params, "maxDepth") result, err := b.webview.GetDOMTree(windowName, maxDepth) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"tree": result} case "webview_errors": limit := getIntParam(params, "limit") if limit == 0 { limit = 50 } errors := b.webview.GetErrors(limit) return map[string]any{"errors": errors} case "webview_performance": windowName := getStringParam(params, "window") result, err := b.webview.GetPerformance(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"performance": result} case "webview_resources": windowName := getStringParam(params, "window") result, err := b.webview.GetResources(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"resources": result} case "webview_network": windowName := getStringParam(params, "window") limit := getIntParam(params, "limit") result, err := b.webview.GetNetworkRequests(windowName, limit) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"requests": result} case "webview_network_clear": windowName := getStringParam(params, "window") err := b.webview.ClearNetworkRequests(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_network_inject": windowName := getStringParam(params, "window") err := b.webview.InjectNetworkInterceptor(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} case "webview_pdf": windowName := getStringParam(params, "window") options := make(map[string]any) if filename := getStringParam(params, "filename"); filename != "" { options["filename"] = filename } if margin, ok := params["margin"].(float64); ok { options["margin"] = margin } data, err := b.webview.ExportToPDF(windowName, options) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"data": data} case "webview_print": windowName := getStringParam(params, "window") err := b.webview.PrintToPDF(windowName) if err != nil { return map[string]any{"error": err.Error()} } return map[string]any{"success": true} default: return map[string]any{"error": "unknown webview tool", "tool": tool} } } // Helper functions for parameter extraction func getStringParam(params map[string]any, key string) string { if v, ok := params[key].(string); ok { return v } return "" } func getIntParam(params map[string]any, key string) int { if v, ok := params[key].(float64); ok { return int(v) } return 0 } // GetMCPService returns the MCP service for direct access. func (b *MCPBridge) GetMCPService() *mcp.Service { return b.mcpService } // GetWebView returns the WebView service. func (b *MCPBridge) GetWebView() *webview.Service { return b.webview } // GetDisplay returns the Display service. func (b *MCPBridge) GetDisplay() *display.Service { return b.display } // handleEventsWebSocket handles WebSocket connections for real-time display events. func (b *MCPBridge) handleEventsWebSocket(w http.ResponseWriter, r *http.Request) { eventMgr := b.display.GetEventManager() if eventMgr == nil { http.Error(w, "event manager not available", http.StatusServiceUnavailable) return } eventMgr.HandleWebSocket(w, r) }