package mcp // 本文件是 MCP 客户端 registry 的**状态读面**(切片 M20:MCP 服务器的管理页)。 // // 为什么放在 mcp 包而不是插件管理器:工具名与描述的唯一真源是**已注册进能力注册表 // 的那些条目**(`Register`/`refresh` 写进去、`Unregister` 摘掉的正是它们)。管理器只 // 知道「这个 server 活没活」,它没有工具描述;让它自己去猜工具清单就会出现「面板上的 // 工具列表」与「模型真正能调的工具」两份事实。这里只读,不改任何注册/刷新逻辑。 // ToolSummary 是一把工具在状态快照里的形状:名字 + 截断后的描述。 // // 描述必然要截断:MCP 服务器的描述可以是一整段 Markdown(实测有几百字的),而状态页是 // 给人扫一眼的排障面,把原文全塞进表格只会把有用的列挤没。 type ToolSummary struct { // Name 是工具名(模型调用时用的标识,不截断:它是排障时要对的那个字符串)。 Name string `json:"name"` // Description 是截断后的描述(原始描述见 mcp 客户端的 tools/list)。 Description string `json:"description,omitempty"` } // ToolSummaries 返回本 registry 当前注册的工具摘要(按注册顺序)。 // // 「当前注册」= `ToolNames` 里还活着的名字;被 `refresh` 摘掉的工具不会留下影子。 // descriptionLimit ≤ 0 表示不截断。描述取自能力注册表的条目:**注册失败的工具不在 // 其中**,因此清单与模型可见的工具集合同源。 func (r *Registry) ToolSummaries(descriptionLimit int) []ToolSummary { if r == nil { return nil } names := r.ToolNames() out := make([]ToolSummary, 0, len(names)) for _, name := range names { desc := "" if r.reg != nil { if cap, err := r.reg.Get(name); err == nil { desc = cap.Description } } out = append(out, ToolSummary{Name: name, Description: TruncateRunes(desc, descriptionLimit)}) } return out } // TruncateRunes 把 s 截到最多 limit 个字符(按 rune 计,不切坏多字节字符);被截断时 // 末尾补一个省略号,让「这里还有」在界面上可见(静默截断会被读成「原文就这么长」)。 // limit ≤ 0 表示不截断。 func TruncateRunes(s string, limit int) string { if limit <= 0 { return s } runes := []rune(s) if len(runes) <= limit { return s } return string(runes[:limit]) + "…" }