- 新增依赖: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)
124 lines
3.9 KiB
Go
124 lines
3.9 KiB
Go
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"
|
||
}
|
||
}
|