Documentation
¶
Overview ¶
Package runtime provides the hand-written runtime substrate for the auto-generated MCP tool registrations under mcp/tools.
Index ¶
- Constants
- func AnyInputSchema[T any]() *jsonschema.Schema
- func HTTPHeaders(next http.Handler) http.Handler
- func HTTPLogID(next http.Handler) http.Handler
- func HeadersFromContext(ctx context.Context) http.Header
- func LogIDFromContext(ctx context.Context) string
- func NewOwnedID() string
- func OwnerOf(id string) (hostPort string, ok bool)
- func OwnerRoutedParam(toolName string) (paramName string, ok bool)
- func OwnerRoutedPathRegistered(prefix string) bool
- func PeerAllowed(hostPort string) bool
- func PublicBaseURL() string
- func RegisterOwnerRouted(toolName, paramName string)
- func RegisterOwnerRoutedPath(prefix string, fn PathOwnerExtractor)
- func RegisterTool(t ToolInfo)
- func ResetOwnerRoutedForTest()
- func ResetOwnerRoutedPathsForTest()
- func ResetToolsForTest()
- func SelfHostPort() string
- func SetLogIDHeader(name string)
- func SetPeerProvider(fn func() []string)
- func SetPeers(hosts []string)
- func SetPublicBaseURL(base string)
- func ToolError(err error) *mcp.CallToolResult
- func ToolText(text string) *mcp.CallToolResult
- func WithHeaders(ctx context.Context, h http.Header) context.Context
- func WithLogID(ctx context.Context, id string) context.Context
- func WithOwnerRouting(next http.Handler) http.Handler
- func WithPathOwnerRouting(next http.Handler) http.Handler
- func WithSubject(ctx context.Context, s *Subject) context.Context
- type Call
- type Config
- type Handler
- type LogConfig
- type Middleware
- type PathOwnerExtractor
- type RegisterOptions
- type Registrar
- type Registry
- func (r *Registry) Config(dst any) error
- func (r *Registry) Handlers() (mcpHandler http.Handler, routes map[string]http.Handler, err error)
- func (r *Registry) Middlewares() []Middleware
- func (r *Registry) Named(name string)
- func (r *Registry) OnBuild(fn func() error)
- func (r *Registry) OnStop(fn func())
- func (r *Registry) PluginNames() []string
- func (r *Registry) RegisterCounts() (registered, filtered int)
- func (r *Registry) Route(pattern string, h http.Handler)
- func (r *Registry) RoutePublic(pattern string, h http.Handler)
- func (r *Registry) Routes() map[string]http.Handler
- func (r *Registry) RunStop(ctx context.Context)
- func (r *Registry) Server() *mcp.Server
- func (r *Registry) Start(ctx context.Context) error
- func (r *Registry) Tool(add func(s *mcp.Server))
- func (r *Registry) Use(mw Middleware)
- type Result
- type Subject
- type TokenAuthz
- func (a *TokenAuthz) HTTPMiddleware(next http.Handler) http.Handler
- func (a *TokenAuthz) IdentityHeaders() []string
- func (a *TokenAuthz) Middleware() Middleware
- func (a *TokenAuthz) ResolveIdentity(get func(name string) string) string
- func (a *TokenAuthz) SubjectOf(token string, get func(name string) string) (*Subject, bool)
- func (a *TokenAuthz) TokenName(token string) (name string, ok bool)
- type TokenAuthzConfig
- type TokenConfig
- type ToolInfo
Constants ¶
const ( MetaDeniedBy = "denied_by" MetaDenyReason = "deny_reason" )
Meta 里由 DenyResult 写入的固定 key,审计插件读这两个 key 记录拒绝详情。
纪律:凡跨插件读取的 key 必须在此处声明常量; 插件私有 key 用「插件名.」前缀避免撞名。
Variables ¶
This section is empty.
Functions ¶
func AnyInputSchema ¶
func AnyInputSchema[T any]() *jsonschema.Schema
AnyInputSchema 反射出 T 的输入 schema,并把其中所有「无类型约束」节点 (interface{} / []any 的 items / map[string]any 的 additionalProperties 等) 放宽为 anyTypes 类型联合。生成器对含 interface{} 入参的工具用它显式设置 Tool.InputSchema,从而绕过 SDK 默认反射产生的空 schema。
func HTTPHeaders ¶ added in v0.5.0
HTTPHeaders 是把请求头快照注入 ctx 的 net/http 中间件,装在 MCP handler 之外。 插件读 Call.Headers(如按配置采集审计头)全靠它。
func HTTPLogID ¶
HTTPLogID 解析或生成本次请求的 logid:优先取配置头(默认 X-Log-Id),缺失或**不合规** 则生成;注入 ctx(供审计/access 日志读取),并回写同名响应头供上游网关串联。
为什么要校验而不是原样采信:logid 是「宿主 access log ↔ 审计事件 ↔ 框架日志」三条线 唯一的 join key,而它来自请求头、完全由调用方控制。终审探针实测:原样采信时 `X-Log-Id: fake logid=deadbeef tool=greeter.greet actor=admin` 会被整串写进审计事件与 日志行,而框架日志是无引号的 key=value 形状,等于让调用方往日志里注入伪造字段; 300 字节的头也照收。校验后不合规就丢弃自生成,注入面随之消失。
**仍然做不到的事**(对账时要知道):合规的 logid 不做去重,调用方每次发同一个值, 审计事件与 access log 就没法一对一。需要严格一对一时请在网关侧保证唯一。
func HeadersFromContext ¶ added in v0.5.0
HeadersFromContext 取请求头快照;缺失或注入的是 nil Header 时返回非 nil 的空 Header(h.Clone() 对 nil 返回 nil),保证插件既能 Get 也能 Set 而不 panic。
func LogIDFromContext ¶
LogIDFromContext 取出 ctx 中的 logid,缺失返回 ""。
func NewOwnedID ¶ added in v0.5.0
func NewOwnedID() string
NewOwnedID 生成内嵌本副本 host:port 的不透明 id,供有状态插件使用。 未配置对外地址(未调用 SetPublicBaseURL)时退回纯随机 id,行为与单机一致。
func OwnerRoutedParam ¶ added in v0.5.0
OwnerRoutedParam 返回某工具声明的 owner 路由参数名;ok=false 表示未声明。 供插件与宿主自检「我的 RegisterOwnerRouted 真的生效了」——漏调它在单副本部署下 完全看不出来,多副本上线才会变成「回执/资源找不到」。
func OwnerRoutedPathRegistered ¶ added in v0.5.0
OwnerRoutedPathRegistered 返回某前缀是否声明过路径 owner 路由,供插件与宿主自检 「我的 RegisterOwnerRoutedPath 真的调到了」——漏调它在单副本部署下完全看不出来。
func PeerAllowed ¶ added in v0.5.0
PeerAllowed 返回 hostPort 是否为当前已知的兄弟副本之一(仅这些地址允许被反代)。 provider 未注册或返回空时返回 false:默认拒绝一切远端转发,防 SSRF。
func RegisterOwnerRouted ¶ added in v0.4.0
func RegisterOwnerRouted(toolName, paramName string)
RegisterOwnerRouted 声明「工具 toolName 按参数 paramName(owned id)路由」。 应在 init/启动期调用,早于开始处理请求。
func RegisterOwnerRoutedPath ¶ added in v0.5.0
func RegisterOwnerRoutedPath(prefix string, fn PathOwnerExtractor)
RegisterOwnerRoutedPath 声明「前缀 prefix 下的请求按提取器给出的 owned id 路由」。 应在启动期调用(插件的 Install 里),早于开始处理请求。
存在的理由:有状态插件的回调入口不一定是 tools/call。带外人工确认走的是 IM → 推送服务 → HTTP 回调,而挂起的请求只存在于发起副本;没有这条路,回调被负载 均衡打到别的副本就是一次静默的「确认丢失」,而单副本部署完全看不出来。
func RegisterTool ¶ added in v0.5.0
func RegisterTool(t ToolInfo)
RegisterTool 登记一个工具的元数据,由生成代码在注册工具时调用。 Labels 会被拷贝一份,避免注册表与调用方共享同一个 map。
func ResetOwnerRoutedForTest ¶ added in v0.5.0
func ResetOwnerRoutedForTest()
ResetOwnerRoutedForTest 清空 owner 路由声明表,**仅测试使用,禁止在运行期调用**: 表是进程级的,运行期清空会让多副本部署静默退化成「回执/资源找不到」。 与 ResetOwnerRoutedPathsForTest 成对存在——两张启动期注册表都要能被包外插件的测试 复位,少一个会让「探针跑一遍看有没有残留」这类自检得出相反结论。
func ResetOwnerRoutedPathsForTest ¶ added in v0.5.0
func ResetOwnerRoutedPathsForTest()
ResetOwnerRoutedPathsForTest 清空路径路由声明表,**仅测试使用**。 理由与 ResetOwnerRoutedForTest 相同:表是进程级的,运行期清空会让多副本部署 静默退化成「回调丢失」。
func ResetToolsForTest ¶ added in v0.5.0
func ResetToolsForTest()
ResetToolsForTest 清空注册表,仅测试使用。 线上调用会清空全部工具元数据,并因此影响 authz 对未登记工具的判定。
func SelfHostPort ¶ added in v0.5.0
func SelfHostPort() string
SelfHostPort 返回本副本对外可达的 host:port;未设置对外地址时为空串。
func SetPeerProvider ¶ added in v0.5.0
func SetPeerProvider(fn func() []string)
SetPeerProvider 注册动态兄弟副本发现函数:每次白名单校验时实时调用它,返回当前 可达的兄弟副本 host:port 列表。传 nil 清除(回退到拒绝一切远端转发)。 与 SetPeers 互为覆盖,后调用者生效。
func SetPeers ¶ added in v0.5.0
func SetPeers(hosts []string)
SetPeers 设置静态兄弟副本列表(通常来自配置),内部包装成返回该快照的 provider。 与 SetPeerProvider 互为覆盖:后调用者生效。空列表等价于无兄弟副本。
func SetPublicBaseURL ¶ added in v0.5.0
func SetPublicBaseURL(base string)
SetPublicBaseURL 设置本副本对外可直连的基础地址;插件拼下载 URL、基座判 id 归属 都以它为准。传空串表示没有对外地址(此时生成的 id 不带归属信息)。
func ToolError ¶
func ToolError(err error) *mcp.CallToolResult
ToolError converts a Go error into an IsError CallToolResult so callers can distinguish tool failures from protocol-level errors.
func ToolText ¶ added in v0.5.0
func ToolText(text string) *mcp.CallToolResult
ToolText 构造一个成功态的纯文本工具结果,供插件自注册的 MCP 工具使用 (TextResult 是链上的 *Result 形式,这里要的是 handler 直接返回的 SDK 结果)。
func WithHeaders ¶ added in v0.5.0
WithHeaders 把请求头快照注入 ctx,由 HTTP 层(net/http 中间件)在进入 MCP handler 前调用,是 HeadersFromContext / Call.Headers 的唯一数据来源。 必须 Clone:Call.Headers 承诺是快照,若直接持有 r.Header,插件写它就会改到活的 *http.Request,与上游 net/http 中间件共享状态。 Clone 之后剔除凭据类头(见 credentialHeaders)。
func WithOwnerRouting ¶ added in v0.4.0
WithOwnerRouting 包裹上游 MCP handler:若请求携带的 owned id 归属兄弟副本,则把整条请求 单跳反代到该副本的**同一路径**(属主本地执行);其余交 next 本地处理。
两种 owner 提取形态:
- HTTP 路径(RegisterOwnerRoutedPath):不读 body,因此 GET/DELETE 这类回调同样适用;
- tools/call 参数(RegisterOwnerRouted):要读 body 解析 JSON-RPC,只对 POST 成立。
func WithPathOwnerRouting ¶ added in v0.5.0
WithPathOwnerRouting 只做**路径形态**的 owner 路由,基座用它包裹插件注册的 HTTP 路由 (见 Registry.Routes)。
与 WithOwnerRouting 分开的理由:后者对 POST 会把整个 body 读进内存来解析 JSON-RPC, 而插件路由的 POST body 形状与大小都不由基座决定(上传类路由完全合法), 无条件缓冲它是一次白送的内存放大器。插件路由要跨副本,就把 id 放进路径。
Types ¶
type Call ¶ added in v0.5.0
type Call struct {
Method string // tools/list | tools/call | ...
Tool string // tools/call 时的工具名
Labels map[string]string // 该工具的完整 labels(含 name/pkg 投影)
// Args 是原始入参。**插件改它会真的生效**:链的执行终点在调用下游之前把
// Call.Tool / Call.Args 回写进合成的请求(见 runtime/chain.go 的 terminus)。
Args json.RawMessage
Headers http.Header // 请求头,插件不得修改;快照语义由注入侧保证
Subject *Subject // 调用主体
LogID string
Meta map[string]any // 插件间通信通道,基座不认识任何 key
Tools func() []ToolInfo // 查全部工具,tools/list 过滤用;由基座在构造 Call 时填充,插件调用前判 nil
}
Call 是一次 MCP 请求的上下文。插件通过它读写调用相关信息。
type Config ¶
type Config struct {
// Addr 是 HTTP 监听地址,例如 ":8080";为空时由系统分配端口。
Addr string
// PublicBaseURL 是 agent 侧可直连的对外基础地址(如 http://host:8011)。
// 跨机多副本部署时必须设置:有归属 id 与 owner 路由都靠它判断「本副本是谁」;
// 为空时回退到实际监听地址(仅同机/本地场景可用)。
PublicBaseURL string
// ConfigPath 指向含 [[tokens]] 等段的 TOML 文件。基座 token 鉴权必须配置,
// 为空即启动失败。
ConfigPath string
// Enable 是工具注册期的包名白名单,空表示不过滤。
Enable []string
// Match 是工具注册期的 label selector,空表示不过滤;语法错误即启动失败。
Match string
// RequiredPlugins 声明必须在场的插件名,缺失即启动失败。
// 全插件化之后基座不认识「审计」「鉴权」这些概念,忘装插件就是静默放开,
// 这个声明是唯一的补偿手段。也可写在配置文件的同名顶层键里(两者取并集)。
RequiredPlugins []string `toml:"required_plugins"`
// Peers 是静态兄弟副本白名单(host:port),供 owner 路由校验反代目标。
// 多副本部署通常改用 SetPeerProvider 对接服务发现。
Peers []string
}
Config 控制 server 的启动行为。只保留基座关心的项: 监听、对外地址、配置文件、工具注册期过滤、必需插件、兄弟副本白名单。 spill 阈值、审计 header 这类能力相关配置由对应插件自己从 ConfigPath 解析。
type Handler ¶ added in v0.5.0
Handler 是链上的一环。
func Chain ¶ added in v0.5.0
func Chain(mws []Middleware, final Handler) Handler
Chain 把中间件按注册顺序包成洋葱:先注册的在最外层,返回时最后被唤醒。 nil 元素被跳过,允许调用方按固定位置传 nil 占位。
panic 不在基座兜底:中间件里的 panic 会原样穿透,交由 MCP SDK / HTTP 层处理。
type LogConfig ¶
type LogConfig struct {
// LogIDHeader 是读取入站 logid 的请求头名,同时作为回写响应头名。
// 缺省用 defaultLogIDHeader(X-Log-Id)。
LogIDHeader string `toml:"logid_header"`
}
LogConfig 是日志相关配置(对应基座 TOML 的 log 段,由 Registry 一并解码)。
type Middleware ¶ added in v0.5.0
Middleware 是唯一的插件注入点,形状与 net/http 中间件一致。
type PathOwnerExtractor ¶ added in v0.5.0
PathOwnerExtractor 从一次 HTTP 请求里取出 owned id;返回空串表示「这条请求不参与 owner 路由」(本副本按普通请求处理)。
type RegisterOptions ¶
type RegisterOptions struct {
Enable []string // 包名白名单,空表示不过滤
Match string // selector 字符串,空表示不过滤
// contains filtered or unexported fields
}
RegisterOptions 控制注册哪些生成的工具。
生成代码按值接收它并逐个工具调用 Allow,因此新增字段必须是「拷贝安全」的: 需要跨拷贝共享的可变状态(如计数器)只能放指针。
func (RegisterOptions) Allow ¶
func (o RegisterOptions) Allow(pkg string, labels map[string]string) bool
Allow 判定某包某 labels 的工具是否应被注册。 基座只额外注入内置 label pkg(包名),其余 label 语义一概不解释。
未经 Compile 时(直接构造 RegisterOptions 的调用方)现场 Parse, 语法错误 fail-closed 返回 false。
func (RegisterOptions) Compile ¶ added in v0.5.0
func (o RegisterOptions) Compile() (RegisterOptions, error)
Compile 预编译 Match 并分配计数器,返回可直接交给 registrar 的副本。 Match 语法错误在此报出——启动期失败远好过「服务起来了但 tools/list 是空的」。 纯空白的 Match 视同未配置(不过滤),否则配置里多打一个空格就会翻转语义。
func (RegisterOptions) Counts ¶ added in v0.5.0
func (o RegisterOptions) Counts() (registered, filtered int)
Counts 返回本轮注册的放行数与被过滤数;未经 Compile 时均为 0。
type Registrar ¶
type Registrar func(s *mcp.Server, opts RegisterOptions)
Registrar 是生成代码暴露的注册函数类型(通常是生成的 tools.RegisterAll)。
type Registry ¶ added in v0.5.0
type Registry struct {
// contains filtered or unexported fields
}
Registry 是插件的唯一依赖类型:注册中间件、MCP 工具、HTTP 路由、清理钩子。
生命周期:New 构造并注册生成的工具 → 各插件 Install(r) → Start/Handlers 组装并运行。 组装之后不应再调用注册类方法(Use/Tool/Route):链与 handler 已定型,改它没效果。
func New ¶ added in v0.5.0
New 构造 Registry 并按 cfg 的包白名单 / label selector 注册生成的工具, 返回值交给各插件的 Install 安装自己。
cfg.Match 在 registrar 之前就被 Parse:语法错误直接记为启动失败并跳过注册, 避免「服务起来了但 tools/list 是空的」这种无痕迹故障。
func NewRegistry ¶ added in v0.5.0
NewRegistry 构造一个空 Registry(不注册任何工具),供测试与自定义组装使用。
func (*Registry) Config ¶ added in v0.5.0
Config 把 ConfigPath 指向的 TOML 解码到 dst(插件自带段落名),并把解码到的键 登记为「已认领」:没人认领的键会在启动时报错,避免段落名或字段名拼错后静默用默认值。 文件未配置或不存在时不改动 dst,插件应自带可用默认值。
插件必须遵守的两条契约。认领制把「配置拼错检查」这项安全属性下沉给了插件, 基座无法代为保证——toml.MetaData.Undecoded() 只能看出「哪些键没被解码」, 看不出「解到哪去了」:
- 禁止用 map 兜底解码自己的段落(map[string]any / map[string]string), 必须用具名字段结构体。用 map 时段内**所有**键都会被判为「已解码」, 于是 `[quota] bakcend=... limmit=...` 这类拼错被完全吞掉,Handlers() 照常 返回 nil error、插件按默认值上线——正是认领制要拦的那类事故。
- 文档里承诺支持的每一个字段都必须在结构体里声明。只声明一部分时, 部署方照文档把配置写全反而启动失败(报「无人认领的配置项 [quota.addr ...]」)。 换言之:结构体的字段集就是该段落的对外契约,不能比文档窄。
func (*Registry) Handlers ¶ added in v0.5.0
Handlers 返回可挂载到既有 HTTP server 的 MCP handler 与插件注册的路由。 与 Start 不同,它不自己监听端口,由宿主决定挂在哪个子路径、共用哪个生命周期。
返回的 routes 里,Route 注册的已套好 token 认证,RoutePublic 注册的是裸 handler。 重复调用返回同一份结果(幂等)。
func (*Registry) Middlewares ¶ added in v0.5.0
func (r *Registry) Middlewares() []Middleware
Middlewares 返回已注册的插件中间件(基座内部与测试使用)。
func (*Registry) OnBuild ¶ added in v0.5.0
OnBuild 注册 build 期校验钩子:在基座自身的校验(loadBase / 配置认领检查 / validate)全部通过之后、组装出 handler 之前执行,任一钩子返回 error 即启动失败。
存在的理由(都是插件在 Install 里做不到的):
- **消除「Install 期校验」的假阳性**:使用方注入的回调(audit 的 sink、confirm 的 notifier)允许在 Install 之后、开始接流之前才注册,Install 里检查「有没有注册」 会把这种合法用法误判成启动失败。
- **把「注册时机晚于接流」的空窗堵在接流前**,而不是只靠运行期兜底: 插件的运行期兜底(deny)仍必须保留,两层不是二选一——OnBuild 只覆盖 「启动之前就能看出来」的那一半,运行期把回调置回 nil 这类情况只有兜底管得住。
- 钩子能看到最终生效的全局状态(如 PublicBaseURL),Install 时它还可能未定型。
语义:按注册顺序执行,首个报错或 panic 即中止(后面的钩子不再跑);随 build 一起 幂等,一次进程生命周期内恰好执行一次;nil 钩子被忽略。钩子 panic 会被 recover 并 转成启动失败(理由见 runOnBuild)。
钩子里**只做校验与日志**:注册类调用(Use / Tool / Route / RoutePublic / Named / Config / OnBuild)在这里一律被拒绝——runOnBuild 前后比对注册表状态,变了就启动失败。 为什么拒绝而不是「文档里劝一句」:这些调用此刻的表现各不相同且**全是静默的** —— 此时链还没装,钩子里 `r.Use` 会真的生效却不会出现在**前一步已经打印**的 `plugin chain` 日志里(部署方从日志看不到那个中间件);`r.Named` 赶不上 required_plugins 校验;`r.Config` 赶不上配置认领检查(多认领的键没人再核对)。 三种形态都属于本项目一贯要消灭的「静默失效」,所以直接报错。 `OnStop` 不在此列:清理钩子在 RunStop 时才用,钩子里登记是真生效的。
func (*Registry) PluginNames ¶ added in v0.5.0
PluginNames 返回已安装插件的名字(**外→内**,即安装顺序)。 与 `plugin chain (outer→inner)` 启动日志同源,供宿主自检链序—— 「confirm 装在 quota 之内还是之外」这类顺序约束只有断言得到名单才守得住。
func (*Registry) RegisterCounts ¶ added in v0.5.0
RegisterCounts 返回本次注册放行与被过滤的工具数(New 之外的构造方式下均为 0)。
func (*Registry) Route ¶ added in v0.5.0
Route 注册插件自带的、**需要鉴权**的 HTTP 路由(如 /spill/<id>)。 基座在挂载时统一套上 token 认证:插件路由与 MCP 端点在同一个进程里, 一个裸奔的 /spill/<id> 就把大结果原文变成了无认证接口。
同一个 pattern 重复注册即启动失败(见 claimRoute)。
func (*Registry) RoutePublic ¶ added in v0.5.0
RoutePublic 注册**不需要鉴权**的 HTTP 路由(健康检查、探活这类)。 与 Route 分成两个方法而不是加一个 bool 参数:每个路由都必须显式表态, 漏写一个 public 只会多一层鉴权,漏写一个 bool 却会少一层。 同一个 pattern 重复注册即启动失败(见 claimRoute)。
func (*Registry) Routes ¶ added in v0.5.0
Routes 返回插件路由,需鉴权的已套上认证层(基座挂载与测试使用)。 必须在配置加载之后调用;未加载时 fail-closed 返回空 map。
每条路由都再套一层 WithPathOwnerRouting:插件路由上的 id 常常是有归属的 (spill 的 /spill/<id>、confirm 的 /confirm/<回执 id>),而资源与挂起的请求只存在于 产出它的那个副本。少了这一层,多副本部署下「回调/下载被负载均衡打到别的副本」 就是一次静默失败,而单副本部署完全看不出来。 顺序与 MCP 端点一致(认证在外、owner 路由在内):反代出去的请求会被属主再认证一次。
func (*Registry) RunStop ¶ added in v0.5.0
RunStop 依次执行清理钩子。**幂等**(sync.Once):清理只该发生一次,而现在它有两个 触发点——正常退出,以及启动失败时基座自己兜的那一次(见 build 的失败路径)。 宿主照旧 defer 一次即可,不必判断「是不是已经清过了」。
跑完即把本 Registry 标记为报废:之后 build/Start 一律拒绝(见 checkStopped)。
单个钩子 panic 不连坐其余钩子(callStopHook 逐个 recover)。这条与幂等是配套的: 幂等意味着「第二次调用不会补跑」,若第一个钩子 panic 就中断,后面的连接池与文件句柄 将永远没人关——一个坏钩子把整条清理链废掉。
func (*Registry) Server ¶ added in v0.5.0
Server 返回底层 MCP server,供插件之外的宿主代码注册手写工具。 注意:这样注册的工具必须另行调用 RegisterTool 登记 labels,否则在 token 准入里 既不可见也不可执行(deny-by-default)。
func (*Registry) Start ¶ added in v0.5.0
Start 校验插件、组装链、挂载路由并阻塞运行 HTTP server,直到 ctx 取消或 server 退出。 退出前执行插件注册的 OnStop 钩子。
func (*Registry) Use ¶ added in v0.5.0
func (r *Registry) Use(mw Middleware)
Use 注册中间件。注册顺序即洋葱进入顺序(先注册的在最外层)。
type Result ¶ added in v0.5.0
type Result struct {
Tool *mcp.CallToolResult
List *mcp.ListToolsResult
// contains filtered or unexported fields
}
Result 是一次调用的结果。tools/call 用 Tool 字段,tools/list 用 List 字段, 其他 MCP 方法用 raw 直通(否则会被吞成 nil)。 读取侧按 Tool → List → raw 取第一个非 nil;三者同时非 nil 属调用方错误,行为未定义。 三者同时为 nil(如插件返回 &Result{})时 unwrapResult 返回 nil 接口,等同于没有结果。
func DenyResult ¶ added in v0.5.0
DenyResult 构造一个错误态结果,并在 Meta 里记录拒绝方与原因供审计读取。 结果按 MCP 约定用 IsError 表达业务级拒绝,而不是协议级错误, 这样模型能看到被拒的原因并自我纠正。
func ListResult ¶ added in v0.5.0
func ListResult(l *mcp.ListToolsResult) *Result
ListResult 构造一个 tools/list 结果,供插件过滤工具清单后返回。
func TextResult ¶ added in v0.5.0
TextResult 构造一个纯文本的 tools/call 结果,供插件短路返回使用。
type Subject ¶ added in v0.5.0
type Subject struct {
ID string // 人(身份头解析所得)
// Token 必须填 token 的**用途名**(配置 [[tokens]].name),绝不是 token 值:
// 填了 token 值既查不到准入规则(全部请求被拒),又会把密文带进日志。
Token string
Labels map[string]string // 主体自身标注,插件自定义
}
Subject 是调用主体。基座只负责填充,不解释 Labels。
func SubjectFromContext ¶ added in v0.5.0
SubjectFromContext 取出调用主体;未注入时返回 nil,调用方按「无身份」处理。
type TokenAuthz ¶ added in v0.5.0
type TokenAuthz struct {
// contains filtered or unexported fields
}
TokenAuthz 按 token 判定工具的可见性与可执行性:判据只有工具 labels (对基座不透明)与配置里的 allow/deny selector。deny 优先,两者都不命中即拒绝。
不变量:NewTokenAuthz 返回后全部字段只读,可并发使用。刻意不提供任何 setter—— ResolveIdentity 与 Middleware 都跑在请求路径上,启动后改字段就是 data race。
按人(identity)的授权不在基座:基座只把身份解析进 Subject.ID, 具体判定由使用方写插件实现。
func NewTokenAuthz ¶ added in v0.5.0
func NewTokenAuthz(cfg TokenAuthzConfig) (*TokenAuthz, error)
NewTokenAuthz 从配置构造 TokenAuthz。缺 token/name/applicant、token 值或 name 重复、 selector 语法错误一律返回 error,调用方应据此让启动失败(fail-fast,而非带着坏规则上线)。
错误信息只用配置里的序号与 name 定位条目,绝不回显 token 值(错误会进日志/终端)。
func (*TokenAuthz) HTTPMiddleware ¶ added in v0.5.0
func (a *TokenAuthz) HTTPMiddleware(next http.Handler) http.Handler
HTTPMiddleware 是基座的 HTTP 认证层,装在 MCP handler 与插件路由之外: 解析 Authorization 里的 token → 查用途名 → 未登记的 token 一律 401(fail-closed), 合法则把 Subject{Token: 用途名, ID: 身份} 注入 ctx,供 newCall 投影进 Call。
只注入不判定:能不能看见/执行某个工具由 Middleware() 在 MCP 层按 labels 判。 401 文案刻意不回显收到的 token,避免密文进日志或返回给调用方。
func (*TokenAuthz) IdentityHeaders ¶ added in v0.5.0
func (a *TokenAuthz) IdentityHeaders() []string
IdentityHeaders 返回取调用人身份的有序请求头名;未配置时回退 [X-MCP-User]。
func (*TokenAuthz) Middleware ¶ added in v0.5.0
func (a *TokenAuthz) Middleware() Middleware
Middleware 返回基座的准入中间件:tools/call 拦截、tools/list 过滤,其余方法透传。 它不是插件,由基座固定装在插件链之前。
不变量(两条路径必须一致,否则「不可见」被误当成访问控制):
- 判据只取自工具注册表(LookupTool + AllLabels),不信任 Call.Labels——链上任何 一环都能改它,而注册表是拷贝出来的权威值。
- 判据在**进入链之前**取一次并留在本地变量里:Call.Subject 是指针,插件在 next 之前改一行就能换掉规则;tools/list 的过滤发生在 next 返回之后,若那时才读 Subject.Token,一个中间件就能把自己提权成可见范围更大的 token。
- 注册表里查不到 labels 的工具(直接注册在 mcp.Server 上、或忘了登记的外部工具) 既不可见也不可执行(deny-by-default);只隐藏不拦截等于没有访问控制。
func (*TokenAuthz) ResolveIdentity ¶ added in v0.5.0
func (a *TokenAuthz) ResolveIdentity(get func(name string) string) string
ResolveIdentity 按配置的头名顺序取第一个非空值。它只是「读头」这一步, 不含信任判定——是否采用这个值由 SubjectOf 按 trust_identity_header 决定。 get 通常为 http.Header.Get。
func (*TokenAuthz) SubjectOf ¶ added in v0.5.0
SubjectOf 按 token 值构造调用主体;token 未配置时 ok=false(HTTP 层据此 401)。
身份(Subject.ID)的来源优先级,默认不信任客户端:
- token 上绑定的 identity = "fixed:<id>":确定值,完全不看请求头;
- trust_identity_header=true 时才读 identity_headers;
- 都没有则为空串——由需要按人判定的插件自己决定怎么处理空身份。
之所以默认取空而不是取头值:Subject.ID 是配额计数键与二次确认归属的唯一判据, 默认信任会让「部署时漏了重写身份头的网关」变成静默的人人可冒充。
type TokenAuthzConfig ¶ added in v0.5.0
type TokenAuthzConfig struct {
Tokens []TokenConfig `toml:"tokens"`
// IdentityHeaders 指定取调用人身份的请求头名(有序):按从前到后的顺序,取第一个
// 在请求中非空的头值作为身份。缺省/为空时用 defaultIdentityHeader(X-MCP-User)。
// 仅在 TrustIdentityHeader 为 true 时生效。
IdentityHeaders []string `toml:"identity_headers"`
// TrustIdentityHeader 决定是否信任客户端送来的身份头。
//
// 默认 false(不信任):身份头是客户端可以随手伪造的,而 Subject.ID 是配额计数键、
// 二次确认归属这类判据的唯一来源。默认信任的话,「部署时漏了那层会重写身份头的
// 可信网关」不会有任何报错,只会静默变成人人可冒充——静默失效比启动失败危险。
// 只有把这个开关显式打开(声明「我前面确实有可信网关」)才启用 IdentityHeaders。
TrustIdentityHeader bool `toml:"trust_identity_header"`
}
TokenAuthzConfig 是 token 准入与身份解析的配置(对应 TOML 里的 [[tokens]]、 identity_headers 与 trust_identity_header),供配置加载器一次性解出后交给 NewTokenAuthz。
type TokenConfig ¶ added in v0.5.0
type TokenConfig struct {
Token string `toml:"token"`
Name string `toml:"name"` // 用途标识,进审计
Applicant string `toml:"applicant"` // 申请人,仅配置内留痕
Allow []string `toml:"allow"` // selector 列表,OR
Deny []string `toml:"deny"` // selector 列表,OR,优先于 allow
// Identity 把这个 token 绑定到一个固定的调用人身份,写法为 "fixed:<id>"。
// 配了它就不看任何请求头(也不受 trust_identity_header 影响):
// 服务账号类 token 的身份本来就是确定的,让请求头能改它等于白送一个冒充入口。
Identity string `toml:"identity"`
}
TokenConfig 是单个 token 的配置(对应 [[tokens]])。
type ToolInfo ¶ added in v0.5.0
ToolInfo 是一个工具的元数据。Labels 对基座不透明:基座只做投影与匹配, 不解释任何 key 的语义(risk / capability 等均由配置与插件解释)。
func LookupTool ¶ added in v0.5.0
LookupTool 查单个工具。返回的 Labels 是拷贝:调用方改它既不会与并发的 LookupTool 撞成 concurrent map read/write,也改不动注册表里的 authz 判据。