design/09 §5.2.1 / §5.6.1:补上「看不见连没连上 / 暴露了哪些工具 / 为什么挂了」的洞。
- runtimeconfig:PluginConfig 新增 `enabled`(*bool,缺省=启用 ⇒ 既有 plugins.json
向后兼容);新增 RedactPluginSecrets(整把凭据 + 任意 ≥6 字符片段一并抹除)。
- mcp:新增 status.go 的只读状态读面(ToolSummaries:工具名 + 截断描述,取自能力
注册表本身,因此与模型可见的工具集合同源)。
- plugin 管理器:
· Install/Restore 尊重 enabled(停用=落盘不连接,重启后仍是停用,且一次都不连);
· SetEnabled 落盘 enabled(停用先落盘再下线;启用失败时 enabled 保持 false);
· 新增 Reconnect(对已停用者拒绝,不替用户启用);
· 新增 Statuses(三态:connected 缺席=未知,工具数/明细/时间只在知道时给出,
lastError 在记录那一刻即脱敏截断);
· 内置「MCP 服务器」页声明 + 7 个已声明动作(list/install/reconnect/enable/
disable/uninstall/transports),走既有通用动作端点(owner-admin + 审计);
· 护栏:热装插件的生命周期不改动内置提供者(mcp)的目录条目。
- gateway:已安装列表与 install 响应加 enabled,install 支持 config.enabled。
57 lines
2.4 KiB
Go
57 lines
2.4 KiB
Go
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]) + "…"
|
||
}
|