mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-05-25 16:00:35 +00:00
639b32703a
* Support streaming * fix: stream pico reasoning updates Route Pico reasoning through the active streamer and hide empty thought placeholders. * fix: harden configured streaming delivery * fix ci * fix split issue
153 lines
4.0 KiB
Go
153 lines
4.0 KiB
Go
//go:build integration
|
|
|
|
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
sdkmcp "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
)
|
|
|
|
// TestIntegration_RealConfiguredServer is an opt-in smoke test for a real MCP
|
|
// server configured via environment variables.
|
|
//
|
|
// Run with:
|
|
//
|
|
// go test -tags=integration ./pkg/mcp -run TestIntegration_RealConfiguredServer -v
|
|
//
|
|
// Minimum configuration:
|
|
//
|
|
// PICOCLAW_MCP_REAL_SERVER_JSON='{"enabled":true,"type":"http","url":"http://127.0.0.1:8080/mcp"}'
|
|
//
|
|
// Optional tool invocation:
|
|
//
|
|
// PICOCLAW_MCP_REAL_TOOL_NAME=echo
|
|
// PICOCLAW_MCP_REAL_TOOL_ARGS_JSON='{"message":"hello"}'
|
|
// PICOCLAW_MCP_REAL_EXPECT_SUBSTRING=hello
|
|
//
|
|
// Stdio subprocess example:
|
|
//
|
|
// PICOCLAW_MCP_REAL_SERVER_JSON='{"enabled":true,"type":"stdio","command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","."]}'
|
|
func TestIntegration_RealConfiguredServer(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test in short mode")
|
|
}
|
|
|
|
serverJSON := strings.TrimSpace(os.Getenv("PICOCLAW_MCP_REAL_SERVER_JSON"))
|
|
if serverJSON == "" {
|
|
t.Skip("skipping integration test (set PICOCLAW_MCP_REAL_SERVER_JSON to enable)")
|
|
}
|
|
|
|
serverCfg, err := loadRealServerConfig(serverJSON)
|
|
if err != nil {
|
|
t.Fatalf("loadRealServerConfig() error = %v", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
mgr := NewManager()
|
|
if err := mgr.ConnectServer(ctx, "real", serverCfg); err != nil {
|
|
t.Fatalf("ConnectServer() error = %v", err)
|
|
}
|
|
defer func() {
|
|
if err := mgr.Close(); err != nil {
|
|
t.Errorf("Manager.Close() error = %v", err)
|
|
}
|
|
}()
|
|
|
|
tools := mgr.GetAllTools()["real"]
|
|
if len(tools) == 0 {
|
|
t.Fatal("expected at least one discovered tool from real MCP server")
|
|
}
|
|
|
|
t.Logf(
|
|
"connected to real MCP server via %s with %d tool(s)",
|
|
config.EffectiveMCPTransportType(serverCfg),
|
|
len(tools),
|
|
)
|
|
for _, tool := range tools {
|
|
if tool != nil {
|
|
t.Logf("discovered tool: %s", tool.Name)
|
|
}
|
|
}
|
|
|
|
if expectedCountRaw := strings.TrimSpace(os.Getenv("PICOCLAW_MCP_REAL_EXPECT_TOOL_COUNT")); expectedCountRaw != "" {
|
|
expectedCount, err := strconv.Atoi(expectedCountRaw)
|
|
if err != nil {
|
|
t.Fatalf("invalid PICOCLAW_MCP_REAL_EXPECT_TOOL_COUNT %q: %v", expectedCountRaw, err)
|
|
}
|
|
if len(tools) != expectedCount {
|
|
t.Fatalf("tool count = %d, want %d", len(tools), expectedCount)
|
|
}
|
|
}
|
|
|
|
toolName := strings.TrimSpace(os.Getenv("PICOCLAW_MCP_REAL_TOOL_NAME"))
|
|
if toolName == "" {
|
|
return
|
|
}
|
|
|
|
toolArgs, err := loadRealToolArgs(os.Getenv("PICOCLAW_MCP_REAL_TOOL_ARGS_JSON"))
|
|
if err != nil {
|
|
t.Fatalf("loadRealToolArgs() error = %v", err)
|
|
}
|
|
|
|
result, err := mgr.CallTool(ctx, "real", toolName, toolArgs)
|
|
if err != nil {
|
|
t.Fatalf("CallTool(%q) error = %v", toolName, err)
|
|
}
|
|
|
|
textPayload := joinTextContents(result)
|
|
t.Logf("tool %q returned text payload: %q", toolName, textPayload)
|
|
|
|
if want := os.Getenv("PICOCLAW_MCP_REAL_EXPECT_SUBSTRING"); want != "" && !strings.Contains(textPayload, want) {
|
|
t.Fatalf("tool result %q does not contain expected substring %q", textPayload, want)
|
|
}
|
|
}
|
|
|
|
func loadRealServerConfig(raw string) (config.MCPServerConfig, error) {
|
|
var cfg config.MCPServerConfig
|
|
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
|
return config.MCPServerConfig{}, err
|
|
}
|
|
if !cfg.Enabled {
|
|
cfg.Enabled = true
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func loadRealToolArgs(raw string) (map[string]any, error) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return map[string]any{}, nil
|
|
}
|
|
|
|
var args map[string]any
|
|
if err := json.Unmarshal([]byte(raw), &args); err != nil {
|
|
return nil, err
|
|
}
|
|
return args, nil
|
|
}
|
|
|
|
func joinTextContents(result *sdkmcp.CallToolResult) string {
|
|
if result == nil || len(result.Content) == 0 {
|
|
return ""
|
|
}
|
|
|
|
parts := make([]string, 0, len(result.Content))
|
|
for _, content := range result.Content {
|
|
if text, ok := content.(*sdkmcp.TextContent); ok && text != nil {
|
|
parts = append(parts, text.Text)
|
|
}
|
|
}
|
|
return strings.Join(parts, "\n")
|
|
}
|