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,123 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// TestNewTransport_SelectsByType 覆盖显式传输类型与历史推断。
|
||||
func TestNewTransport_SelectsByType(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg MCPConfig
|
||||
want any
|
||||
}{
|
||||
{name: "http", cfg: MCPConfig{URL: "http://x", TransportType: "http"}, want: &mcpsdk.StreamableClientTransport{}},
|
||||
{name: "streamable 别名", cfg: MCPConfig{URL: "http://x", TransportType: "streamable"}, want: &mcpsdk.StreamableClientTransport{}},
|
||||
{name: "sse", cfg: MCPConfig{URL: "http://x", TransportType: "sse"}, want: &mcpsdk.SSEClientTransport{}},
|
||||
{name: "stdio", cfg: MCPConfig{Command: "x", TransportType: "stdio"}, want: &mcpsdk.CommandTransport{}},
|
||||
{name: "推断 stdio", cfg: MCPConfig{Command: "x"}, want: &mcpsdk.CommandTransport{}},
|
||||
{name: "推断 sse", cfg: MCPConfig{URL: "http://x"}, want: &mcpsdk.SSEClientTransport{}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tr, err := newTransport(tt.cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("newTransport: %v", err)
|
||||
}
|
||||
if got, want := typeName(tr), typeName(tt.want); got != want {
|
||||
t.Fatalf("传输类型 = %s, 期望 %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewTransport_NoTransport 两个字段都空时必须报 ErrNoTransport。
|
||||
func TestNewTransport_NoTransport(t *testing.T) {
|
||||
t.Parallel()
|
||||
if _, err := newTransport(MCPConfig{Name: "s"}); err == nil {
|
||||
t.Fatal("没有 Command/URL 却选出了传输")
|
||||
}
|
||||
// 显式类型但缺必要字段同样要报错(不静默构造坏传输)。
|
||||
if _, err := newTransport(MCPConfig{TransportType: "http"}); err == nil {
|
||||
t.Fatal("TransportType=http 但 URL 为空却没有报错")
|
||||
}
|
||||
if _, err := newTransport(MCPConfig{TransportType: "stdio"}); err == nil {
|
||||
t.Fatal("TransportType=stdio 但 Command 为空却没有报错")
|
||||
}
|
||||
}
|
||||
|
||||
// TestBearerTransport_SetsAuthorization 凭据以 Authorization: Bearer 下发,
|
||||
// 并且不污染原始请求。
|
||||
func TestBearerTransport_SetsAuthorization(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := make(chan string, 1)
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got <- r.Header.Get("Authorization")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
client := httpClientFor(MCPConfig{APIKey: "sk-1234"})
|
||||
if client == nil {
|
||||
t.Fatal("配了 APIKey 却没有返回自定义 http client")
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, ts.URL, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("NewRequest: %v", err)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("Do: %v", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
if auth := <-got; auth != "Bearer sk-1234" {
|
||||
t.Fatalf("Authorization = %q, 期望 Bearer sk-1234", auth)
|
||||
}
|
||||
if req.Header.Get("Authorization") != "" {
|
||||
t.Fatal("原始请求被就地改写了")
|
||||
}
|
||||
if httpClientFor(MCPConfig{}) != nil {
|
||||
t.Fatal("无 APIKey 时应返回 nil(用 SDK 默认客户端)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestEnvWith_OverridesAndPreserves stdio 子进程环境变量叠加语义。
|
||||
func TestEnvWith_OverridesAndPreserves(t *testing.T) {
|
||||
t.Parallel()
|
||||
base := []string{"PATH=/usr/bin", "HOME=/root"}
|
||||
got := envWith(base, map[string]string{"HOME": "/tmp", "TOKEN": "abc"})
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("len = %d, 期望 3(%v)", len(got), got)
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, kv := range got {
|
||||
seen[kv] = true
|
||||
}
|
||||
if !seen["PATH=/usr/bin"] || !seen["HOME=/tmp"] || !seen["TOKEN=abc"] {
|
||||
t.Fatalf("合并后的环境 = %v", got)
|
||||
}
|
||||
if len(envWith(base, nil)) != len(base) {
|
||||
t.Fatal("空 extra 改变了基础环境")
|
||||
}
|
||||
}
|
||||
|
||||
// typeName 返回传输的类别名,避免在断言里依赖具体指针比较。
|
||||
func typeName(v any) string {
|
||||
switch v.(type) {
|
||||
case *mcpsdk.StreamableClientTransport:
|
||||
return "streamable"
|
||||
case *mcpsdk.SSEClientTransport:
|
||||
return "sse"
|
||||
case *mcpsdk.CommandTransport:
|
||||
return "stdio"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user