2026-01-30 08:44:23 +00:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/base64"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"net/http"
|
|
|
|
|
"sync"
|
|
|
|
|
|
2026-02-16 06:44:02 +00:00
|
|
|
"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"
|
2026-01-30 08:44:23 +00:00
|
|
|
"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)
|
|
|
|
|
}
|