runtime

package
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 1, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Overview

Package runtime provides the hand-written runtime substrate for the auto-generated MCP tool registrations under mcp/tools.

Index

Constants

View Source
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

func HTTPHeaders(next http.Handler) http.Handler

HTTPHeaders 是把请求头快照注入 ctx 的 net/http 中间件,装在 MCP handler 之外。 插件读 Call.Headers(如按配置采集审计头)全靠它。

func HTTPLogID

func HTTPLogID(next http.Handler) http.Handler

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

func HeadersFromContext(ctx context.Context) http.Header

HeadersFromContext 取请求头快照;缺失或注入的是 nil Header 时返回非 nil 的空 Header(h.Clone() 对 nil 返回 nil),保证插件既能 Get 也能 Set 而不 panic。

func LogIDFromContext

func LogIDFromContext(ctx context.Context) string

LogIDFromContext 取出 ctx 中的 logid,缺失返回 ""。

func NewOwnedID added in v0.5.0

func NewOwnedID() string

NewOwnedID 生成内嵌本副本 host:port 的不透明 id,供有状态插件使用。 未配置对外地址(未调用 SetPublicBaseURL)时退回纯随机 id,行为与单机一致。

func OwnerOf added in v0.4.0

func OwnerOf(id string) (hostPort string, ok bool)

OwnerOf 解出 id 内嵌的属主 host:port;ok=false 表示无归属 id。

func OwnerRoutedParam added in v0.5.0

func OwnerRoutedParam(toolName string) (paramName string, ok bool)

OwnerRoutedParam 返回某工具声明的 owner 路由参数名;ok=false 表示未声明。 供插件与宿主自检「我的 RegisterOwnerRouted 真的生效了」——漏调它在单副本部署下 完全看不出来,多副本上线才会变成「回执/资源找不到」。

func OwnerRoutedPathRegistered added in v0.5.0

func OwnerRoutedPathRegistered(prefix string) bool

OwnerRoutedPathRegistered 返回某前缀是否声明过路径 owner 路由,供插件与宿主自检 「我的 RegisterOwnerRoutedPath 真的调到了」——漏调它在单副本部署下完全看不出来。

func PeerAllowed added in v0.5.0

func PeerAllowed(hostPort string) bool

PeerAllowed 返回 hostPort 是否为当前已知的兄弟副本之一(仅这些地址允许被反代)。 provider 未注册或返回空时返回 false:默认拒绝一切远端转发,防 SSRF。

func PublicBaseURL added in v0.5.0

func PublicBaseURL() string

PublicBaseURL 返回本副本对外基础地址;未设置时为空串。

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 SetLogIDHeader

func SetLogIDHeader(name string)

SetLogIDHeader 设置 logid 头名;空值归一化为默认值。

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

func WithHeaders(ctx context.Context, h http.Header) context.Context

WithHeaders 把请求头快照注入 ctx,由 HTTP 层(net/http 中间件)在进入 MCP handler 前调用,是 HeadersFromContext / Call.Headers 的唯一数据来源。 必须 Clone:Call.Headers 承诺是快照,若直接持有 r.Header,插件写它就会改到活的 *http.Request,与上游 net/http 中间件共享状态。 Clone 之后剔除凭据类头(见 credentialHeaders)。

func WithLogID

func WithLogID(ctx context.Context, id string) context.Context

WithLogID 把 logid 注入 ctx。

func WithOwnerRouting added in v0.4.0

func WithOwnerRouting(next http.Handler) http.Handler

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

func WithPathOwnerRouting(next http.Handler) http.Handler

WithPathOwnerRouting 只做**路径形态**的 owner 路由,基座用它包裹插件注册的 HTTP 路由 (见 Registry.Routes)。

与 WithOwnerRouting 分开的理由:后者对 POST 会把整个 body 读进内存来解析 JSON-RPC, 而插件路由的 POST body 形状与大小都不由基座决定(上传类路由完全合法), 无条件缓冲它是一次白送的内存放大器。插件路由要跨副本,就把 id 放进路径。

func WithSubject added in v0.5.0

func WithSubject(ctx context.Context, s *Subject) context.Context

WithSubject 把认证阶段解析出的调用主体注入 ctx,是 newCall 填充 Call.Subject 的 唯一数据来源。由 HTTP 层(TokenAuthz.HTTPMiddleware)在进入 MCP handler 前调用。

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 请求的上下文。插件通过它读写调用相关信息。

func (*Call) SetMeta added in v0.5.0

func (c *Call) SetMeta(key string, val any)

SetMeta 往 Meta 写值,Meta 为 nil 时自动初始化。

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

type Handler func(ctx context.Context, c *Call) (*Result, error)

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

type Middleware func(next Handler) Handler

Middleware 是唯一的插件注入点,形状与 net/http 中间件一致。

type PathOwnerExtractor added in v0.5.0

type PathOwnerExtractor func(r *http.Request) string

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

func New(cfg Config, registrar Registrar) *Registry

New 构造 Registry 并按 cfg 的包白名单 / label selector 注册生成的工具, 返回值交给各插件的 Install 安装自己。

cfg.Match 在 registrar 之前就被 Parse:语法错误直接记为启动失败并跳过注册, 避免「服务起来了但 tools/list 是空的」这种无痕迹故障。

func NewRegistry added in v0.5.0

func NewRegistry(cfg Config) *Registry

NewRegistry 构造一个空 Registry(不注册任何工具),供测试与自定义组装使用。

func (*Registry) Config added in v0.5.0

func (r *Registry) Config(dst any) error

Config 把 ConfigPath 指向的 TOML 解码到 dst(插件自带段落名),并把解码到的键 登记为「已认领」:没人认领的键会在启动时报错,避免段落名或字段名拼错后静默用默认值。 文件未配置或不存在时不改动 dst,插件应自带可用默认值。

插件必须遵守的两条契约。认领制把「配置拼错检查」这项安全属性下沉给了插件, 基座无法代为保证——toml.MetaData.Undecoded() 只能看出「哪些键没被解码」, 看不出「解到哪去了」:

  1. 禁止用 map 兜底解码自己的段落(map[string]any / map[string]string), 必须用具名字段结构体。用 map 时段内**所有**键都会被判为「已解码」, 于是 `[quota] bakcend=... limmit=...` 这类拼错被完全吞掉,Handlers() 照常 返回 nil error、插件按默认值上线——正是认领制要拦的那类事故。
  2. 文档里承诺支持的每一个字段都必须在结构体里声明。只声明一部分时, 部署方照文档把配置写全反而启动失败(报「无人认领的配置项 [quota.addr ...]」)。 换言之:结构体的字段集就是该段落的对外契约,不能比文档窄。

func (*Registry) Handlers added in v0.5.0

func (r *Registry) Handlers() (mcpHandler http.Handler, routes map[string]http.Handler, err error)

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) Named added in v0.5.0

func (r *Registry) Named(name string)

Named 声明当前插件名,用于 required_plugins 校验与启动日志。

func (*Registry) OnBuild added in v0.5.0

func (r *Registry) OnBuild(fn func() error)

OnBuild 注册 build 期校验钩子:在基座自身的校验(loadBase / 配置认领检查 / validate)全部通过之后、组装出 handler 之前执行,任一钩子返回 error 即启动失败。

存在的理由(都是插件在 Install 里做不到的):

  1. **消除「Install 期校验」的假阳性**:使用方注入的回调(audit 的 sink、confirm 的 notifier)允许在 Install 之后、开始接流之前才注册,Install 里检查「有没有注册」 会把这种合法用法误判成启动失败。
  2. **把「注册时机晚于接流」的空窗堵在接流前**,而不是只靠运行期兜底: 插件的运行期兜底(deny)仍必须保留,两层不是二选一——OnBuild 只覆盖 「启动之前就能看出来」的那一半,运行期把回调置回 nil 这类情况只有兜底管得住。
  3. 钩子能看到最终生效的全局状态(如 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) OnStop added in v0.5.0

func (r *Registry) OnStop(fn func())

OnStop 注册进程退出前的清理钩子。

func (*Registry) PluginNames added in v0.5.0

func (r *Registry) PluginNames() []string

PluginNames 返回已安装插件的名字(**外→内**,即安装顺序)。 与 `plugin chain (outer→inner)` 启动日志同源,供宿主自检链序—— 「confirm 装在 quota 之内还是之外」这类顺序约束只有断言得到名单才守得住。

func (*Registry) RegisterCounts added in v0.5.0

func (r *Registry) RegisterCounts() (registered, filtered int)

RegisterCounts 返回本次注册放行与被过滤的工具数(New 之外的构造方式下均为 0)。

func (*Registry) Route added in v0.5.0

func (r *Registry) Route(pattern string, h http.Handler)

Route 注册插件自带的、**需要鉴权**的 HTTP 路由(如 /spill/<id>)。 基座在挂载时统一套上 token 认证:插件路由与 MCP 端点在同一个进程里, 一个裸奔的 /spill/<id> 就把大结果原文变成了无认证接口。

同一个 pattern 重复注册即启动失败(见 claimRoute)。

func (*Registry) RoutePublic added in v0.5.0

func (r *Registry) RoutePublic(pattern string, h http.Handler)

RoutePublic 注册**不需要鉴权**的 HTTP 路由(健康检查、探活这类)。 与 Route 分成两个方法而不是加一个 bool 参数:每个路由都必须显式表态, 漏写一个 public 只会多一层鉴权,漏写一个 bool 却会少一层。 同一个 pattern 重复注册即启动失败(见 claimRoute)。

func (*Registry) Routes added in v0.5.0

func (r *Registry) Routes() map[string]http.Handler

Routes 返回插件路由,需鉴权的已套上认证层(基座挂载与测试使用)。 必须在配置加载之后调用;未加载时 fail-closed 返回空 map。

每条路由都再套一层 WithPathOwnerRouting:插件路由上的 id 常常是有归属的 (spill 的 /spill/<id>、confirm 的 /confirm/<回执 id>),而资源与挂起的请求只存在于 产出它的那个副本。少了这一层,多副本部署下「回调/下载被负载均衡打到别的副本」 就是一次静默失败,而单副本部署完全看不出来。 顺序与 MCP 端点一致(认证在外、owner 路由在内):反代出去的请求会被属主再认证一次。

func (*Registry) RunStop added in v0.5.0

func (r *Registry) RunStop(ctx context.Context)

RunStop 依次执行清理钩子。**幂等**(sync.Once):清理只该发生一次,而现在它有两个 触发点——正常退出,以及启动失败时基座自己兜的那一次(见 build 的失败路径)。 宿主照旧 defer 一次即可,不必判断「是不是已经清过了」。

跑完即把本 Registry 标记为报废:之后 build/Start 一律拒绝(见 checkStopped)。

单个钩子 panic 不连坐其余钩子(callStopHook 逐个 recover)。这条与幂等是配套的: 幂等意味着「第二次调用不会补跑」,若第一个钩子 panic 就中断,后面的连接池与文件句柄 将永远没人关——一个坏钩子把整条清理链废掉。

func (*Registry) Server added in v0.5.0

func (r *Registry) Server() *mcp.Server

Server 返回底层 MCP server,供插件之外的宿主代码注册手写工具。 注意:这样注册的工具必须另行调用 RegisterTool 登记 labels,否则在 token 准入里 既不可见也不可执行(deny-by-default)。

func (*Registry) Start added in v0.5.0

func (r *Registry) Start(ctx context.Context) error

Start 校验插件、组装链、挂载路由并阻塞运行 HTTP server,直到 ctx 取消或 server 退出。 退出前执行插件注册的 OnStop 钩子。

func (*Registry) Tool added in v0.5.0

func (r *Registry) Tool(add func(s *mcp.Server))

Tool 注册一个插件自带的 MCP 工具(如 confirm)。

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

func DenyResult(c *Call, by, reason string) *Result

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

func TextResult(text string) *Result

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

func SubjectFromContext(ctx context.Context) *Subject

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

func (a *TokenAuthz) SubjectOf(token string, get func(name string) string) (*Subject, bool)

SubjectOf 按 token 值构造调用主体;token 未配置时 ok=false(HTTP 层据此 401)。

身份(Subject.ID)的来源优先级,默认不信任客户端:

  1. token 上绑定的 identity = "fixed:<id>":确定值,完全不看请求头;
  2. trust_identity_header=true 时才读 identity_headers;
  3. 都没有则为空串——由需要按人判定的插件自己决定怎么处理空身份。

之所以默认取空而不是取头值:Subject.ID 是配额计数键与二次确认归属的唯一判据, 默认信任会让「部署时漏了重写身份头的网关」变成静默的人人可冒充。

func (*TokenAuthz) TokenName added in v0.5.0

func (a *TokenAuthz) TokenName(token string) (name string, ok bool)

TokenName 按 token 值取其用途名;token 未配置时 ok=false(HTTP 层据此 401)。 它只做查表,不含任何「能力/风险上限」语义。

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

type ToolInfo struct {
	Name   string
	Pkg    string
	Labels map[string]string
}

ToolInfo 是一个工具的元数据。Labels 对基座不透明:基座只做投影与匹配, 不解释任何 key 的语义(risk / capability 等均由配置与插件解释)。

func LookupTool added in v0.5.0

func LookupTool(name string) (ToolInfo, bool)

LookupTool 查单个工具。返回的 Labels 是拷贝:调用方改它既不会与并发的 LookupTool 撞成 concurrent map read/write,也改不动注册表里的 authz 判据。

func Tools added in v0.5.0

func Tools() []ToolInfo

Tools 返回全部已注册工具,Labels 同样是拷贝(理由见 LookupTool)。

func (ToolInfo) AllLabels added in v0.5.0

func (t ToolInfo) AllLabels() map[string]string

AllLabels 返回含内置投影 name/pkg 的完整 label 集合,供 selector 匹配。 name/pkg 在拷贝之后覆写:工具自带的同名 label 不得覆盖真实值, 否则可以靠伪造 label 骗过基于 selector 的过滤。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL