From a89e66c832b9c69172040f733bb8c68db2e700bd Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 13 Mar 2026 16:08:34 +0000 Subject: [PATCH] feat(mcp): add browser, contextmenu, keybinding, dock, lifecycle tools (11) Co-Authored-By: Claude Opus 4.6 --- pkg/mcp/tools_browser.go | 32 +++++++++++++ pkg/mcp/tools_contextmenu.go | 87 ++++++++++++++++++++++++++++++++++++ pkg/mcp/tools_dock.go | 64 ++++++++++++++++++++++++++ pkg/mcp/tools_keybinding.go | 51 +++++++++++++++++++++ pkg/mcp/tools_lifecycle.go | 31 +++++++++++++ 5 files changed, 265 insertions(+) create mode 100644 pkg/mcp/tools_browser.go create mode 100644 pkg/mcp/tools_contextmenu.go create mode 100644 pkg/mcp/tools_dock.go create mode 100644 pkg/mcp/tools_keybinding.go create mode 100644 pkg/mcp/tools_lifecycle.go diff --git a/pkg/mcp/tools_browser.go b/pkg/mcp/tools_browser.go new file mode 100644 index 0000000..18f670c --- /dev/null +++ b/pkg/mcp/tools_browser.go @@ -0,0 +1,32 @@ +// pkg/mcp/tools_browser.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/browser" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- browser_open_url --- + +type BrowserOpenURLInput struct { + URL string `json:"url"` +} +type BrowserOpenURLOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) browserOpenURL(_ context.Context, _ *mcp.CallToolRequest, input BrowserOpenURLInput) (*mcp.CallToolResult, BrowserOpenURLOutput, error) { + _, _, err := s.core.PERFORM(browser.TaskOpenURL{URL: input.URL}) + if err != nil { + return nil, BrowserOpenURLOutput{}, err + } + return nil, BrowserOpenURLOutput{Success: true}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerBrowserTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "browser_open_url", Description: "Open a URL in the default system browser"}, s.browserOpenURL) +} diff --git a/pkg/mcp/tools_contextmenu.go b/pkg/mcp/tools_contextmenu.go new file mode 100644 index 0000000..6c84aae --- /dev/null +++ b/pkg/mcp/tools_contextmenu.go @@ -0,0 +1,87 @@ +// pkg/mcp/tools_contextmenu.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/contextmenu" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- contextmenu_add --- + +type ContextMenuAddInput struct { + Name string `json:"name"` + Menu contextmenu.ContextMenuDef `json:"menu"` +} +type ContextMenuAddOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) contextMenuAdd(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuAddInput) (*mcp.CallToolResult, ContextMenuAddOutput, error) { + _, _, err := s.core.PERFORM(contextmenu.TaskAdd{Name: input.Name, Menu: input.Menu}) + if err != nil { + return nil, ContextMenuAddOutput{}, err + } + return nil, ContextMenuAddOutput{Success: true}, nil +} + +// --- contextmenu_remove --- + +type ContextMenuRemoveInput struct { + Name string `json:"name"` +} +type ContextMenuRemoveOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) contextMenuRemove(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuRemoveInput) (*mcp.CallToolResult, ContextMenuRemoveOutput, error) { + _, _, err := s.core.PERFORM(contextmenu.TaskRemove{Name: input.Name}) + if err != nil { + return nil, ContextMenuRemoveOutput{}, err + } + return nil, ContextMenuRemoveOutput{Success: true}, nil +} + +// --- contextmenu_get --- + +type ContextMenuGetInput struct { + Name string `json:"name"` +} +type ContextMenuGetOutput struct { + Menu *contextmenu.ContextMenuDef `json:"menu"` +} + +func (s *Subsystem) contextMenuGet(_ context.Context, _ *mcp.CallToolRequest, input ContextMenuGetInput) (*mcp.CallToolResult, ContextMenuGetOutput, error) { + result, _, err := s.core.QUERY(contextmenu.QueryGet{Name: input.Name}) + if err != nil { + return nil, ContextMenuGetOutput{}, err + } + menu, _ := result.(*contextmenu.ContextMenuDef) + return nil, ContextMenuGetOutput{Menu: menu}, nil +} + +// --- contextmenu_list --- + +type ContextMenuListInput struct{} +type ContextMenuListOutput struct { + Menus map[string]contextmenu.ContextMenuDef `json:"menus"` +} + +func (s *Subsystem) contextMenuList(_ context.Context, _ *mcp.CallToolRequest, _ ContextMenuListInput) (*mcp.CallToolResult, ContextMenuListOutput, error) { + result, _, err := s.core.QUERY(contextmenu.QueryList{}) + if err != nil { + return nil, ContextMenuListOutput{}, err + } + menus, _ := result.(map[string]contextmenu.ContextMenuDef) + return nil, ContextMenuListOutput{Menus: menus}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerContextMenuTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "contextmenu_add", Description: "Register a context menu"}, s.contextMenuAdd) + mcp.AddTool(server, &mcp.Tool{Name: "contextmenu_remove", Description: "Unregister a context menu"}, s.contextMenuRemove) + mcp.AddTool(server, &mcp.Tool{Name: "contextmenu_get", Description: "Get a context menu by name"}, s.contextMenuGet) + mcp.AddTool(server, &mcp.Tool{Name: "contextmenu_list", Description: "List all registered context menus"}, s.contextMenuList) +} diff --git a/pkg/mcp/tools_dock.go b/pkg/mcp/tools_dock.go new file mode 100644 index 0000000..bff74fc --- /dev/null +++ b/pkg/mcp/tools_dock.go @@ -0,0 +1,64 @@ +// pkg/mcp/tools_dock.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/dock" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- dock_show --- + +type DockShowInput struct{} +type DockShowOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) dockShow(_ context.Context, _ *mcp.CallToolRequest, _ DockShowInput) (*mcp.CallToolResult, DockShowOutput, error) { + _, _, err := s.core.PERFORM(dock.TaskShowIcon{}) + if err != nil { + return nil, DockShowOutput{}, err + } + return nil, DockShowOutput{Success: true}, nil +} + +// --- dock_hide --- + +type DockHideInput struct{} +type DockHideOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) dockHide(_ context.Context, _ *mcp.CallToolRequest, _ DockHideInput) (*mcp.CallToolResult, DockHideOutput, error) { + _, _, err := s.core.PERFORM(dock.TaskHideIcon{}) + if err != nil { + return nil, DockHideOutput{}, err + } + return nil, DockHideOutput{Success: true}, nil +} + +// --- dock_badge --- + +type DockBadgeInput struct { + Label string `json:"label"` +} +type DockBadgeOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) dockBadge(_ context.Context, _ *mcp.CallToolRequest, input DockBadgeInput) (*mcp.CallToolResult, DockBadgeOutput, error) { + _, _, err := s.core.PERFORM(dock.TaskSetBadge{Label: input.Label}) + if err != nil { + return nil, DockBadgeOutput{}, err + } + return nil, DockBadgeOutput{Success: true}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerDockTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "dock_show", Description: "Show the dock/taskbar icon"}, s.dockShow) + mcp.AddTool(server, &mcp.Tool{Name: "dock_hide", Description: "Hide the dock/taskbar icon"}, s.dockHide) + mcp.AddTool(server, &mcp.Tool{Name: "dock_badge", Description: "Set the dock/taskbar badge label"}, s.dockBadge) +} diff --git a/pkg/mcp/tools_keybinding.go b/pkg/mcp/tools_keybinding.go new file mode 100644 index 0000000..f8a5bf6 --- /dev/null +++ b/pkg/mcp/tools_keybinding.go @@ -0,0 +1,51 @@ +// pkg/mcp/tools_keybinding.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/keybinding" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- keybinding_add --- + +type KeybindingAddInput struct { + Accelerator string `json:"accelerator"` + Description string `json:"description,omitempty"` +} +type KeybindingAddOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) keybindingAdd(_ context.Context, _ *mcp.CallToolRequest, input KeybindingAddInput) (*mcp.CallToolResult, KeybindingAddOutput, error) { + _, _, err := s.core.PERFORM(keybinding.TaskAdd{Accelerator: input.Accelerator, Description: input.Description}) + if err != nil { + return nil, KeybindingAddOutput{}, err + } + return nil, KeybindingAddOutput{Success: true}, nil +} + +// --- keybinding_remove --- + +type KeybindingRemoveInput struct { + Accelerator string `json:"accelerator"` +} +type KeybindingRemoveOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) keybindingRemove(_ context.Context, _ *mcp.CallToolRequest, input KeybindingRemoveInput) (*mcp.CallToolResult, KeybindingRemoveOutput, error) { + _, _, err := s.core.PERFORM(keybinding.TaskRemove{Accelerator: input.Accelerator}) + if err != nil { + return nil, KeybindingRemoveOutput{}, err + } + return nil, KeybindingRemoveOutput{Success: true}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerKeybindingTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "keybinding_add", Description: "Register a keyboard shortcut"}, s.keybindingAdd) + mcp.AddTool(server, &mcp.Tool{Name: "keybinding_remove", Description: "Unregister a keyboard shortcut"}, s.keybindingRemove) +} diff --git a/pkg/mcp/tools_lifecycle.go b/pkg/mcp/tools_lifecycle.go new file mode 100644 index 0000000..26715bd --- /dev/null +++ b/pkg/mcp/tools_lifecycle.go @@ -0,0 +1,31 @@ +// pkg/mcp/tools_lifecycle.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/lifecycle" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- app_quit --- + +type AppQuitInput struct{} +type AppQuitOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) appQuit(_ context.Context, _ *mcp.CallToolRequest, _ AppQuitInput) (*mcp.CallToolResult, AppQuitOutput, error) { + // Broadcast the will-terminate action which triggers application shutdown + err := s.core.ACTION(lifecycle.ActionWillTerminate{}) + if err != nil { + return nil, AppQuitOutput{}, err + } + return nil, AppQuitOutput{Success: true}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerLifecycleTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "app_quit", Description: "Quit the application"}, s.appQuit) +}