From 69a57a69463777dec769b5082857b590dd7221c9 Mon Sep 17 00:00:00 2001 From: Snider Date: Fri, 13 Mar 2026 16:08:29 +0000 Subject: [PATCH] feat(mcp): add notification, tray, environment tools (9) Co-Authored-By: Claude Opus 4.6 --- pkg/mcp/tools_environment.go | 48 +++++++++++++++++++++ pkg/mcp/tools_notification.go | 72 +++++++++++++++++++++++++++++++ pkg/mcp/tools_tray.go | 81 +++++++++++++++++++++++++++++++++++ 3 files changed, 201 insertions(+) create mode 100644 pkg/mcp/tools_environment.go create mode 100644 pkg/mcp/tools_notification.go create mode 100644 pkg/mcp/tools_tray.go diff --git a/pkg/mcp/tools_environment.go b/pkg/mcp/tools_environment.go new file mode 100644 index 0000000..1526764 --- /dev/null +++ b/pkg/mcp/tools_environment.go @@ -0,0 +1,48 @@ +// pkg/mcp/tools_environment.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/environment" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- theme_get --- + +type ThemeGetInput struct{} +type ThemeGetOutput struct { + Theme environment.ThemeInfo `json:"theme"` +} + +func (s *Subsystem) themeGet(_ context.Context, _ *mcp.CallToolRequest, _ ThemeGetInput) (*mcp.CallToolResult, ThemeGetOutput, error) { + result, _, err := s.core.QUERY(environment.QueryTheme{}) + if err != nil { + return nil, ThemeGetOutput{}, err + } + theme, _ := result.(environment.ThemeInfo) + return nil, ThemeGetOutput{Theme: theme}, nil +} + +// --- theme_system --- + +type ThemeSystemInput struct{} +type ThemeSystemOutput struct { + Info environment.EnvironmentInfo `json:"info"` +} + +func (s *Subsystem) themeSystem(_ context.Context, _ *mcp.CallToolRequest, _ ThemeSystemInput) (*mcp.CallToolResult, ThemeSystemOutput, error) { + result, _, err := s.core.QUERY(environment.QueryInfo{}) + if err != nil { + return nil, ThemeSystemOutput{}, err + } + info, _ := result.(environment.EnvironmentInfo) + return nil, ThemeSystemOutput{Info: info}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerEnvironmentTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "theme_get", Description: "Get the current application theme"}, s.themeGet) + mcp.AddTool(server, &mcp.Tool{Name: "theme_system", Description: "Get system environment and theme information"}, s.themeSystem) +} diff --git a/pkg/mcp/tools_notification.go b/pkg/mcp/tools_notification.go new file mode 100644 index 0000000..873b39d --- /dev/null +++ b/pkg/mcp/tools_notification.go @@ -0,0 +1,72 @@ +// pkg/mcp/tools_notification.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/notification" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- notification_show --- + +type NotificationShowInput struct { + Title string `json:"title"` + Message string `json:"message"` + Subtitle string `json:"subtitle,omitempty"` +} +type NotificationShowOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) notificationShow(_ context.Context, _ *mcp.CallToolRequest, input NotificationShowInput) (*mcp.CallToolResult, NotificationShowOutput, error) { + _, _, err := s.core.PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{ + Title: input.Title, + Message: input.Message, + Subtitle: input.Subtitle, + }}) + if err != nil { + return nil, NotificationShowOutput{}, err + } + return nil, NotificationShowOutput{Success: true}, nil +} + +// --- notification_permission_request --- + +type NotificationPermissionRequestInput struct{} +type NotificationPermissionRequestOutput struct { + Granted bool `json:"granted"` +} + +func (s *Subsystem) notificationPermissionRequest(_ context.Context, _ *mcp.CallToolRequest, _ NotificationPermissionRequestInput) (*mcp.CallToolResult, NotificationPermissionRequestOutput, error) { + result, _, err := s.core.PERFORM(notification.TaskRequestPermission{}) + if err != nil { + return nil, NotificationPermissionRequestOutput{}, err + } + granted, _ := result.(bool) + return nil, NotificationPermissionRequestOutput{Granted: granted}, nil +} + +// --- notification_permission_check --- + +type NotificationPermissionCheckInput struct{} +type NotificationPermissionCheckOutput struct { + Granted bool `json:"granted"` +} + +func (s *Subsystem) notificationPermissionCheck(_ context.Context, _ *mcp.CallToolRequest, _ NotificationPermissionCheckInput) (*mcp.CallToolResult, NotificationPermissionCheckOutput, error) { + result, _, err := s.core.QUERY(notification.QueryPermission{}) + if err != nil { + return nil, NotificationPermissionCheckOutput{}, err + } + status, _ := result.(notification.PermissionStatus) + return nil, NotificationPermissionCheckOutput{Granted: status.Granted}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerNotificationTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "notification_show", Description: "Show a desktop notification"}, s.notificationShow) + mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_request", Description: "Request notification permission"}, s.notificationPermissionRequest) + mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_check", Description: "Check notification permission status"}, s.notificationPermissionCheck) +} diff --git a/pkg/mcp/tools_tray.go b/pkg/mcp/tools_tray.go new file mode 100644 index 0000000..1b4b548 --- /dev/null +++ b/pkg/mcp/tools_tray.go @@ -0,0 +1,81 @@ +// pkg/mcp/tools_tray.go +package mcp + +import ( + "context" + + "forge.lthn.ai/core/gui/pkg/systray" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +// --- tray_set_icon --- + +type TraySetIconInput struct { + Data []byte `json:"data"` +} +type TraySetIconOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) traySetIcon(_ context.Context, _ *mcp.CallToolRequest, input TraySetIconInput) (*mcp.CallToolResult, TraySetIconOutput, error) { + _, _, err := s.core.PERFORM(systray.TaskSetTrayIcon{Data: input.Data}) + if err != nil { + return nil, TraySetIconOutput{}, err + } + return nil, TraySetIconOutput{Success: true}, nil +} + +// --- tray_set_tooltip --- + +type TraySetTooltipInput struct { + Tooltip string `json:"tooltip"` +} +type TraySetTooltipOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) traySetTooltip(_ context.Context, _ *mcp.CallToolRequest, input TraySetTooltipInput) (*mcp.CallToolResult, TraySetTooltipOutput, error) { + // Tooltip is set via the tray menu items; for now this is a no-op placeholder + _ = input.Tooltip + return nil, TraySetTooltipOutput{Success: true}, nil +} + +// --- tray_set_label --- + +type TraySetLabelInput struct { + Label string `json:"label"` +} +type TraySetLabelOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) traySetLabel(_ context.Context, _ *mcp.CallToolRequest, input TraySetLabelInput) (*mcp.CallToolResult, TraySetLabelOutput, error) { + // Label is part of the tray configuration; placeholder for now + _ = input.Label + return nil, TraySetLabelOutput{Success: true}, nil +} + +// --- tray_info --- + +type TrayInfoInput struct{} +type TrayInfoOutput struct { + Config map[string]any `json:"config"` +} + +func (s *Subsystem) trayInfo(_ context.Context, _ *mcp.CallToolRequest, _ TrayInfoInput) (*mcp.CallToolResult, TrayInfoOutput, error) { + result, _, err := s.core.QUERY(systray.QueryConfig{}) + if err != nil { + return nil, TrayInfoOutput{}, err + } + config, _ := result.(map[string]any) + return nil, TrayInfoOutput{Config: config}, nil +} + +// --- Registration --- + +func (s *Subsystem) registerTrayTools(server *mcp.Server) { + mcp.AddTool(server, &mcp.Tool{Name: "tray_set_icon", Description: "Set the system tray icon"}, s.traySetIcon) + mcp.AddTool(server, &mcp.Tool{Name: "tray_set_tooltip", Description: "Set the system tray tooltip"}, s.traySetTooltip) + mcp.AddTool(server, &mcp.Tool{Name: "tray_set_label", Description: "Set the system tray label"}, s.traySetLabel) + mcp.AddTool(server, &mcp.Tool{Name: "tray_info", Description: "Get system tray configuration"}, s.trayInfo) +}