package tools import ( "context" "encoding/json" "github.com/modelcontextprotocol/go-sdk/mcp" "isphere-ai-bridge/internal/helperclient" ) const ( ToolNameVersion = "win_helper_version" ToolNameSelfCheck = "win_helper_self_check" ToolNameScanWindows = "win_helper_scan_windows" ToolNameDumpUIA = "win_helper_dump_uia" ) type HelperCaller interface { Call(ctx context.Context, op string, args map[string]any) (*helperclient.HelperResponse, error) } type VersionArgs struct{} type SelfCheckArgs struct{} type ScanWindowsArgs struct { IncludeAllVisible bool `json:"include_all_visible,omitempty" jsonschema:"include all visible desktop windows, not only likely iSphere candidates"` } type DumpUIAArgs struct { Hwnd string `json:"hwnd,omitempty" jsonschema:"window handle to dump, for example 0x001A0B2C"` MaxDepth int `json:"max_depth,omitempty" jsonschema:"maximum UI Automation tree depth"` IncludeText bool `json:"include_text,omitempty" jsonschema:"include text values when available"` MaxChildren int `json:"max_children,omitempty" jsonschema:"maximum children per node"` } func RegisterWinHelperTools(server *mcp.Server, caller HelperCaller) { if caller == nil { caller = helperclient.Client{} } mcp.AddTool[VersionArgs, map[string]any](server, &mcp.Tool{ Name: ToolNameVersion, Description: "Return ISphereWinHelper version and protocol metadata.", }, func(ctx context.Context, _ *mcp.CallToolRequest, _ VersionArgs) (*mcp.CallToolResult, map[string]any, error) { return callHelper(ctx, caller, "version", map[string]any{}) }) mcp.AddTool[SelfCheckArgs, map[string]any](server, &mcp.Tool{ Name: ToolNameSelfCheck, Description: "Run ISphereWinHelper read-only desktop and UI Automation availability checks.", }, func(ctx context.Context, _ *mcp.CallToolRequest, _ SelfCheckArgs) (*mcp.CallToolResult, map[string]any, error) { return callHelper(ctx, caller, "self_check", map[string]any{}) }) mcp.AddTool[ScanWindowsArgs, map[string]any](server, &mcp.Tool{ Name: ToolNameScanWindows, Description: "List visible desktop windows or likely iSphere window candidates in read-only mode.", }, func(ctx context.Context, _ *mcp.CallToolRequest, input ScanWindowsArgs) (*mcp.CallToolResult, map[string]any, error) { return callHelper(ctx, caller, "scan_windows", map[string]any{ "include_all_visible": input.IncludeAllVisible, }) }) mcp.AddTool[DumpUIAArgs, map[string]any](server, &mcp.Tool{ Name: ToolNameDumpUIA, Description: "Dump a read-only UI Automation tree for a specified window handle.", }, func(ctx context.Context, _ *mcp.CallToolRequest, input DumpUIAArgs) (*mcp.CallToolResult, map[string]any, error) { return callHelper(ctx, caller, "dump_uia", map[string]any{ "hwnd": input.Hwnd, "max_depth": float64(input.MaxDepth), "include_text": input.IncludeText, "max_children": float64(input.MaxChildren), }) }) } func callHelper(ctx context.Context, caller HelperCaller, op string, args map[string]any) (*mcp.CallToolResult, map[string]any, error) { response, err := caller.Call(ctx, op, args) if err != nil { return nil, nil, err } return nil, helperResponseToMap(response), nil } func helperResponseToMap(response *helperclient.HelperResponse) map[string]any { if response == nil { return map[string]any{"ok": false} } out := map[string]any{ "protocol": response.Protocol, "request_id": response.RequestID, "op": response.Op, "ok": response.OK, "data": decodeRawData(response.Data), "warnings": response.Warnings, } if response.Error != nil { out["error"] = map[string]any{ "code": response.Error.Code, "message": response.Error.Message, } } else { out["error"] = nil } return out } func decodeRawData(data map[string]json.RawMessage) map[string]any { decoded := map[string]any{} for key, raw := range data { var value any if err := json.Unmarshal(raw, &value); err != nil { decoded[key] = string(raw) continue } decoded[key] = value } return decoded }