diff --git a/scripts/verify-go-mcp.ps1 b/scripts/verify-go-mcp.ps1 new file mode 100644 index 0000000..80199e1 --- /dev/null +++ b/scripts/verify-go-mcp.ps1 @@ -0,0 +1,219 @@ +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$repo = (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path +$expectedTools = @( + "win_helper_version", + "win_helper_self_check", + "win_helper_scan_windows", + "win_helper_dump_uia" +) + +$tempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("isphere-go-mcp-verify-" + [guid]::NewGuid().ToString("N")) +$goMcpExe = Join-Path $tempRoot "isphere-mcp.exe" +$harnessDir = Join-Path $repo ("runs\go-mcp-harness-" + [guid]::NewGuid().ToString("N")) +$harnessMain = Join-Path $harnessDir "main.go" + +function Assert-True([bool]$Condition, [string]$Message) { + if (-not $Condition) { + throw $Message + } +} + +function Invoke-Step([string]$Name, [scriptblock]$Body) { + Write-Host "## $Name" + & $Body +} + +try { + New-Item -ItemType Directory -Force -Path $tempRoot | Out-Null + New-Item -ItemType Directory -Force -Path $harnessDir | Out-Null + + Push-Location $repo + try { + Invoke-Step "build C# helper" { + & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $repo "scripts\build-win-helper.ps1") | Out-Host + if ($LASTEXITCODE -ne 0) { + throw "build-win-helper.ps1 failed with exit code $LASTEXITCODE" + } + } + + Invoke-Step "build Go MCP binary" { + & go build -o $goMcpExe ./cmd/isphere-mcp + if ($LASTEXITCODE -ne 0) { + throw "go build ./cmd/isphere-mcp failed with exit code $LASTEXITCODE" + } + Assert-True (Test-Path -LiteralPath $goMcpExe) "Go MCP binary was not created: $goMcpExe" + } + } + finally { + Pop-Location + } + + Invoke-Step "write temporary SDK harness" { + @" +package main + +import ( + "context" + "encoding/json" + "fmt" + "os" + "reflect" + "sort" + "strings" + "time" + + "github.com/modelcontextprotocol/go-sdk/mcp" + + "isphere-ai-bridge/internal/mcpserver" +) + +var expectedTools = []string{ + "win_helper_version", + "win_helper_self_check", + "win_helper_scan_windows", + "win_helper_dump_uia", +} + +var forbiddenTerms = []string{"send", "search", "file", "login", "conversation", "upload", "download", "receive", "autologin"} + +func main() { + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + serverTransport, clientTransport := mcp.NewInMemoryTransports() + server := mcpserver.NewServer() + serverErr := make(chan error, 1) + go func() { + serverErr <- server.Run(ctx, serverTransport) + }() + + client := mcp.NewClient(&mcp.Implementation{Name: "verify-go-mcp", Version: "0.1.0"}, nil) + session, err := client.Connect(ctx, clientTransport, nil) + if err != nil { + fail("initialize/connect", err) + } + defer func() { + _ = session.Close() + cancel() + select { + case <-serverErr: + case <-time.After(2 * time.Second): + fail("server shutdown", fmt.Errorf("server did not stop")) + } + }() + + listResult, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + if err != nil { + fail("tools/list", err) + } + + toolNames := make([]string, 0, len(listResult.Tools)) + for _, tool := range listResult.Tools { + toolNames = append(toolNames, tool.Name) + lower := strings.ToLower(tool.Name) + for _, term := range forbiddenTerms { + if strings.Contains(lower, term) { + fail("tools/list", fmt.Errorf("forbidden action term %q found in tool %q", term, tool.Name)) + } + } + } + sort.Strings(toolNames) + want := append([]string(nil), expectedTools...) + sort.Strings(want) + if len(toolNames) != 4 || !reflect.DeepEqual(toolNames, want) { + fail("tools/list", fmt.Errorf("tool names = %#v, want %#v", toolNames, want)) + } + + callResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "win_helper_version", Arguments: map[string]any{}}) + if err != nil { + fail("tools/call win_helper_version", err) + } + if callResult.IsError { + fail("tools/call win_helper_version", fmt.Errorf("tool returned isError=true: %#v", callResult.Content)) + } + + structured, ok := callResult.StructuredContent.(map[string]any) + if !ok { + payload, _ := json.Marshal(callResult.StructuredContent) + if err := json.Unmarshal(payload, &structured); err != nil { + fail("tools/call win_helper_version", fmt.Errorf("structuredContent was not object: %T", callResult.StructuredContent)) + } + } + if structured["ok"] != true { + fail("tools/call win_helper_version", fmt.Errorf("ok = %#v, want true", structured["ok"])) + } + data, ok := structured["data"].(map[string]any) + if !ok { + fail("tools/call win_helper_version", fmt.Errorf("data missing or not object: %#v", structured["data"])) + } + helperName, _ := data["helper_name"].(string) + if helperName != "ISphereWinHelper" { + fail("tools/call win_helper_version", fmt.Errorf("helper_name = %q, want ISphereWinHelper", helperName)) + } + + result := map[string]any{ + "ok": true, + "transport": "sdk_in_memory", + "initialized": true, + "tool_count": len(toolNames), + "tools": toolNames, + "helper_name": helperName, + "real_isphere_login_required": false, + "python_runtime_required": false, + "action_tools_present": false, + } + encoded, _ := json.Marshal(result) + fmt.Println(string(encoded)) +} + +func fail(step string, err error) { + payload, _ := json.Marshal(map[string]any{"ok": false, "step": step, "error": err.Error()}) + fmt.Fprintln(os.Stderr, string(payload)) + os.Exit(1) +} +"@ | Set-Content -LiteralPath $harnessMain -Encoding UTF8 + } + + $harnessJson = $null + Invoke-Step "run MCP SDK harness initialize/tools/list/tools/call" { + Push-Location $harnessDir + try { + $output = & go run . + if ($LASTEXITCODE -ne 0) { + throw "go run SDK harness failed with exit code $LASTEXITCODE" + } + $output | ForEach-Object { Write-Host $_ } + $lastLine = @($output | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) | Select-Object -Last 1 + $script:harnessJson = $lastLine | ConvertFrom-Json + } + finally { + Pop-Location + } + } + + Assert-True ($script:harnessJson.ok -eq $true) "SDK harness did not return ok=true" + Assert-True ($script:harnessJson.helper_name -eq "ISphereWinHelper") "SDK harness helper_name mismatch: $($script:harnessJson.helper_name)" + Assert-True ($script:harnessJson.tool_count -eq 4) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)" + + [ordered]@{ + ok = $true + verification = "go_mcp_sdk_harness" + go_mcp_binary_built = (Test-Path -LiteralPath $goMcpExe) + helper_name = $script:harnessJson.helper_name + tool_count = $script:harnessJson.tool_count + tools = $script:harnessJson.tools + python_runtime_required = $false + real_isphere_login_required = $false + action_tools_present = $false + } | ConvertTo-Json -Depth 8 -Compress +} +finally { + if (Test-Path -LiteralPath $tempRoot) { + Remove-Item -LiteralPath $tempRoot -Recurse -Force + } + if (Test-Path -LiteralPath $harnessDir) { + Remove-Item -LiteralPath $harnessDir -Recurse -Force + } +}