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)
This commit is contained in:
1 parent
7b05b4bfa9
commit
a1083a322f
26 files changed
+1903
-3011
No files matched your search
@@ -0,0 +1,119 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// TestFeaturesFrom_DeclarationMatrix 覆盖「声明了才算可用」的判定矩阵。
|
||||
func TestFeaturesFrom_DeclarationMatrix(t *testing.T) {
|
||||
t.Parallel()
|
||||
host := hostFeatures{sampling: true, elicitation: true, roots: true}
|
||||
tests := []struct {
|
||||
name string
|
||||
caps map[string]any
|
||||
wantTools bool
|
||||
wantPrompts bool
|
||||
wantSampling bool
|
||||
}{
|
||||
{name: "未声明任何能力(legacy 服务器)", caps: map[string]any{}, wantTools: true, wantSampling: false},
|
||||
{name: "只有 tools", caps: map[string]any{"tools": map[string]any{}}, wantTools: true, wantSampling: true},
|
||||
{name: "只有 logging", caps: map[string]any{"logging": map[string]any{}}, wantTools: false, wantSampling: true},
|
||||
{name: "tools+prompts", caps: map[string]any{"tools": map[string]any{}, "prompts": map[string]any{}}, wantTools: true, wantPrompts: true, wantSampling: true},
|
||||
{name: "tools 为 null 视为未声明", caps: map[string]any{"tools": nil}, wantTools: false, wantSampling: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := featuresFrom(tt.caps, host)
|
||||
if got.Tools != tt.wantTools {
|
||||
t.Fatalf("Tools = %v, 期望 %v(caps=%v)", got.Tools, tt.wantTools, tt.caps)
|
||||
}
|
||||
if got.Prompts != tt.wantPrompts {
|
||||
t.Fatalf("Prompts = %v, 期望 %v", got.Prompts, tt.wantPrompts)
|
||||
}
|
||||
if got.Sampling != tt.wantSampling {
|
||||
t.Fatalf("Sampling = %v, 期望 %v", got.Sampling, tt.wantSampling)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestFeaturesFrom_HostNotConfigured 宿主没配回调时,即使服务器声明了能力也不可用。
|
||||
func TestFeaturesFrom_HostNotConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := featuresFrom(map[string]any{"tools": map[string]any{}}, hostFeatures{})
|
||||
if got.Sampling || got.Elicitation || got.Roots {
|
||||
t.Fatalf("宿主未配置任何客户端能力,却推断为可用:%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_SamplingHandler_RefusedWhenNotDeclared 服务器未声明能力族时,
|
||||
// 包装后的 sampling 回调必须直接拒绝,不放行给宿主。
|
||||
func TestClient_SamplingHandler_RefusedWhenNotDeclared(t *testing.T) {
|
||||
t.Parallel()
|
||||
called := false
|
||||
c := New(MCPConfig{Name: "s"}, WithSamplingHandler(func(context.Context, *mcpsdk.CreateMessageRequest) (*mcpsdk.CreateMessageResult, error) {
|
||||
called = true
|
||||
return &mcpsdk.CreateMessageResult{}, nil
|
||||
}))
|
||||
h := c.wrapSampling()
|
||||
if h == nil {
|
||||
t.Fatal("配了 sampling 回调,wrapSampling 却返回 nil")
|
||||
}
|
||||
// 未连接(等价服务器什么都没声明):必须拒绝。
|
||||
if _, err := h(context.Background(), &mcpsdk.CreateMessageRequest{}); !errors.Is(err, ErrCapabilityNotDeclared) {
|
||||
t.Fatalf("未声明时 error = %v, 期望 ErrCapabilityNotDeclared", err)
|
||||
}
|
||||
if called {
|
||||
t.Fatal("宿主回调在服务器未声明能力时被调用了")
|
||||
}
|
||||
|
||||
// 服务器声明过能力族 → 放行。
|
||||
c.mu.Lock()
|
||||
c.info.Features = featuresFrom(map[string]any{"tools": map[string]any{}}, c.hostFeatures())
|
||||
c.mu.Unlock()
|
||||
if _, err := h(context.Background(), &mcpsdk.CreateMessageRequest{}); err != nil {
|
||||
t.Fatalf("已声明时 error = %v, 期望放行", err)
|
||||
}
|
||||
if !called {
|
||||
t.Fatal("已声明时宿主回调没有被调用")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_SamplingHandler_RefusedWhenHostNotConfigured 没配回调就不包装(返回 nil)。
|
||||
func TestClient_SamplingHandler_RefusedWhenHostNotConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := New(MCPConfig{Name: "s"})
|
||||
if c.wrapSampling() != nil {
|
||||
t.Fatal("未配置 sampling 回调时不应注册 SDK handler")
|
||||
}
|
||||
if c.wrapElicitation() != nil {
|
||||
t.Fatal("未配置 elicitation 回调时不应注册 SDK handler")
|
||||
}
|
||||
}
|
||||
|
||||
// TestClient_ClientCapabilities_OnlyConfigured 客户端只宣告宿主真正具备的能力。
|
||||
func TestClient_ClientCapabilities_OnlyConfigured(t *testing.T) {
|
||||
t.Parallel()
|
||||
bare := New(MCPConfig{Name: "s"}).clientCapabilities()
|
||||
if bare.Sampling != nil || bare.Elicitation != nil || bare.RootsV2 != nil {
|
||||
t.Fatalf("默认客户端能力应为空:%+v", bare)
|
||||
}
|
||||
full := New(MCPConfig{Name: "s"},
|
||||
WithSamplingHandler(func(context.Context, *mcpsdk.CreateMessageRequest) (*mcpsdk.CreateMessageResult, error) {
|
||||
return nil, nil
|
||||
}),
|
||||
WithElicitationHandler(func(context.Context, *mcpsdk.ElicitRequest) (*mcpsdk.ElicitResult, error) {
|
||||
return nil, nil
|
||||
}),
|
||||
WithRoots(&mcpsdk.Root{URI: "file:///workspace"}),
|
||||
).clientCapabilities()
|
||||
if full.Sampling == nil || full.Elicitation == nil || full.RootsV2 == nil {
|
||||
t.Fatalf("配置齐全时客户端能力不全:%+v", full)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user