Files
Cyrene_v2/internal/capability/mcp/schemacache.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

143 lines
4.0 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 (
"strings"
"sync"
"github.com/google/jsonschema-go/jsonschema"
)
// SchemaCache 缓存 tools/list 里已解析的 input schema(design/09 §1.1)。
//
// 键 = 服务器名 + 工具名;收到 notifications/tools/list_changed 时按服务器整表失效。
// 命中时 CallTool 直接用缓存 schema 校验参数并补默认值,不重复拉取/解析 schema。
// 并发安全;命中/未命中计数对调用方可读,便于验证缓存真的生效。
type SchemaCache struct {
mu sync.Mutex
entries map[string]*cachedSchema
hits int
misses int
}
// cachedSchema 是一条缓存:原始 schema(模型可见的形状)+ 已解析结果。
// resolved 为 nil 表示 schema 解析失败,调用时降级为不校验。
type cachedSchema struct {
raw map[string]any
resolved *jsonschema.Resolved
}
// NewSchemaCache 构建空缓存。
func NewSchemaCache() *SchemaCache {
return &SchemaCache{entries: map[string]*cachedSchema{}}
}
// Put 解析并缓存一条 input schema。解析失败时仍缓存原始 schema(校验降级)并返回
// 解析错误,由调用方决定是否告警。
func (c *SchemaCache) Put(server, tool string, schema map[string]any) error {
if schema == nil {
schema = emptyObjectSchema()
}
entry := &cachedSchema{raw: schema}
resolved, err := resolveSchema(schema)
entry.resolved = resolved
c.mu.Lock()
c.entries[cacheKey(server, tool)] = entry
c.mu.Unlock()
return err
}
// Get 返回缓存的原始 input schema。
func (c *SchemaCache) Get(server, tool string) (map[string]any, bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.entries[cacheKey(server, tool)]
if !ok {
return nil, false
}
return entry.raw, true
}
// Prepare 返回「已补默认值、已校验」的参数:
//
// - 命中缓存:用缓存 schema 补默认值 + 校验(计一次命中),不再重复拉取/解析;
// - 未命中:有 fallback(本次 tools/list 发现结果)则解析后缓存再用;没有可用
// schema 时原样返回(计一次未命中),即首次工具清单到达前的直接调用降级为不校验。
//
// 传给服务器的是返回的参数值;args 为 nil 时按空对象处理。
func (c *SchemaCache) Prepare(server, tool string, fallback, args map[string]any) (map[string]any, error) {
entry := c.lookup(server, tool)
if entry == nil {
if fallback == nil {
return args, nil
}
// 解析失败不阻断调用:Put 已把原始 schema 放进缓存,resolved 为 nil 即不校验。
_ = c.Put(server, tool, fallback)
entry = c.lookup(server, tool)
}
if entry == nil || entry.resolved == nil {
return args, nil
}
if args == nil {
args = map[string]any{}
}
if err := applySchema(entry.resolved, args); err != nil {
return nil, err
}
return args, nil
}
// lookup 取一条缓存并记账(命中/未命中)。
func (c *SchemaCache) lookup(server, tool string) *cachedSchema {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.entries[cacheKey(server, tool)]
if !ok {
c.misses++
return nil
}
c.hits++
return entry
}
// Invalidate 让某台服务器的全部缓存失效;工具清单变化时调用。
func (c *SchemaCache) Invalidate(server string) {
prefix := server + keySeparator
c.mu.Lock()
defer c.mu.Unlock()
for k := range c.entries {
if strings.HasPrefix(k, prefix) {
delete(c.entries, k)
}
}
}
// Clear 清空所有缓存。
func (c *SchemaCache) Clear() {
c.mu.Lock()
defer c.mu.Unlock()
c.entries = map[string]*cachedSchema{}
}
// Len 返回当前缓存条目数。
func (c *SchemaCache) Len() int {
c.mu.Lock()
defer c.mu.Unlock()
return len(c.entries)
}
// Stats 返回命中次数与未命中次数。
func (c *SchemaCache) Stats() (hits, misses int) {
c.mu.Lock()
defer c.mu.Unlock()
return c.hits, c.misses
}
// keySeparator 分隔服务器名与工具名,避免不同服务器同名工具互相覆盖。
const keySeparator = "\x00"
// cacheKey 拼接缓存键。
func cacheKey(server, tool string) string {
return server + keySeparator + tool
}