Files
isphere-ai-bridge/scripts/verify-go-mcp.ps1
2026-07-09 23:27:52 +08:00

383 lines
13 KiB
PowerShell

$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",
"isphere_receive_messages"
)
$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"
"crypto/cipher"
"crypto/des"
"encoding/base64"
"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",
"isphere_receive_messages",
}
var forbiddenTerms = []string{"send", "search", "file", "login", "conversation", "upload", "download", "autologin"}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
if err := os.Unsetenv("ISPHERE_PACKET_LOG_FILE"); err != nil {
fail("server/env", err)
}
serverTransport, clientTransport := mcp.NewInMemoryTransports()
server, err := mcpserver.NewServerFromEnv()
if err != nil {
fail("server/config", err)
}
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) != 5 || !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))
}
receiveResult, err := session.CallTool(ctx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{"limit": 5}})
if err != nil {
fail("tools/call isphere_receive_messages", err)
}
if receiveResult.IsError {
fail("tools/call isphere_receive_messages", fmt.Errorf("tool returned isError=true: %#v", receiveResult.Content))
}
var receivePayload map[string]any
receiveEncoded, _ := json.Marshal(receiveResult.StructuredContent)
if err := json.Unmarshal(receiveEncoded, &receivePayload); err != nil {
fail("tools/call isphere_receive_messages", fmt.Errorf("decode structuredContent %s: %w", receiveEncoded, err))
}
messagesValue, ok := receivePayload["messages"].([]any)
if !ok {
fail("tools/call isphere_receive_messages", fmt.Errorf("messages missing or not array: %s", receiveEncoded))
}
if len(messagesValue) != 0 {
fail("tools/call isphere_receive_messages", fmt.Errorf("default empty source returned %d messages, want 0", len(messagesValue)))
}
configuredReceiveCount := verifyConfiguredReceiveSource(ctx)
result := map[string]any{
"ok": true,
"transport": "sdk_in_memory",
"initialized": true,
"tool_count": len(toolNames),
"tools": toolNames,
"helper_name": helperName,
"receive_message_count": len(messagesValue),
"configured_receive_message_count": configuredReceiveCount,
"packet_log_file_configured": os.Getenv("ISPHERE_PACKET_LOG_FILE") != "",
"real_isphere_login_required": false,
"python_runtime_required": false,
"action_tools_present": false,
}
encoded, _ := json.Marshal(result)
fmt.Println(string(encoded))
}
func verifyConfiguredReceiveSource(ctx context.Context) int {
plaintext := "--------------------------------------------------------------------------------------------------------------------------------------------\n" +
"2026/7/7 15:30:07\n" +
"<message id=\"msg-fixture-1\" from=\"sender@imopenfire1-lanzhou/imp_pc_4.1.2.6842\" to=\"receiver@imopenfire1-lanzhou\" type=\"chat\">\n" +
" <body>redacted fixture</body>\n" +
" <received xmlns=\"urn:xmpp:receipts\" id=\"receipt-fixture-1\" type=\"1\" stamp=\"\" />\n" +
" <subject>VERIFY_FIXTURE</subject>\n" +
" <isphere xmlns=\"isphere.im\" type=\"1001\" sendtime=\"1783423807000\" version=\"1\" />\n" +
" <readed />\n" +
"</message>"
line, err := encryptPacketLogLineForHarness(plaintext)
if err != nil {
fail("fixture/encrypt", err)
}
file, err := os.CreateTemp("", "isphere-packet-log-*.txt")
if err != nil {
fail("fixture/create", err)
}
fixturePath := file.Name()
defer os.Remove(fixturePath)
if _, err := file.WriteString("\n" + line + "\n"); err != nil {
_ = file.Close()
fail("fixture/write", err)
}
if err := file.Close(); err != nil {
fail("fixture/close", err)
}
oldValue, hadOldValue := os.LookupEnv("ISPHERE_PACKET_LOG_FILE")
if err := os.Setenv("ISPHERE_PACKET_LOG_FILE", fixturePath); err != nil {
fail("fixture/env", err)
}
defer func() {
if hadOldValue {
_ = os.Setenv("ISPHERE_PACKET_LOG_FILE", oldValue)
} else {
_ = os.Unsetenv("ISPHERE_PACKET_LOG_FILE")
}
}()
serverCtx, cancel := context.WithCancel(ctx)
defer cancel()
serverTransport, clientTransport := mcp.NewInMemoryTransports()
server, err := mcpserver.NewServerFromEnv()
if err != nil {
fail("fixture/server_config", err)
}
serverErr := make(chan error, 1)
go func() {
serverErr <- server.Run(serverCtx, serverTransport)
}()
client := mcp.NewClient(&mcp.Implementation{Name: "verify-go-mcp-fixture", Version: "0.1.0"}, nil)
session, err := client.Connect(serverCtx, clientTransport, nil)
if err != nil {
cancel()
fail("fixture/connect", err)
}
defer func() {
_ = session.Close()
cancel()
select {
case <-serverErr:
case <-time.After(2 * time.Second):
fail("fixture/server_shutdown", fmt.Errorf("server did not stop"))
}
}()
callResult, err := session.CallTool(serverCtx, &mcp.CallToolParams{Name: "isphere_receive_messages", Arguments: map[string]any{"limit": 5}})
if err != nil {
fail("fixture/call isphere_receive_messages", err)
}
if callResult.IsError {
fail("fixture/call isphere_receive_messages", fmt.Errorf("tool returned isError=true: %#v", callResult.Content))
}
var receivePayload map[string]any
receiveEncoded, _ := json.Marshal(callResult.StructuredContent)
if err := json.Unmarshal(receiveEncoded, &receivePayload); err != nil {
fail("fixture/decode", fmt.Errorf("decode structuredContent %s: %w", receiveEncoded, err))
}
messagesValue, ok := receivePayload["messages"].([]any)
if !ok {
fail("fixture/decode", fmt.Errorf("messages missing or not array: %s", receiveEncoded))
}
if len(messagesValue) != 1 {
fail("fixture/assert", fmt.Errorf("configured source returned %d messages, want 1", len(messagesValue)))
}
message, ok := messagesValue[0].(map[string]any)
if !ok {
fail("fixture/assert", fmt.Errorf("message was not object: %#v", messagesValue[0]))
}
if message["id"] != "msg-fixture-1" || message["text"] != "redacted fixture" || message["subject"] != "VERIFY_FIXTURE" {
fail("fixture/assert", fmt.Errorf("unexpected message: %#v", message))
}
if message["conversation_id"] != "sender@imopenfire1-lanzhou|receiver@imopenfire1-lanzhou" {
fail("fixture/assert", fmt.Errorf("conversation_id = %#v", message["conversation_id"]))
}
return len(messagesValue)
}
func encryptPacketLogLineForHarness(plaintext string) (string, error) {
block, err := des.NewCipher([]byte("hyhccdtm"))
if err != nil {
return "", err
}
plain := padPKCS7ForHarness([]byte(plaintext), des.BlockSize)
ciphertext := make([]byte, len(plain))
iv := []byte{0x12, 0x34, 0x56, 0x78, 0x90, 0xAB, 0xCD, 0xEF}
cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, plain)
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
func padPKCS7ForHarness(data []byte, blockSize int) []byte {
pad := blockSize - len(data)%blockSize
out := append([]byte(nil), data...)
for i := 0; i < pad; i++ {
out = append(out, byte(pad))
}
return out
}
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 5) "SDK harness tool_count mismatch: $($script:harnessJson.tool_count)"
Assert-True ($script:harnessJson.receive_message_count -eq 0) "SDK harness receive_message_count mismatch: $($script:harnessJson.receive_message_count)"
Assert-True ($script:harnessJson.configured_receive_message_count -eq 1) "SDK harness configured_receive_message_count mismatch: $($script:harnessJson.configured_receive_message_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
receive_message_count = $script:harnessJson.receive_message_count
configured_receive_message_count = $script:harnessJson.configured_receive_message_count
packet_log_file_configured = $script:harnessJson.packet_log_file_configured
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
}
}