feat(mcp): 协议层换成官方 Model Context Protocol SDK,删自研实现(裁决 10.4.6 + AGENTS §9.5)
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled

- 新增依赖: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:
AskaEth committed 2026-10-04 13:53:47 +08:00
1 parent 7b05b4bfa9
commit a1083a322f
26 files changed
+1903 -3011

No files matched your search

+123
View File
@@ -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"
}
}