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,135 @@
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
// hostFeatures 表示宿主侧配置了哪些客户端能力(决定向服务器宣告什么)。
|
||||
type hostFeatures struct {
|
||||
sampling bool
|
||||
elicitation bool
|
||||
roots bool
|
||||
}
|
||||
|
||||
// hostFeatures 返回宿主侧已配置的客户端能力。
|
||||
func (c *Client) hostFeatures() hostFeatures {
|
||||
return hostFeatures{
|
||||
sampling: c.sampling != nil,
|
||||
elicitation: c.elicitation != nil,
|
||||
roots: len(c.roots) > 0,
|
||||
}
|
||||
}
|
||||
|
||||
// clientCapabilities 构造客户端能力声明:只宣告宿主真的具备的能力。
|
||||
// 显式传非 nil 的 Capabilities 会关掉 SDK 默认宣告的 roots——本包默认不宣告任何
|
||||
// 能力,避免对未声明能力的服务器暴露工作区根目录或代跑 LLM。
|
||||
func (c *Client) clientCapabilities() *mcpsdk.ClientCapabilities {
|
||||
caps := &mcpsdk.ClientCapabilities{}
|
||||
if c.sampling != nil {
|
||||
caps.Sampling = &mcpsdk.SamplingCapabilities{}
|
||||
}
|
||||
if c.elicitation != nil {
|
||||
caps.Elicitation = &mcpsdk.ElicitationCapabilities{}
|
||||
}
|
||||
if len(c.roots) > 0 {
|
||||
caps.RootsV2 = &mcpsdk.RootCapabilities{}
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// serverInfoFrom 把 SDK 会话的 initialize 结果整理成本包的 ServerInfo,
|
||||
// 其中 capabilities 原文保留、对客户端可读(design/09 §1.1)。
|
||||
func serverInfoFrom(session *mcpsdk.ClientSession, host hostFeatures) ServerInfo {
|
||||
ir := session.InitializeResult()
|
||||
if ir == nil {
|
||||
return ServerInfo{Capabilities: map[string]any{}}
|
||||
}
|
||||
info := ServerInfo{Instructions: ir.Instructions, ProtocolVersion: ir.ProtocolVersion}
|
||||
if ir.ServerInfo != nil {
|
||||
info.Name = ir.ServerInfo.Name
|
||||
info.Version = ir.ServerInfo.Version
|
||||
}
|
||||
info.Capabilities = capabilitiesMap(ir.Capabilities)
|
||||
info.Features = featuresFrom(info.Capabilities, host)
|
||||
return info
|
||||
}
|
||||
|
||||
// capabilitiesMap 把 SDK 解析出的服务器能力序列化回原文 map 保留下来。
|
||||
// 它只做无损转换,不做增删;极端情况下序列化失败则返回空 map(等价服务器未声明)。
|
||||
func capabilitiesMap(caps *mcpsdk.ServerCapabilities) map[string]any {
|
||||
if caps == nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
data, err := json.Marshal(caps)
|
||||
if err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
out := map[string]any{}
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return map[string]any{}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// featuresFrom 依据服务器 initialize 声明推断可用能力族(design/09 §1.1:
|
||||
// 声明了才算可用,未声明不得调用):
|
||||
//
|
||||
// - tools / prompts / resources / logging / completions 直接按服务器是否声明该字段;
|
||||
// - 老式服务器完全不发 capabilities(nil 或空)时按 legacy 处理:只认 tools/list
|
||||
// 是唯一发现入口(Tools=true),其余一律不可用;
|
||||
// - sampling / elicitation / roots 属于客户端侧能力,服务器不会声明「自身支持」它们。
|
||||
// 因此判定为「宿主配置了回调」且「服务器声明过能力族(即确实是会主动交互的对端)」
|
||||
// 两者同时成立才可用——什么都没声明的服务器不得回调这些能力。
|
||||
func featuresFrom(caps map[string]any, host hostFeatures) ServerFeatures {
|
||||
declared := len(caps) > 0
|
||||
f := ServerFeatures{
|
||||
Tools: !declared || hasCap(caps, "tools"),
|
||||
Prompts: hasCap(caps, "prompts"),
|
||||
Resources: hasCap(caps, "resources"),
|
||||
Logging: hasCap(caps, "logging"),
|
||||
Completions: hasCap(caps, "completions"),
|
||||
}
|
||||
f.Sampling = host.sampling && declared
|
||||
f.Elicitation = host.elicitation && declared
|
||||
f.Roots = host.roots && declared
|
||||
return f
|
||||
}
|
||||
|
||||
// hasCap 报告原始 capabilities 里是否声明了名为 name 的能力。
|
||||
func hasCap(caps map[string]any, name string) bool {
|
||||
v, ok := caps[name]
|
||||
return ok && v != nil
|
||||
}
|
||||
|
||||
// wrapSampling 包装宿主 sampling 回调:服务器未声明能力族时拒绝,
|
||||
// 不静默放行(design/09 §1.1)。
|
||||
func (c *Client) wrapSampling() func(context.Context, *mcpsdk.CreateMessageRequest) (*mcpsdk.CreateMessageResult, error) {
|
||||
h := c.sampling
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return func(ctx context.Context, req *mcpsdk.CreateMessageRequest) (*mcpsdk.CreateMessageResult, error) {
|
||||
if !c.Features().Sampling {
|
||||
return nil, fmt.Errorf("mcp sampling %q: %w", c.cfg.Name, ErrCapabilityNotDeclared)
|
||||
}
|
||||
return h(ctx, req)
|
||||
}
|
||||
}
|
||||
|
||||
// wrapElicitation 包装宿主 elicitation 回调,判定同 wrapSampling。
|
||||
func (c *Client) wrapElicitation() func(context.Context, *mcpsdk.ElicitRequest) (*mcpsdk.ElicitResult, error) {
|
||||
h := c.elicitation
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
return func(ctx context.Context, req *mcpsdk.ElicitRequest) (*mcpsdk.ElicitResult, error) {
|
||||
if !c.Features().Elicitation {
|
||||
return nil, fmt.Errorf("mcp elicitation %q: %w", c.cfg.Name, ErrCapabilityNotDeclared)
|
||||
}
|
||||
return h(ctx, req)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user