- 新增依赖: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)
110 lines
3.2 KiB
Go
110 lines
3.2 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
sdkjsonrpc "github.com/modelcontextprotocol/go-sdk/jsonrpc"
|
|
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
// TestClient_KeepAlive_SendsPings keepalive 接线:配了间隔后 ping 真的上了线路。
|
|
// ping 本身由 SDK 负责,这里只验我们把配置传导到了 SDK 会话上。
|
|
func TestClient_KeepAlive_SendsPings(t *testing.T) {
|
|
t.Parallel()
|
|
sn := &sniffTransport{inner: startFakeServer(t, newFakeServer())}
|
|
c := New(MCPConfig{Name: "fake"},
|
|
WithTransport(sn),
|
|
// 固定到 keepalive 生效的旧协议版本,避免新版协议的订阅路径干扰。
|
|
WithProtocolVersion("2025-11-25"),
|
|
WithKeepAlive(20*time.Millisecond, 1))
|
|
if err := c.Connect(context.Background()); err != nil {
|
|
t.Fatalf("Connect: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if got := c.ServerInfo().ProtocolVersion; got != "2025-11-25" {
|
|
t.Fatalf("协议版本 = %q, 期望 2025-11-25", got)
|
|
}
|
|
waitFor(t, 5*time.Second, "线路上出现 keepalive ping", func() bool { return sn.called("ping") })
|
|
}
|
|
|
|
// TestClient_NoKeepAlive_NoPings 不配 keepalive 时不发 ping(反证:ping 不是 SDK 默认行为)。
|
|
func TestClient_NoKeepAlive_NoPings(t *testing.T) {
|
|
t.Parallel()
|
|
sn := &sniffTransport{inner: startFakeServer(t, newFakeServer())}
|
|
c := New(MCPConfig{Name: "fake"}, WithTransport(sn), WithProtocolVersion("2025-11-25"))
|
|
if err := c.Connect(context.Background()); err != nil {
|
|
t.Fatalf("Connect: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if _, err := c.DiscoverTools(context.Background()); err != nil {
|
|
t.Fatalf("DiscoverTools: %v", err)
|
|
}
|
|
time.Sleep(150 * time.Millisecond)
|
|
if sn.called("ping") {
|
|
t.Fatal("没配 keepalive 却发了 ping")
|
|
}
|
|
if !sn.called("tools/list") {
|
|
t.Fatalf("线路上没有 tools/list, 已见方法 = %v", sn.methods())
|
|
}
|
|
}
|
|
|
|
// sniffTransport 包一层 SDK 传输,记录客户端发出的 JSON-RPC 请求方法,
|
|
// 用于在线路层断言客户端确实发了某个请求。
|
|
type sniffTransport struct {
|
|
inner mcpsdk.Transport
|
|
mu sync.Mutex
|
|
seen []string
|
|
}
|
|
|
|
// Connect 实现 mcpsdk.Transport。
|
|
func (t *sniffTransport) Connect(ctx context.Context) (mcpsdk.Connection, error) {
|
|
conn, err := t.inner.Connect(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &sniffConn{Connection: conn, owner: t}, nil
|
|
}
|
|
|
|
// called 报告某个方法是否出现过。
|
|
func (t *sniffTransport) called(method string) bool {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
for _, m := range t.seen {
|
|
if m == method {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// methods 返回已见方法快照。
|
|
func (t *sniffTransport) methods() []string {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return append([]string(nil), t.seen...)
|
|
}
|
|
|
|
// record 记录一个发出的方法名。
|
|
func (t *sniffTransport) record(method string) {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
t.seen = append(t.seen, method)
|
|
}
|
|
|
|
// sniffConn 拦截写入的请求消息,记录方法名后交给内层连接。
|
|
type sniffConn struct {
|
|
mcpsdk.Connection
|
|
owner *sniffTransport
|
|
}
|
|
|
|
// Write 记录请求方法后转发。
|
|
func (c *sniffConn) Write(ctx context.Context, msg sdkjsonrpc.Message) error {
|
|
if req, ok := msg.(*sdkjsonrpc.Request); ok {
|
|
c.owner.record(req.Method)
|
|
}
|
|
return c.Connection.Write(ctx, msg)
|
|
}
|