Files
Cyrene_v2/internal/capability/mcp/helpers_test.go
T
AskaEth a1083a322f
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(mcp): 协议层换成官方 Model Context Protocol SDK,删自研实现(裁决 10.4.6 + AGENTS §9.5)
- 新增依赖:github.com/modelcontextprotocol/go-sdk v1.8.0(direct)+ github.com/google/jsonschema-go v0.4.3
  (SDK 自带依赖,直接用于 schema 校验/默认值,避免再造一份);go.sum 纯新增
- 删除自研协议层:rpc.go / stdio.go / http.go / sse.go 及其测试
- 用 SDK 的成熟逻辑:命令传输(stdio 子进程)/ SSE / Streamable HTTP、协议版本协商与
  -32022 回退、keepalive(含失败阈值)、tools/list_changed 订阅、能力协商(InitializeResult.Capabilities)
- 保留自研的四件事(其余零自研):配置→传输、发现→registry 注册、能力族推断、schema 缓存(Prepare 命中不重复解析)
- 能力闸诚实化:服务器声明 capabilities 但不含 tools 时 ListTools/DiscoverTools 返回
  ErrCapabilityNotDeclared(不静默空集);完全不声明 capabilities 的老式服务器仍按 legacy 处理
- 复核:build 绿 + capability 全包测试绿(mcp 42 用例,含真子进程 stdio / httptest Streamable HTTP /
  内存传输三类假 server,不依赖外网)+ `-race` 干净;变异 2 条(featuresFrom 恒 true / 删 applySchema 调用)已还原
- 无 REST/WS/env/数据目录变更;包内 API 有增删(见回报),仓外调用方签名未变(未改 cmd/cyrene 与 plugin/manager)
2026-10-04 13:53:47 +08:00

154 lines
4.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mcp
import (
"context"
"encoding/json"
"fmt"
"os"
"testing"
"time"
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
)
// stdioHelperEnv 标记「测试二进制以 stdio MCP server 身份运行」。
const stdioHelperEnv = "CYRENE_MCP_STDIO_HELPER"
// echoToolSpec 返回入参 name(string,默认 "world")的回显工具定义。
func echoToolSpec() *mcpsdk.Tool {
return &mcpsdk.Tool{
Name: "echo",
Description: "回显问候语",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"name": map[string]any{"type": "string", "default": "world"},
},
},
}
}
// addToolSpec 返回入参 a、b(均为必填 number)的加法工具定义。
func addToolSpec() *mcpsdk.Tool {
return &mcpsdk.Tool{
Name: "add",
Description: "两数相加",
InputSchema: map[string]any{
"type": "object",
"required": []string{"a", "b"},
"properties": map[string]any{
"a": map[string]any{"type": "number"},
"b": map[string]any{"type": "number"},
},
},
}
}
// echoHandler 按 schema 解参并回显;arguments 在 SDK 里是 json.RawMessage。
func echoHandler(_ context.Context, req *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) {
var in struct {
Name string `json:"name"`
}
if len(req.Params.Arguments) > 0 {
if err := json.Unmarshal(req.Params.Arguments, &in); err != nil {
return nil, fmt.Errorf("decode echo args: %w", err)
}
}
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: "hello " + in.Name}},
}, nil
}
// addHandler 求和并同时返回文本与结构化结果。
func addHandler(_ context.Context, req *mcpsdk.CallToolRequest) (*mcpsdk.CallToolResult, error) {
var in struct {
A float64 `json:"a"`
B float64 `json:"b"`
}
if err := json.Unmarshal(req.Params.Arguments, &in); err != nil {
return nil, fmt.Errorf("decode add args: %w", err)
}
sum := in.A + in.B
return &mcpsdk.CallToolResult{
Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: fmt.Sprintf("%g", sum)}},
StructuredContent: map[string]any{"sum": sum},
}, nil
}
// addEchoAndAdd 往 srv 挂上两把测试工具。
func addEchoAndAdd(srv *mcpsdk.Server) {
srv.AddTool(echoToolSpec(), echoHandler)
srv.AddTool(addToolSpec(), addHandler)
}
// newFakeServer 构建一台真实的 MCP server(官方 SDK 服务端),挂上 echo/add。
func newFakeServer() *mcpsdk.Server {
srv := mcpsdk.NewServer(&mcpsdk.Implementation{Name: "fake-mcp", Version: "1.0.0"}, nil)
addEchoAndAdd(srv)
return srv
}
// newEmptyServer 构建一台不提供任何工具的服务器(只声明 logging 能力)。
func newEmptyServer() *mcpsdk.Server {
return mcpsdk.NewServer(&mcpsdk.Implementation{Name: "empty-mcp", Version: "1.0.0"}, nil)
}
// startFakeServer 在 SDK 内存传输上跑 srv,返回客户端侧传输。
func startFakeServer(t *testing.T, srv *mcpsdk.Server) mcpsdk.Transport {
t.Helper()
serverT, clientT := mcpsdk.NewInMemoryTransports()
ctx, cancel := context.WithCancel(context.Background())
go func() { _ = srv.Run(ctx, serverT) }()
t.Cleanup(cancel)
return clientT
}
// connectFakeClient 通过内存传输连上一台 fake server。
func connectFakeClient(t *testing.T, srv *mcpsdk.Server, opts ...Option) *Client {
t.Helper()
opts = append(opts, WithTransport(startFakeServer(t, srv)))
c := New(MCPConfig{Name: "fake"}, opts...)
if err := c.Connect(context.Background()); err != nil {
t.Fatalf("Connect: %v", err)
}
t.Cleanup(func() { _ = c.Close() })
return c
}
// toolNames 取出工具名列表。
func toolNames(tools []ToolInfo) []string {
out := make([]string, 0, len(tools))
for _, t := range tools {
out = append(out, t.Name)
}
return out
}
// waitFor 轮询 cond 直到成立或超时。
func waitFor(t *testing.T, timeout time.Duration, what string, cond func() bool) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if cond() {
return
}
time.Sleep(5 * time.Millisecond)
}
t.Fatalf("等待超时:%s", what)
}
// TestMCPStdioHelper 是 stdio 打桩用的辅助进程入口:以真实 MCP server 身份
// 在 stdin/stdout 上讲协议。stdout 是协议通道,因此诊断只写 stderr,且退出时
// 直接 os.Exit(避免测试框架往 stdout 打 PASS/ok 破坏协议帧)。
func TestMCPStdioHelper(t *testing.T) {
if os.Getenv(stdioHelperEnv) != "1" {
t.Skip("仅作为 stdio 辅助进程入口")
}
srv := newFakeServer()
if err := srv.Run(context.Background(), &mcpsdk.StdioTransport{}); err != nil {
fmt.Fprintln(os.Stderr, "stdio helper server:", err)
os.Exit(1)
}
os.Exit(0)
}