216 lines
5.3 KiB
Go
216 lines
5.3 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
core "dappco.re/go/core"
|
|
coreerr "dappco.re/go/core/log"
|
|
"forge.lthn.ai/core/gui/pkg/menu"
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
type MenuGetInput struct{}
|
|
|
|
type MenuOutput struct {
|
|
Items []map[string]any `json:"items"`
|
|
}
|
|
|
|
type MenuSetInput struct {
|
|
Items []map[string]any `json:"items"`
|
|
}
|
|
|
|
func (s *Subsystem) menuGet(_ context.Context, _ *mcp.CallToolRequest, _ MenuGetInput) (*mcp.CallToolResult, MenuOutput, error) {
|
|
items, err := s.queryMenuItems()
|
|
if err != nil {
|
|
return nil, MenuOutput{}, err
|
|
}
|
|
return nil, MenuOutput{Items: items}, nil
|
|
}
|
|
|
|
func (s *Subsystem) menuSet(_ context.Context, _ *mcp.CallToolRequest, input MenuSetInput) (*mcp.CallToolResult, MenuOutput, error) {
|
|
items, err := decodeMenuItems(input.Items)
|
|
if err != nil {
|
|
return nil, MenuOutput{}, err
|
|
}
|
|
r := s.core.Action("menu.setAppMenu").Run(context.Background(), core.NewOptions(
|
|
core.Option{Key: "task", Value: menu.TaskSetAppMenu{Items: items}},
|
|
))
|
|
if !r.OK {
|
|
if e, ok := r.Value.(error); ok {
|
|
return nil, MenuOutput{}, e
|
|
}
|
|
return nil, MenuOutput{}, nil
|
|
}
|
|
snapshot, err := s.queryMenuItems()
|
|
if err != nil {
|
|
return nil, MenuOutput{}, err
|
|
}
|
|
return nil, MenuOutput{Items: snapshot}, nil
|
|
}
|
|
|
|
func (s *Subsystem) queryMenuItems() ([]map[string]any, error) {
|
|
r := s.core.QUERY(menu.QueryGetAppMenu{})
|
|
if !r.OK {
|
|
if e, ok := r.Value.(error); ok {
|
|
return nil, e
|
|
}
|
|
return nil, nil
|
|
}
|
|
items, ok := r.Value.([]menu.MenuItem)
|
|
if !ok {
|
|
return nil, coreerr.E("mcp.menuGet", "unexpected result type", nil)
|
|
}
|
|
return encodeMenuItems(items), nil
|
|
}
|
|
|
|
func encodeMenuItems(items []menu.MenuItem) []map[string]any {
|
|
if len(items) == 0 {
|
|
return nil
|
|
}
|
|
out := make([]map[string]any, 0, len(items))
|
|
for _, item := range items {
|
|
spec := map[string]any{}
|
|
if item.Label != "" {
|
|
spec["label"] = item.Label
|
|
}
|
|
if item.Accelerator != "" {
|
|
spec["accelerator"] = item.Accelerator
|
|
}
|
|
if item.Type != "" {
|
|
spec["type"] = item.Type
|
|
}
|
|
if item.Checked {
|
|
spec["checked"] = item.Checked
|
|
}
|
|
if item.Disabled {
|
|
spec["disabled"] = item.Disabled
|
|
}
|
|
if item.Tooltip != "" {
|
|
spec["tooltip"] = item.Tooltip
|
|
}
|
|
if children := encodeMenuItems(item.Children); len(children) > 0 {
|
|
rawChildren := make([]any, len(children))
|
|
for i, child := range children {
|
|
rawChildren[i] = child
|
|
}
|
|
spec["children"] = rawChildren
|
|
}
|
|
if item.Role != nil {
|
|
spec["role"] = encodeMenuRole(*item.Role)
|
|
}
|
|
out = append(out, spec)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func decodeMenuItems(items []map[string]any) ([]menu.MenuItem, error) {
|
|
if len(items) == 0 {
|
|
return nil, nil
|
|
}
|
|
out := make([]menu.MenuItem, 0, len(items))
|
|
for _, item := range items {
|
|
roleName, _ := item["role"].(string)
|
|
role, err := decodeMenuRole(roleName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
children, err := decodeMenuChildren(item["children"])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, menu.MenuItem{
|
|
Label: stringValue(item, "label"),
|
|
Accelerator: stringValue(item, "accelerator"),
|
|
Type: stringValue(item, "type"),
|
|
Checked: boolValue(item, "checked"),
|
|
Disabled: boolValue(item, "disabled"),
|
|
Tooltip: stringValue(item, "tooltip"),
|
|
Children: children,
|
|
Role: role,
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func decodeMenuChildren(value any) ([]menu.MenuItem, error) {
|
|
switch children := value.(type) {
|
|
case nil:
|
|
return nil, nil
|
|
case []any:
|
|
items := make([]map[string]any, 0, len(children))
|
|
for _, child := range children {
|
|
childMap, ok := child.(map[string]any)
|
|
if !ok {
|
|
return nil, coreerr.E("mcp.decodeMenuChildren", "child menu item must be an object", nil)
|
|
}
|
|
items = append(items, childMap)
|
|
}
|
|
return decodeMenuItems(items)
|
|
case []map[string]any:
|
|
return decodeMenuItems(children)
|
|
default:
|
|
return nil, coreerr.E("mcp.decodeMenuChildren", "children must be an array", nil)
|
|
}
|
|
}
|
|
|
|
func stringValue(item map[string]any, key string) string {
|
|
value, _ := item[key].(string)
|
|
return value
|
|
}
|
|
|
|
func boolValue(item map[string]any, key string) bool {
|
|
value, _ := item[key].(bool)
|
|
return value
|
|
}
|
|
|
|
func encodeMenuRole(role menu.MenuRole) string {
|
|
switch role {
|
|
case menu.RoleAppMenu:
|
|
return "app"
|
|
case menu.RoleFileMenu:
|
|
return "file"
|
|
case menu.RoleEditMenu:
|
|
return "edit"
|
|
case menu.RoleViewMenu:
|
|
return "view"
|
|
case menu.RoleWindowMenu:
|
|
return "window"
|
|
case menu.RoleHelpMenu:
|
|
return "help"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func decodeMenuRole(role string) (*menu.MenuRole, error) {
|
|
switch strings.TrimSpace(strings.ToLower(role)) {
|
|
case "":
|
|
return nil, nil
|
|
case "app":
|
|
value := menu.RoleAppMenu
|
|
return &value, nil
|
|
case "file":
|
|
value := menu.RoleFileMenu
|
|
return &value, nil
|
|
case "edit":
|
|
value := menu.RoleEditMenu
|
|
return &value, nil
|
|
case "view":
|
|
value := menu.RoleViewMenu
|
|
return &value, nil
|
|
case "window":
|
|
value := menu.RoleWindowMenu
|
|
return &value, nil
|
|
case "help":
|
|
value := menu.RoleHelpMenu
|
|
return &value, nil
|
|
default:
|
|
return nil, coreerr.E("mcp.decodeMenuRole", "unknown menu role: "+role, nil)
|
|
}
|
|
}
|
|
|
|
func (s *Subsystem) registerMenuTools(server *mcp.Server) {
|
|
addTool(s, server, &mcp.Tool{Name: "menu_get", Description: "Get the current application menu structure"}, s.menuGet)
|
|
addTool(s, server, &mcp.Tool{Name: "menu_set", Description: "Set the application menu structure"}, s.menuSet)
|
|
}
|