主人 2026-10-24:「能否将每个 MCP 服务器所承载的插件和工具按上游(来源)服务器分类
并与其他插件相对隔离?」
归属信息此前后端根本没有(GET /admin/plugins 每条只有 name/kind/description/两个
布尔),而 `mcp:` 是每台 server 一条的 KindMCP 条目的注册键,**不是**工具的命名空间
(工具名由 server 原样给出,连前缀都没有)⇒ 只能靠注册时的元数据,不能解析名字。
后端(唯一汇点盖章):
- 新增 pkg/plugins/sdk/origin.go:Origin{Kind,Server,Transport} + 可选接口
OriginAware(没实现 = 宿主内置,老工具零改动)+ OriginOf(零值视为无来源)。
- registry.Capability 加 Origin 字段;RegisterTool 盖章(覆盖热装与 .env
MCP_SERVERS_JSON 两条接入路径的合流处),ReplaceTool **同样重新盖章**(它逐字段
重建条目,漏了就是「热替换把来源抹掉」)。
- mcp.mcpTool 实现 Origin()(server 名与传输都在 cfg 上),transport 用新的
transportNameOf 报**实际会用的**形态(stdio/sse/http),与 newTransport 同一推断。
- 网关 PluginSummary 加可选 origin(omitempty):零值 ⇒ 键缺席,不是空对象。
- 卸载守卫:对单把 MCP 工具或 `mcp:<server>` 登记条目直接 400
(plugin_uninstall_scope)并指向「MCP 服务器」页 —— 原来会绕过插件管理器裸删,
server 的 names 账本不同步、重连后工具复活。判据是来源章,不是名字前缀。
前端(呈现分组):
- admin/modules/pluginOrigin.ts:纯函数按 origin.server 分节(按 server 名排序),
transport 未报告时标题省略括号,缺 server 名不冒充内置。
- Plugins.tsx:工具段两级(`来自 fs(stdio)· N 把工具` + 宿主内置),MCP 工具行去掉
「卸载」改为指向 MCP 页;`kind:"mcp"` 登记条目与未知 kind(iot)各有段如实显示,
不再静默丢弃(design/09 §5.2.1 红线③)。
零影响(有断言):tool_policies 表/策略写路径未改;ToolSchemas 不发来源(测试断言
模型面既无 origin 键也无 server 名);停用 server ⇒ 条目连来源一起消失,无孤儿。
测试:registry 盖章/保章(两条均已实测变异变红)/内置零值/改策略不清章;mcp 注册
路径盖章 + transport 推断表;网关读面缺席与存在、热装整链、卸载守卫;前端 esbuild+cjs
逻辑测试 12 项。验收:go build/vet/test 全绿,frontend `npm run build` 通过。
54 lines
2.3 KiB
Go
54 lines
2.3 KiB
Go
package sdk
|
||
|
||
// Origin 描述一个能力的**上游来源**:它是谁提供的,而不是它叫什么。
|
||
//
|
||
// 它存在的理由是「归属不能从名字里解析」:MCP server 的工具名由 server 自己给,
|
||
// 宿主不在上面加任何前缀(同名的两把工具在注册表里至多一个 owner,见
|
||
// internal/capability/mcp/registry.go),因此「这把工具来自哪台 server」只能由
|
||
// **注册那一刻的元数据**回答,不能靠字符串推断(改名、重名、前缀碰撞都会错)。
|
||
//
|
||
// 宿主内置能力(plugins/tools/**、web_search、read_file ……)**不实现** OriginAware,
|
||
// 它们的来源是「宿主自己」,不需要也不应该编一个章出来 —— 管理层面上「没有 origin
|
||
// 键」正是「宿主内置」的表达。
|
||
type Origin struct {
|
||
// Kind 是来源种类。目前只有 OriginKindMCP;空串 = 无来源。
|
||
Kind string `json:"kind"`
|
||
// Server 是提供该能力的上游 server 名(= plugins.json / MCP_SERVERS_JSON 里的
|
||
// name),不是工具名。
|
||
Server string `json:"server,omitempty"`
|
||
// Transport 是该 server 的传输形态(stdio / sse / http),来自配置而不是
|
||
// 工具的自我描述。
|
||
Transport string `json:"transport,omitempty"`
|
||
}
|
||
|
||
// OriginKindMCP 表示该能力由一台外部 MCP server 提供。
|
||
const OriginKindMCP = "mcp"
|
||
|
||
// OriginAware 由**自带来源**的能力实现:宿主在注册时据此给能力条目盖章。
|
||
//
|
||
// 它是可选接口 —— 没实现的一律按「宿主内置 / 来源未知」处理,因此既有的工具
|
||
// 实现零改动。实现方只需回报自己的来源,不参与任何路由或权限判定。
|
||
type OriginAware interface {
|
||
// Origin 返回该能力的上游来源;返回零值 Origin 表示「无可归属的来源」。
|
||
Origin() Origin
|
||
}
|
||
|
||
// IsZero 报告 o 是否是「没有来源」的零值。
|
||
func (o Origin) IsZero() bool {
|
||
return o.Kind == "" && o.Server == "" && o.Transport == ""
|
||
}
|
||
|
||
// OriginOf 返回 v 自报的来源。v 没实现 OriginAware,或自报的是零值(= 无来源)时,
|
||
// ok 为 false —— 调用方据此**省略**来源字段,而不是下发一个空对象。
|
||
func OriginOf(v any) (Origin, bool) {
|
||
oa, ok := v.(OriginAware)
|
||
if !ok {
|
||
return Origin{}, false
|
||
}
|
||
o := oa.Origin()
|
||
if o.IsZero() {
|
||
return Origin{}, false
|
||
}
|
||
return o, true
|
||
}
|