Files
Cyrene_v2/internal/capability/mcp/capabilities.go
T
AskaEth a1083a322f
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
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)
2026-10-04 13:53:47 +08:00

136 lines
4.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}