- 新增依赖: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)
154 lines
4.6 KiB
Go
154 lines
4.6 KiB
Go
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)
|
||
}
|