关于 Manifold
Manifold 是连接 MCP 客户端与多个外部 MCP 服务、OpenAPI/Swagger REST API 及 A2A Agent 的网关。它可将 API 规范转换为 MCP 工具,并提供路由、工具控制、身份验证和可观测性功能。
主要功能
- 01将 OpenAPI 和 Swagger 规范转为工具
- 02聚合外部 MCP 服务
- 03将 A2A Agent 技能作为工具提供
- 04筛选并重命名工具
- 05支持 OAuth 和后端身份验证
- 06导出遥测数据和审计日志
适用场景
- 01通过一个 MCP 端点接入多个后端
- 02生成并审查 API 工具目录
- 03控制客户端可调用的后端工具
- 04跟踪网关活动和遥测数据
MCP 相关
Manifold 作为 MCP 服务面向客户端,将请求路由到外部 MCP 服务、REST API 和 A2A Agent。它可转发后端工具和提示词,也可透传 MCP Apps 界面。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
docker run -i --rm -e ENCRYPT_KEY -e SERVER_NAME ghcr.io/nonchan7720/manifold:v1.24.1ENCRYPT_KEY密钥Base64-encoded 32-byte AES-256 key used to encrypt stored tokens (generate with: openssl rand -base64 32)SERVER_NAMEName of a backend server defined under mcpServers in config.yaml; forms the MCP endpoint path /mcp/{SERVER_NAME} (not read by the container itself)本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。
ghcr.io/nonchan7720/manifold:v1.24.1Streamable HTTP工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
Manifold
一个接口。多种连接。Manifold。
English | 日本語
Manifold 是一个网关,作为 MCP 服务运行,同时在后端连接多个外部 MCP 服务和符合 OpenAPI / Swagger 规范的 REST API。
为什么叫“Manifold”?
Manifold 这个名字源自发动机的进气歧管。
进气歧管是一种部件,能将空气和燃料从单个入口均匀、高效地分配到多个气缸。我们将这个项目命名为 Manifold,因为它的结构与之相似。
| 发动机歧管 | 本项目 |
|---|---|
| 单个入口 | 来自 MCP 客户端的请求 |
| 分配 / 路由 | 协议转换 / 路由 |
| 通往多个气缸 | 通往多个外部 MCP / REST API |
架构
flowchart TD
client["MCP Client"] --> manifold["Manifold<br/>(this server)"]
manifold --> mcp["External MCP Servers"]
manifold --> rest["OpenAPI / Swagger<br/>REST API Servers"]
manifold --> a2a["A2A Agents"]
功能
- OpenAPI / Swagger → MCP 转换:根据 OpenAPI 3.x / Swagger 2.x 规范自动生成 MCP 工具
- 静态工具目录:在启动网关前检查 OpenAPI 规范将生成的 MCP 工具(
manifold openapi tools),并从已提交、可比较差异的生成文件启动,而非在启动时获取规范(manifold openapi generate、mcpServers.<name>.tools.file) - 破坏性变更检测:使用 oasdiff 将上游规范变更分类为破坏性或非破坏性,并映射到受影响的 MCP 工具(
manifold openapi diff、manifold openapi generate --check) - MCP 后端聚合:透明反向代理至外部 MCP 服务,并转发其工具和提示词
- 工具筛选和重命名:仅公开所需工具,并使用自选名称和描述(
mcpServers.<name>.tools.include/exclude/overrides) - 审计日志:每次工具调用写入一行 JSON(
audit) - 将 A2A Agent 作为 MCP 服务:将 A2A (Agent2Agent) Agent 的 Agent Card 技能作为 MCP 工具公开,可以独立提供服务(
agents),也可以附加到服务(mcpServers.<name>.agents;技能作为<agent>__<skill>工具,与服务自身的工具并列公开);调用方的会话 ID 会作为 A2AcontextId传递,响应上下文则通过_meta.a2a返回 - 内置 OAuth 2.1 服务:支持 PKCE(S256)的授权服务器。下游客户端通过 DCR(RFC 7591)或客户端 ID 元数据文档(CIMD)注册,并可与上游 OAuth 客户端一对一映射
- 可插拔后端身份验证:可选择静态标头(
authValue)/ OAuth 2.0(oauth2)/ API 密钥 Token Exchange(tokenExchange)/ Google 服务账号(googleServiceAccount) - MCP Apps:将 MCP 后端的 MCP Apps(
io.modelcontextprotocol/ui)界面透传给支持它们的宿主——包括工具上的_meta.ui,以及通过resources/list/resources/read提供的ui://资源;对不支持的宿主则会移除这些内容(mcpServers.<name>.apps) - 资源链接:将工具响应中的二进制内容(包括 JSON 响应中
format: binary字段的内容)存储到 S3,并返回下载 URL(资源链接) - 惰性连接(stdio)/ 无状态连接(http):stdio 后端在首次请求时连接(网关启动时无需依赖后端);http 后端每次请求都会建立新连接,调用方之间绝不共享会话
- 可选存储:会话 / 令牌默认在内存中管理,也可使用 Redis 或 SQLite
- 支持 OpenTelemetry:通过 OTLP 导出追踪、指标和日志(指标也支持 Prometheus 风格的拉取)
要求
- 预编译二进制文件或 Docker 镜像无需其他依赖。从源代码构建需要 Go 1.26+
- 可选:使用 Redis 或 SQLite,在重启后保留会话和 OAuth 令牌,或在多个副本间共享。若两者都不使用,Manifold 会将数据保存在内存中
安装
下载二进制文件
从 Releases 下载最新的二进制文件。
从源代码构建
git clone https://github.com/nonchan7720/manifold.git
cd manifold
go build -o manifold .
Docker
docker pull ghcr.io/nonchan7720/manifold:latest
用法
启动网关
# Run the binary
manifold gateway
# Specify a config file explicitly (-c / --config, config name without extension)
manifold gateway -c config
# Run from source
go run main.go gateway
# Docker (working directory is /home/nonroot)
docker run -p 9999:9999 \
-v $(pwd)/config.yaml:/home/nonroot/config.yaml \
ghcr.io/nonchan7720/manifold:latest
Docker Compose(开发)
启动包含 Redis 的开发环境。
docker compose up -d
examples/ 目录中提供了可直接运行的配置示例。
检查并生成 MCP 工具
对于 OpenAPI 模式的服务(已配置 spec 和/或 tools.file),manifold openapi 会显示网关将注册的内容,并可将其写入网关启动时使用的文件,而无需在启动时获取规范。
# Print the tools every OpenAPI-mode server would register (no gateway started)
manifold openapi tools -c config
# One server, with the full inputSchema
manifold openapi tools -c config --server petstore --json
# Write the generated tools file for every server that has tools.file configured
# (errors for a server that has no spec configured)
manifold openapi generate -c config
# CI: fail if the committed file doesn't match the live spec, without writing anything
manifold openapi generate -c config --check
# Show breaking changes between the committed file and the live spec (oasdiff)
manifold openapi diff -c config
openapi tools 输出:
SERVER TOOL OPERATION DESCRIPTION
petstore addpet POST /pet Add a new pet to the store.
petstore getpetbyid GET /pet/{petId} Find pet by ID.
生成的文件(tools.file)采用 YAML 格式,包含可比较差异的 tools 部分,后接解析后的规范:
version: 1
generatedBy: manifold 1.12.0
source:
spec: https://petstore3.swagger.io/api/v3/openapi.json
sha256: "..."
fetchedAt: "2026-09-04T00:00:00Z"
format: openapi3
tools:
- name: getpetbyid
operation: GET /pet/{petId}
description: Find pet by ID.
binaryResponse: false
inputSchema: { ... }
spec: { ... } # openapi3 document, external $refs internalized
二进制字段和响应
multipart/form-data、application/x-www-form-urlencoded 或 application/json 请求体中的 format: binary 属性(对于 Swagger 2,包括 type: file 表单参数,以及 in: body schema 中的 format: binary 字段,包括嵌套对象、数组、$ref 和 allOf)不会作为普通字符串公开。它会变为一个 oneOf,接受字符串(base64 内容或用于获取文件的 URL)或显式指定来源的对象(url / base64 / text / content,以及可选的 filename 和 contentType),并携带 _meta.manifold.file: true,以便客户端将其识别为文件输入。成功响应为二进制的操作(例如 image/png、application/octet-stream)会标记为 binaryResponse: true;运行时,此类响应会作为二进制内容处理,并在配置了 storage 时以资源链接的形式返回(参见 storage)。只有实际 Content-Type 为非文本类型的成功(2xx)响应才会被视为二进制:错误响应、3xx 响应以及文本/JSON/XML/YAML 响应体(包括 +json / +xml 类型)都会原样返回,即使工具设置了 binaryResponse: true。以下示例来自一个包含一个上传操作和一个下载操作的规范:
tools:
- name: uploadfile
operation: POST /files
description: Upload a file
binaryResponse: false
inputSchema:
properties:
file:
_meta:
manifold:
file: true
fileInputHint: 'Provide the file content as a base64-encoded string, or as a URL (e.g. a presigned URL) to download the file from. For explicit control, an object may be passed instead with one of these keys: {url:"..."} ...'
description: File to upload
oneOf:
- description: Base64-encoded file content, or a URL (e.g. a presigned URL) to download the file from.
type: string
- description: Explicit file source; provide exactly one of url/base64/text/content.
properties:
base64: { type: string, description: Base64-encoded file content. }
url: { type: string, description: URL to download the file content from. }
text: { type: string, description: Raw (non-base64-encoded) text file content. }
content: { type: string, description: Legacy auto-detected base64 or URL content. }
filename: { type: string, description: Filename to use for the upload. }
contentType: { type: string, description: MIME content type to use for the upload. }
type: object
label:
_meta: {}
description: ""
type: string
required:
- file
type: object
- name: downloadfile
operation: GET /files/{fileId}/content
description: Download a file
binaryResponse: true
inputSchema:
properties:
fileId:
description: ""
type: string
required:
- fileId
type: object
在 JSON 请求体中,二进制字段的值会以相同方式解析(base64、URL 或显式对象),并以 base64 形式发送给上游 API。反过来,如果 2xx JSON 响应中包含 format: binary 字段(Swagger 2:format: binary 或 type: file;包括嵌套对象和数组;OpenAPI 3.1 使用 contentMediaType 作为内容类型),且已配置 storage,该字段就会上传到存储:JSON 中的 base64 值会替换为其下载 URL,并为每个已上传字段在结果中添加一个资源链接。未配置 storage,或字段值为 null / 非 base64 值时,JSON 会原样返回。
推荐工作流:
- 将
tools.file添加到服务器配置中(参见mcpServers.<name>.tools),然后运行manifold openapi generate -c config。 - 提交生成的文件。文件中的
tools部分使上游规范变更可以像普通 PR 差异一样进行审查。 - 启动网关(
manifold gateway -c config)——它会从文件读取工具,启动时不会通过网络访问spec。 - 上游规范变更后,重新运行
manifold openapi generate -c config并提交更新。如果文件已过期(规范已变更但文件未重新生成),网关启动会失败,并提示你重新生成文件。 - 将
manifold openapi generate -c config --check添加为 CI 步骤,这样,若 PR 更改了上游规范却未重新生成文件,就会在合并前失败。
CI:--check 只检查配置了 tools.file 的服务器——未配置的服务器会被跳过,并在 stderr 中显示提示;--server 可将检查限制为单台服务器。对于每台服务器,它都会根据实时规范重建目录,并与已提交文件进行比较:source.sha256(上游规范的原始字节)、tools 部分,以及嵌入的 spec 部分(网关实际运行所用的内部化文档)——不会比较 generatedBy 和 source.fetchedAt。任何差异都会导致其以非零状态退出,包括规范变更但工具列表未变的情况,因为嵌入的规范也会驱动运行时请求构建。它不会写入任何内容。GitHub Actions 步骤示例:
- name: Check generated OpenAPI tools files are up to date
run: manifold openapi generate -c config --check
嵌入的规范不同时,--check 还会输出该服务器的 oasdiff 破坏性变更摘要,列出每项破坏性变更及其影响的 MCP 工具(非破坏性变更只会计数;完整列表参见下文的 openapi diff)。其退出状态不变——任何偏差都会导致失败:
server "petstore": drift detected (generated/petstore.yaml)
spec changed (sha256 a31896bb… → 3958dc03…)
embedded spec differs from what the live spec produces
compatibility: 2 breaking changes (2 errors, 0 warnings), 2 non-breaking
error GET /pet/{petId} (getpetbyid) new-required-request-parameter: added the new required `query` request parameter `verbose`
error POST /pet/{petId}/uploadImage (uploadfile) api-path-removed-without-deprecation: api path removed without deprecation
+ added: listpets (GET /pet)
- removed: uploadfile (POST /pet/{petId}/uploadImage)
~ changed: getpetbyid (inputSchema)
run "manifold openapi generate" to update
工具注释
每个 OpenAPI / Swagger 工具都会根据其 HTTP 方法携带 MCP 工具注释:
| 方法 | 类型 | readOnlyHint | destructiveHint | idempotentHint |
|---|---|---|---|---|
GET / HEAD / OPTIONS / TRACE | readOnly | true | false | true |
POST / PUT / PATCH(及其他方法) | write | false | false | false |
DELETE | destructive | false | true | false |
操作可以通过 x-mcp.annotations 扩展覆盖这些注释。kind 会替换根据方法推导的预设值,各个提示(readOnlyHint、destructiveHint、idempotentHint、openWorldHint)和 title 则会在此基础上覆盖相应值:
paths:
/pets/search:
post:
operationId: searchPets
x-mcp:
annotations:
kind: readOnly # readOnly | write | destructive
/pets/{petId}:
put:
operationId: replacePet
x-mcp:
annotations:
kind: destructive
idempotentHint: true
title: Replace a pet
如果 kind 或键未知,或者值的类型不正确,则会忽略该操作的整个 x-mcp.annotations,并记录一条指出该操作名称的警告日志;工具会保留根据其方法推导的注释。
破坏性变更检测(openapi diff)
manifold openapi diff 可回答“上游规范变更对我的 MCP 客户端来说安全吗?”。对于配置了 tools.file 的每个服务(其他服务会跳过,并在 stderr 中显示说明),它会使用 oasdiff 的向后兼容性检查,将已提交的生成文件中嵌入的规范(基准)与当前实时规范生成的内容(修订版)进行比较,并报告每项变更的级别——error 和 warning 表示破坏性变更,info 则不表示破坏性变更——以及受其影响的 MCP 工具。它不会写入任何内容。
| 标志 | 默认值 | 说明 |
|---|---|---|
--server | (全部) | 限定为单个服务 |
--fail-on | ERR | 如果有任何变更达到或超过此级别,则以非零状态退出:ERR、WARN 或 INFO。"" 或 NONE 表示变更不会导致失败(加载错误仍会导致失败) |
--format | text | text、json 或 markdown |
--by-tool | false | 按 MCP 工具分组报告,而不是平铺列出变更(见下文) |
# Fail only on errors (default)
manifold openapi diff -c config
# Fail on warnings too, as a Markdown table
manifold openapi diff -c config --fail-on WARN --format markdown
# Report only, never fail on changes
manifold openapi diff -c config --server petstore --format json --fail-on NONE
# Which tool calls break?
manifold openapi diff -c config --by-tool
text 输出:
petstore: 2 breaking changes (2 errors, 0 warnings), 2 non-breaking
error GET /pet/{petId} (getpetbyid) new-required-request-parameter: added the new required `query` request parameter `verbose`
error POST /pet/{petId}/uploadImage (uploadfile) api-path-removed-without-deprecation: api path removed without deprecation
info - api-version-not-bumped: a breaking change was detected but the version is still `1.0.0`
info GET /pet (listpets) endpoint-added: endpoint added
规范未变更的服务会显示 petstore: no API changes。json 输出是一个以服务名称为键的对象,每个对象包含 breaking / errors / warnings / infos 计数,以及一个由 {level, id, operation, message, tool} 组成的 changes 数组(对于 paths 之外的变更,例如 api-version-not-bumped,会省略 operation 和 tool)。markdown 会为每个服务生成一个 ### <server> 章节及对应表格,适合用作 PR 评论或作业摘要。
使用 --by-tool 时,结果会按 MCP 工具分组,回答“哪些工具调用会出问题”。每个受影响的工具都会列出其状态和最高级别,按严重程度从高到低排列;其变更会缩进显示。未受影响的工具会省略,并计入摘要行。无法关联到任何工具的变更(组件、安全性、api-version-not-bumped 等)会最后列在 (spec-wide) 下:
petstore: 3 of 5 tools affected, 2 breaking changes (2 errors, 0 warnings), 2 non-breaking
error changed getpetbyid (GET /pet/{petId})
error new-required-request-parameter: added the new required `query` request parameter `verbose`
error removed uploadfile (POST /pet/{petId}/uploadImage)
error api-path-removed-without-deprecation: api path removed without deprecation
info added listpets (GET /pet)
info endpoint-added: endpoint added
(spec-wide)
info - api-version-not-bumped: a breaking change was detected but the version is still `1.0.0`
工具的状态也会反映生成工具本身的情况(与 generate --check 输出的比较结果相同):removed——不再生成该工具,因此仍在调用它的所有客户端都会出问题;它始终计为 error,即使 oasdiff 未报告任何变更(例如 operationId 重命名);added——新增的工具;changed——oasdiff 报告了其操作变更,或其生成的 inputSchema 发生变化。后一种情况若没有 oasdiff 变更(例如参数描述编辑),则会以 none 级别列出,并附带说明。每个服务的 json 输出将变为 {affected, total, tools: [{name, operation, status, level, note?, changes: [{level, id, message}]}], specWide: [...]};markdown 则会为每个服务生成单个表格,列为 Tool | Operation | Status | Level | Rule | Message,每项变更占一行。--fail-on 仍会根据最高级别进行比较,被移除的工具计为 error。
在重新生成之前于 CI 中运行此命令,以便在 PR 引入上游破坏性变更时,准确标出哪些工具会受影响:
- name: Check upstream OpenAPI changes for breaking changes
run: manifold openapi diff -c config --format markdown >> "$GITHUB_STEP_SUMMARY"
使用 >> 时,该步骤的退出状态仍由 manifold 决定,因此遇到 ERR 级别的变更时作业会失败,同时报告会写入作业摘要。与 generate 一样,Swagger 2.x 规范会被跳过。
配置
在当前目录或 config/ 子目录中放置配置文件(config.yaml)。
配置值支持以 ${VAR} 或 ${VAR:-default} 的形式展开环境变量。
将配置拆分到多个文件(include)
顶层 include 列表会将其他 YAML 文件中的 mcpServers 和 agents 部分合并到配置中,类似于 LiteLLM 的 include 指令:
# config.yaml
include:
- serviceA.yaml # relative to this file (may only contain `mcpServers` / `agents`)
- serviceB.yaml
- services.d/*.yaml # glob patterns are merged in lexical order
gateway:
port: 9998
encryptKey: ${ENCRYPT_KEY}
sqlite:
path: ./tmp/manifold.db
# serviceA.yaml
mcpServers:
notion:
transport: http
url: https://mcp.notion.com/mcp
description: Notion MCP server
# serviceB.yaml
mcpServers:
google:
baseURL: https://www.googleapis.com/calendar/v3
spec: https://example.com/calendar/openapi.yaml
description: Google Calendar API
- 被包含的文件只能包含
mcpServers和agents键;任何其他键(包括嵌套的include)都会导致错误。 - 被包含的文件按列表顺序合并,主配置文件最后合并,因此主配置文件中的值优先。名称不同的服务会合并;如果某个服务在多个文件中定义,其设置也会合并。
- 映射会递归合并;列表和标量值会整体替换。
- 路径相对于主配置文件,并且可以使用
${VAR}展开。文件不存在会导致错误(glob 没有匹配项则不会)。
连接到 MCP 后端
通过 Manifold 暴露外部 MCP 服务。
gateway:
port: 9999
# openssl rand -base64 32
encryptKey: ${ENCRYPT_KEY}
mcpServers:
my-mcp-server:
description: External MCP server
transport: http
url: http://localhost:8080/mcp
sqlite:
path: ./tmp/manifold.db
后端的工具(tools/list / tools/call)和提示词(prompts/list / prompts/get)会原样转发。不声明 prompts 能力的后端会返回空的 prompts/list,而 prompts/get 会返回未知提示词错误。由于提示词可以告知模型如何使用工具,authz 对提示词应用与工具相同的决策(见决策约定);tools.include / exclude / overrides 不适用。后端发出的 notifications/prompts/list_changed 不会转发。
MCP Apps(apps)
MCP 后端(http / stdio)可以提供 MCP Apps:携带 _meta.ui.resourceUri 的工具,其 ui:// 资源(text/html;profile=mcp-app)由宿主呈现。Manifold 会将 resources/list、resources/templates/list 和 resources/read 转发到后端,并在这些端点上声明 resources 能力。
调用方是否支持 MCP Apps 决定了它会收到什么:
- 支持的宿主会原样获取后端的
tools/list,包括_meta.ui。 - 其他宿主获取的
tools/list不含_meta.ui,并且不会获取仅供 UI 调用的工具(_meta.ui.visibility不含"model")。
宿主会在 capabilities.extensions["io.modelcontextprotocol/ui"] 中声明支持情况。Manifold 通过无状态 HTTP 提供 MCP 服务,因此只有当宿主在每个请求中都发送该声明时,Manifold 才能看到它(协议 2026-07-28 及更高版本);较旧的宿主在 initialize 中声明的内容,到它发送下一个请求时就已不可用。设置 apps: true,可在端点上的请求未声明支持情况时,将所有宿主都视为支持 MCP Apps:
mcpServers:
browser:
description: Chrome DevTools with a live view
transport: stdio
command: browser-mcp
apps: true
对于后端,仅当 Manifold 将调用方视为支持 MCP Apps 时,http 后端才会在其 initialize 中获知调用方支持 MCP Apps。stdio 后端由所有调用方共享一个会话,因此始终会被告知支持 MCP Apps;对于不支持 MCP Apps 的调用方,Manifold 会移除 UI。带 UI 的工具仍应返回文本内容,供这些调用方使用。
资源不受工具授权或 tools.include / exclude 限制;只有端点的身份验证适用。
连接到 A2A Agent
通过 Manifold 暴露 A2A(Agent2Agent) Agent。agents 下的每个条目都会像 mcpServers 条目一样通过 /mcp/<name> 提供服务(参见 agents.<name>)。
agents:
translator:
url: https://translator.example.com # Agent Card is fetched from <url>/.well-known/agent-card.json
description: |
Translation agent. Always pass the caller's session id as sessionId.
On _meta.a2a.state = input-required, reply with the same sessionId and the returned taskId.
skills: [translate, summarize] # optional: expose only these skills, in this order
timeout: 30s # optional: timeout of one message/send (default 60s)
oauth2:
clientID: ${TRANSLATOR_CLIENT_ID}
clientSecret: ${TRANSLATOR_CLIENT_SECRET}
authURL: https://auth.example.com/authorize
tokenURL: https://auth.example.com/token
skills用于限制哪些 Agent Card 技能会成为工具。只有列出的技能 ID 会被暴露,并按列出的顺序排列;如果 Agent Card 中没有某个 ID,则会跳过并记录一条警告日志(不影响启动)。未设置skills时,会暴露 Agent Card 中的所有技能。未暴露的技能也无法调用:tools/call会返回与 Agent Card 中不存在该技能时相同的unknown skill错误。- Manifold 会从
url(以及agentCardPath,默认为/.well-known/agent-card.json)解析 Agent Card(v0.3 和 v1.0 格式),并向 Agent Card 声明的端点发送消息——url从不作为消息端点。启动时会获取 Agent Card(失败时只记录警告),并在需要时于首次请求时再次获取,之后会在进程的整个生命周期内缓存。 - 每个暴露的 技能(若未设置
skills,则为 Agent Card 中的所有技能)都会成为一个以技能 ID 命名的 MCP 工具。工具描述由description(操作员给调用 Agent 的指令)以及 Agent Card 中的技能名称、描述、标签和示例组成。/mcp/list?tools=true会列出这些技能。 tools/call参数:sessionId(必需——调用 Agent 的会话 ID,会作为 A2AcontextId转发)、taskId(可选;用于继续任务,例如在input-required之后),以及以下至少一项:message(文本)、data(JSON 对象,作为数据部分发送)或files(每个文件按 OpenAPI 文件输入格式编写——base64 字符串 / URL,或{url|base64|text, filename, contentType}——参见 二进制字段和响应)。消息文本会原样发送;所选技能 ID 会通过消息metadata.skillId传递,因为 A2A 没有按请求选择技能的机制。- 结果:文本和数据部分会成为文本内容(数据部分也会作为
structuredContent返回,且始终是 JSON 对象:单个对象部分原样返回,数组或多个部分返回为{"items": [...]},单个标量仅作为文本返回);文件 URL 会成为资源链接;文件字节会按 OpenAPI 二进制响应处理(上传到storage,配置后返回资源链接,否则内联返回)。_meta.a2a包含protocolVersion、contextId、taskId、state(例如completed、input-required)、messageId和artifacts列表。处于failed/rejected状态的任务会返回isError结果。 - 身份验证(
authValue/oauth2/tokenExchange)、headers和工具授权的工作方式与mcpServers相同;策略输入为server=<name>、service=<service.code, default name>、tool=<skill id>。 - 不使用流式传输(
message/stream)、任务轮询和推送通知;每次调用都是阻塞式message/send。
将 Agent 附加到服务
要将属于某个服务的 Agent 提供给该服务的调用方,请将它们放在对应 mcpServers 条目的 agents 下(除 reverse 外的任何传输方式,包括 OpenAPI)。服务仍通过 /mcp/<name> 提供,并按之前的方式配置;现在,它的 tools/list 会返回服务自身的工具,以及每个 Agent 的每项已公开技能对应的一个工具。
mcpServers:
billing: # a service, configured as before
transport: http
url: https://billing.example.com/mcp
description: Billing service
agents: # A2A agents attached to this service
translator:
url: https://translator.example.com
description: Use for translation.
skills: [translate] # optional
timeout: 30s
reviewer:
url: https://reviewer.example.com
description: Use for review.
- 工具命名为
<agent>__<skill>(双下划线),例如translator__translate。对此类名称调用tools/call,会向该 Agent 的技能发送message/send,与顶层 Agent 的技能工具完全相同;sessionId、taskId、message/data/files以及结果格式也与上文相同。 - 顺序:先列出服务自身的工具,然后按名称顺序列出 Agent;每个 Agent 的技能按卡片中的顺序排列(或按设置了
skills时的顺序排列)。/mcp/list?tools=true返回相同列表。名称不以所附 Agent 的<agent>__开头的tools/call会像之前一样发送给服务。 - 名称冲突:如果所附 Agent 的工具名称与服务自身的某个工具相同(例如,服务有一个工具
translator__translate,而 Agenttranslator有一项技能translate),则以服务的工具为准。该工具只列出一次,tools/call会调用服务,而 Agent 冲突的技能会从列表中移除,并记录一条警告日志。重命名 Agent 即可解决冲突。 - 如果无法获取某个 Agent 的 Agent Card,则会在
tools/list(以及/mcp/list?tools=true)中跳过该 Agent,并记录一条错误日志;服务自身的工具和其他 Agent 仍会返回,且下次请求时会重新获取该卡片。 - 这些 Agent 不支持
oauth2。OAuth 流程由服务端负责:往返处理器转发的是调用方按服务配置的上游令牌,因此 Agent 自身的oauth2客户端设置会被静默忽略。请使用authValue、tokenExchange或headers,或在顶层agents指令下配置 Agent。原因请参阅docs/design/service-agents.md。 - 不支持
transport: reverse(此类服务由反向网关按用户解析)。 - 工具授权会看到服务名称、其服务代码和组合后的工具名称:
server=<server>、service=<the server's service.code>、tool=<agent>__<skill>。因此,同一策略会同时管控服务自身的工具和其 Agent 的技能,tools/list也会据此过滤。
将服务分组(service)
服务通常会公开多个 API 集——MCP 服务、OpenAPI 规范、A2A Agent。service 会将这些 mcpServers / agents 条目归入同一个服务代码,这样工具授权就能授予整个服务,而不必逐一列出每个顶层键:
mcpServers:
billing-api:
description: Billing REST API
baseURL: https://billing.example.com/api
spec: https://billing.example.com/openapi.yaml
service:
code: billing # passed to the policy as input.service
name: Billing # display name for UIs (/mcp/list)
billing-mcp:
transport: http
url: https://billing.example.com/mcp
description: Billing MCP server
service:
code: billing # name omitted: "Billing" from billing-api is used
agents:
billing-assistant:
url: https://billing-agent.example.com
description: Use for billing questions.
service:
code: billing
service.code默认使用条目自身的名称(其顶层键),因此不含service的配置会为每个服务分别建立一个服务。名称需遵循服务名称字符规则(字母数字、_和-)。service.name仅用于显示,默认值为代码。共享同一代码的条目不得设置不同名称;省略此项的条目会采用同一服务组中其他条目设置的名称。- URL 路径(
/mcp/{name})、OAuth 端点以及其他所有以服务名称为键的内容均保持不变;service只会在授权决策中添加input.service,并在/mcp/list条目中添加service。 - 忽略
mcpServers.<name>.agents下 Agent 的service:它的技能是该服务的工具,因此归属于该服务的service。
随后,匹配 <service>/<tool> 而非 <server>/<tool> 的策略(请对照 examples/opa/policy.rego)即可为这三个条目中的所有 billing/* 授权:
allow if {
some group in input.groups
some pattern in data.policies[group].tools
glob.match(pattern, ["/"], sprintf("%s/%s", [input.service, input.tool]))
}
连接到 OpenAPI / Swagger 后端
根据 OpenAPI 规范自动生成 MCP 工具。
gateway:
port: 9999
encryptKey: ${ENCRYPT_KEY}
mcpServers:
my-api:
description: Sample REST API
spec: https://example.com/api/openapi.json
baseURL: https://example.com
使用 OAuth 2.0 身份验证的 OpenAPI 后端
gateway:
port: 9999
encryptKey: ${ENCRYPT_KEY}
mcpServers:
my-api:
description: OAuth-protected API
spec: https://example.com/api/openapi.json
baseURL: https://example.com
oauth2:
clientID: YOUR_CLIENT_ID
clientSecret: YOUR_CLIENT_SECRET
authURL: https://example.com/oauth/authorize
tokenURL: https://example.com/oauth/token
scopes:
- read
- write
redis:
addrs:
- "${REDIS_ADDRS:-localhost:6379}"
db: ${REDIS_DB:-0}
OpenAPI 后端的提示词(prompts)
OpenAPI 规范无法描述 MCP 提示词,因此 OpenAPI 模式的服务可以从单独的提示词文档中提供提示词,该文档与 spec 分开保存。在 mcpServers.<name>.prompts 下恰好设置一个来源:
url:每次调用prompts/list和prompts/get时,都会从此绝对 http(s) URL 获取文档,因此后端无需发布网关即可更改提示词。请求携带与工具调用相同的身份验证信息(authValue/oauth2/tokenExchange)和headers。获取失败时,会向调用方返回错误。file:在启动时读取文档(无法读取或无效的文件会导致启动失败),并在文件发生变化时重新读取。系统会监视目录,因此作为卷挂载的 Kubernetes ConfigMap 即使通过替换其..data符号链接来更新,也能在无需重启的情况下被检测到。若更改后的文档无效,系统会记录日志,并继续提供之前的提示词。
mcpServers:
petstore:
description: Swagger Petstore
spec: https://petstore3.swagger.io/api/v3/openapi.json
prompts:
file: /etc/manifold/prompts/petstore.yaml # or url: https://petstore.example.com/mcp-prompts.yaml
该文档采用 YAML 或 JSON 格式,大小不超过 1 MiB:
prompts:
- name: summarize_pet
title: Summarize a pet # optional
description: Summarize a pet's profile
arguments:
- name: petId
description: ID of the pet
required: true
- name: tone
messages: # role is user or assistant
- role: user
content: "Summarize the pet {{.petId}}.{{if .tone}} Tone: {{.tone}}.{{end}}"
content 是 Go text/template 模板,通过名称引用参数(名称包含 - 时使用 {{index . "pet-id"}})。缺少必需参数时会返回无效参数错误,省略可选参数时会展开为空字符串。authz 对这些提示词应用与工具相同的判定(参见判定契约);tools.include / exclude / overrides 不适用于这些提示词。
选择要公开的工具(tools.include / exclude / overrides)
从 OpenAPI 生成的 API 往往包含远超 Agent 所需数量的工具。tools.include / tools.exclude 使用 path.Match glob 模式,与工具的原始名称匹配;当工具匹配某个 include 模式(或 include 为空)且不匹配任何 exclude 模式时,该工具才会公开。tools.overrides 以原始名称为键,可重命名工具和/或替换其描述。这适用于所有类型的服务(OpenAPI、MCP 后端、通过 agents 独立提供服务的 A2A Agent、WebMCP)。
在 OpenAPI / Swagger 服务上,tools.exclude 的作用更进一步:在构建目录时应用,并同时匹配每个操作的 operationId 以及由其派生的(小写)工具名称,因此 "deletePet" 和 "deletepet" 都有效。对于没有 operationId 的操作,会使用其工具名称所依据的 <method>_<path> ID 进行匹配(例如 get__pets_{id}),该 ID 也就是它的工具名称。匹配的操作不会被转换为工具——它不会出现在 tools/list、manifold openapi tools 或生成的 tools.file 中,openapi diff / specRefresh 也不会有工具可将其变更映射到。规范本身不会被修改,因此仍会检测到排除操作的变更(见下文)。提供工具列表时,include 和 overrides 仍会生效。
mcpServers:
petstore:
description: Swagger Petstore
spec: https://petstore3.swagger.io/api/v3/openapi.json
tools:
include: ["get*", "find*", "addpet"]
exclude: ["*inventory*"]
overrides:
getpetbyid:
name: get_pet
description: Look up a single pet by its numeric ID.
documents: # any key works when `tool` names the original tool explicitly
tool: listDocuments
name: list_documents
- 重命名后的工具只能通过新名称调用。如果新名称与另一个工具的原始名称相同,重命名后的工具优先,另一个工具会被隐藏。
- 过滤在授权之前进行,因此授权机制和
/mcp/list?tools=true只能看到公开的名称。请针对公开的名称编写 OPA 策略。 - 被过滤掉的工具与不存在的工具行为完全相同(
unknown tool)。 - 使用
tools.file时,exclude由manifold openapi generate应用,网关在启动时从文件重建目录也会应用相同的列表。因此,更改exclude会导致已提交的文件过时(generated tools are stale … (run "manifold openapi generate")):请重新生成该文件。 openapi diff或specRefresh在排除的操作上检测到的破坏性变更仍会报告,并计入--fail-on/rejectOn,但不会将其归属于任何工具(使用--by-tool时会归入(spec-wide))。- 在反向(WebMCP)服务上,过滤器只应用于标签页的工具:
create_pairing_code由网关自身注册,始终以该名称公开,因此即使include不匹配它,用户仍可进行配对。同样,mcpServers.<name>.agents的<agent>__<skill>工具永远不会被过滤或重命名:过滤器只应用于服务自身的工具,包括名称恰好以<agent>__开头的服务工具。
审计日志(audit)
audit:
enabled: true
output: /var/log/manifold/audit.jsonl # stdout, stderr (default) or a file path
includeArguments: false # arguments may contain personal or secret data
每次 tools/call 都会写入一行 JSON,与应用日志分开:
{"time":"2026-10-03T05:00:00Z","level":"INFO","msg":"audit","event":"tool_call","server":"petstore","service":"petstore","tool":"get_pet","outcome":"success","duration_ms":42,"user":"alice","groups":"dev","token":"9f86d081884c"}
outcome的值为success、tool_error(工具返回了错误结果)、denied(被授权机制拒绝)或error(未知工具、后端故障等)。对于最后两种情况,error会保存错误消息。- 如果存在
authz.headers.userID/userGroups标头,user/groups就取自这些标头(即使已禁用授权机制)。token是调用方 bearer token 的 SHA-256 哈希值的前 12 个十六进制字符——足以关联调用,同时不会记录该令牌。 identity是反向(WebMCP)服务路由调用时使用的 identityKey(例如,静态配对时为static,远程配对时为解析出的用户)。这些端点会跳过 JWT 验证,因此user/groups/token为空,这是记录的唯一调用方身份。
脱敏密钥(audit.redact)
在记录写入前,会在进程内(不进行外部调用)将记录的 arguments 和 error 中的密钥替换为 [REDACTED]:
- 键名:任何深度的参数键,只要其名称以
password、passwd、secret、token、apikey、authorization、cookie、privatekey、credential、credentials中的任一项结尾,其值就会被脱敏;比较时不区分大小写,并忽略-、_和.(因此githubToken、client_secret和X-Api-Key会匹配,max_tokens则不会)。redact.keys可将名称添加到此列表。 - 模式:
redact.patterns是正则表达式(Go RE2);字符串参数值或错误消息中的每个匹配项都会被脱敏。
audit:
enabled: true
includeArguments: true
redact:
keys: [ssn, cardNumber]
patterns:
- '[\w.+-]+@[\w-]+\.[\w.]+' # e-mail addresses
- 'AKIA[0-9A-Z]{16}' # AWS access key IDs
防护栏(guardrail)
防护栏会检查经过网关的每个 tools/call,方式与 authz 相同:参数在到达后端前(pre_call),以及结果文本在返回调用方前(post_call)。目前没有标准化的防护栏 API,因此 Manifold 使用 LiteLLM 的 Generic Guardrail API:为 LiteLLM 构建的任何防护栏服务都可以直接使用;AWS Bedrock Guardrails 则通过随附的 sidecar(如下所述)使用。
guardrail:
enabled: true
url: http://localhost:8090/beta/litellm_basic_guardrail_api
headers: # optional, e.g. the guardrail's API key
X-Api-Key: ${GUARDRAIL_API_KEY}
timeout: 5s # per call (default 5s)
mode: [pre_call, post_call] # default: both
Manifold 会发送 {"texts": [...], "input_type": "request" | "response", ...},并通过 additional_provider_specific_params 传递 server、service 和 tool:
pre_call会将参数作为一个 JSON 文本发送。若返回GUARDRAIL_INTERVENED,则会使用改写后的参数调用后端(改写后的参数仍必须是 JSON 对象)。post_call会发送结果中的每个text内容;存在structuredContent时,也会将其作为 JSON 发送。若返回GUARDRAIL_INTERVENED,则会返回改写后的内容;其他内容(图像、资源链接等)会未经检查直接通过。BLOCKED会以blocked by guardrail: <blocked_reason>拒绝调用。对于pre_call,不会调用后端。- 失败时拒绝:如果防护栏发生故障、返回非 200 响应或返回无效内容,就会以
guardrail check failed拒绝调用,而不会传递原因(原因会记录到日志中)。
防护栏以同步方式调用,因此其延迟会增加每次 tools/call 的耗时;mode 可将其限制为你需要的一侧。它在 authz 内运行(被拒绝的调用不会到达防护栏),并适用于每个服务,包括反向(WebMCP)服务。审计日志会记录调用方发送的参数(应用 audit.redact 后)。
AWS Bedrock Guardrails sidecar(manifold guardrail bedrock)
manifold guardrail bedrock 提供 Generic Guardrail API,并将每个请求转换为 Bedrock 的 ApplyGuardrail 调用。将其与网关一起运行(使用相同的镜像),并将 guardrail.url 指向它;它不需要 Manifold 配置文件。AWS 凭证和区域会按 AWS SDK 的标准方式解析(环境变量、共享配置、IRSA 等),该角色需要对防护栏拥有 bedrock:ApplyGuardrail 权限。
# Kubernetes: a sidecar container in the gateway Pod
- name: guardrail
image: ghcr.io/nonchan7720/manifold:latest
command: ["manifold", "guardrail", "bedrock", "--port", "8090"]
env:
- {name: BEDROCK_GUARDRAIL_ID, value: abc123xyz} # or --guardrail-id
- {name: BEDROCK_GUARDRAIL_VERSION, value: "1"} # or --guardrail-version (e.g. DRAFT)
- {name: AWS_REGION, value: ap-northeast-1} # or --region
每段文本都会单独发送给 ApplyGuardrail(input_type: request → 来源 INPUT,response → 来源 OUTPUT),因此响应中的 texts 会与请求中的一一对应。若某段文本触发了 ANONYMIZE 敏感信息过滤器,返回内容会被屏蔽(GUARDRAIL_INTERVENED);如果任何策略将其设为 BLOCKED,或 Bedrock 进行了干预但未返回改写内容,整个请求都会被设为 BLOCKED,并附带 Bedrock 的 actionReason。Bedrock 错误会以 502 响应,网关会将其转换为 guardrail check failed。GET /healthz 会对探测请求返回 200。
配置参考
gateway
| 字段 | 类型 | 描述 |
|---|---|---|
port | int | 监听端口(默认值:8081) |
key | string | TLS 私钥文件路径(可选) |
cert | string | TLS 证书文件路径(可选) |
encryptKey | string | 令牌加密密钥。Base64 编码的 32 字节 AES-256 密钥。使用 openssl rand -base64 32 生成。使用 redis 或 sqlite 时必填;使用内存存储时,若未设置,则会在启动时生成随机密钥 |
specRefresh.interval | duration | 重新获取 OpenAPI 模式规范的间隔(例如 5m)。未设置或设为 0 时禁用刷新 |
specRefresh.rejectOn | string | 当刷新后的规范变更达到此级别(ERR、WARN 或 INFO)时拒绝该规范,并继续提供当前工具。未设置、设为 "" 或 NONE 时永不拒绝(默认值)。参见刷新期间的破坏性变更 |
shutdown.drainDelay | duration | 收到 SIGTERM 后,/readyz 返回 503,网关继续提供服务指定时长,然后停止接受连接,以便 Kubernetes 先从 Service 端点中移除该 Pod。如果 preStop 睡眠钩子已执行此操作,请设为 -1s(默认值:5s;负值表示禁用等待) |
shutdown.timeout | duration | 优雅关闭等待正在处理的请求完成的最长时间(默认值:30s) |
timeouts.readHeader | duration | 读取请求标头的最长时间(默认值:10s;负值 = 不限时) |
timeouts.idle | duration | keep-alive 连接的最长空闲时间(默认值:120s;负值 = 不限时) |
timeouts.read | duration | 读取完整请求(包括请求体)的最长时间(默认值:不限时)。请勿设置:MCP Streamable HTTP / SSE 和边缘 WebSocket 都是长连接 |
timeouts.write | duration | 写入响应的最长时间(默认值:不限时)。原因同 timeouts.read,请勿设置 |
maxRequestBodySize | int | /mcp/{server_name} 的请求体最大字节数;超出限制的请求将返回 413(默认值:0 = 不限)。携带 base64 文件内容的工具参数会通过此请求体传输,因此如果使用此类参数,应将其设为至少 fileFetch.maxSize × 4/3(base64 开销),并留出余量 |
gateway.specRefresh
定期重新获取 OpenAPI 模式服务(mcpServers.<name>.spec)的规范,并在不重启 Manifold 的情况下更新 MCP 工具定义。新增的工具会注册,已移除的工具会注销,并通过 notifications/tools/list_changed 通知已连接的客户端。
gateway:
specRefresh:
interval: 5m
通过对获取的规范文档进行哈希来检测变更,因此仅在外部 $ref 引用的文档中发生的变更不会改变哈希,也不会被检测到。如果获取或解析失败,将保留现有工具定义,并在下一个间隔重试。
刷新期间的破坏性变更
当刷新获取的规范与当前提供服务的规范不同时,Manifold 会在切换工具前,使用与 openapi diff 相同的 oasdiff 检查对两者进行差异比较:
- 日志:输出一条摘要日志,包含服务器名称以及
error/warning/info变更的数量;如果存在任何破坏性变更(error或warning),级别为WARN,否则为INFO。此外,每项破坏性变更都会输出一条WARN日志,包含其level、id、operation、受影响的tool和message。 - 指标:每检测到一项变更,OpenTelemetry 计数器
manifold.openapi.spec_refresh.changes(属性server、level=error/warning/info)就递增一次。 - 拒绝:设置
rejectOn后(通过gateway.specRefresh.rejectOn,或按服务器设置mcpServers.<name>.specRefreshRejectOn),如果刷新后的规范中最严重的变更级别达到或超过该级别,则不会采用该规范。系统会继续提供之前的规范和工具服务,记录一条列出导致拒绝的变更的ERROR日志,并递增manifold.openapi.spec_refresh.rejected(属性server、level= 最严重的变更级别)。
gateway:
specRefresh:
interval: 5m
rejectOn: ERR # keep the current tools if upstream introduces an error-level breaking change
拒绝会持续生效,直到上游发布的规范通过与当前仍在提供服务的规范进行的检查;重新获取相同的已拒绝规范时,不会再次进行差异比较、记录日志或计数。重启网关(包括会导致其重启的配置重新加载)时,会采用启动时获取的任何规范,不进行任何检查。检测尽力而为:如果差异比较本身失败,会记录一条警告,并像以前一样采用新规范。如果没有可供比较的规范,则会跳过检查——例如,规范在启动时获取失败后首次成功获取,或规范为 Swagger 2.x。
mcpServers.<name>
服务器名称(<name>)会用于 URL 路径,因此只能包含字母数字字符、_ 和 -。
| Field | Type | Description |
|---|---|---|
description | string | 服务描述(必填;包含在 /mcp/list 响应中) |
service.code | string | 将此条目与其他条目归为一组的服务代码(默认为服务名称)。作为 input.service 传递给 authz(参见将服务器归入服务) |
service.name | string | 在 UI 中显示的服务名称(默认为服务代码)。由 /mcp/list 返回 |
transport | string | MCP 后端的传输方式(http 或 stdio) |
url | string | HTTP 传输方式的端点 |
command | string | stdio 传输方式的命令 |
args | []string | stdio 命令的参数 |
env | map[string]string | stdio 进程的环境变量 |
spec | string | OpenAPI/Swagger 规范的路径、URL 或 configmap://<namespace>/<name>/<key> 引用。除非设置了 tools.file,否则 OpenAPI 模式下必填;设置后网关不会读取此项,但 manifold openapi generate、--check 和 openapi tools --from-spec 仍需要它 |
baseURL | string | OpenAPI 模式的 API 基础 URL。设置 spec 后,默认为规范中第一个 servers 条目(相对 URL 会根据规范 URL 解析);如果由此得出的 URL 不是绝对 http(s) URL,则配置验证(以及网关启动)会失败,因此当本地规范文件没有 servers 或仅包含 /api/v1 等相对 URL 时,请显式设置 baseURL(1.19 中的行为变更)。仅设置 tools.file 时必填 |
headers | map[string]string | 添加到 API 请求中的额外标头 |
authValue | object | 静态身份验证设置(header、prefix、value) |
oauth2 | object | OAuth 2.0 设置(见下文) |
tokenExchange | object | Token Exchange 设置(见下文) |
googleServiceAccount | object | Google 服务账号设置,仅适用于 OpenAPI 模式和 http 传输方式(见下文) |
auth.jwt | object | 在网关验证客户端的 Bearer JWT(见下文) |
specRefreshInterval | duration | 覆盖此服务器的 gateway.specRefresh.interval。0 会禁用此服务器的刷新 |
specRefreshRejectOn | string | 覆盖此服务器的 gateway.specRefresh.rejectOn(ERR、WARN、INFO)。NONE(或 "")表示此服务器永不拒绝 |
tools.file | string | 生成的工具文件路径(参见 mcpServers.<name>.tools)。设置后,网关会从此文件启动,而不是获取 spec |
tools.include / tools.exclude | []string | 用于选择要公开的工具的 Glob 模式。在 OpenAPI / Swagger 服务器上,exclude 也会匹配 operationId,并将匹配的操作完全排除在目录之外(参见选择要公开的工具) |
tools.overrides | map[string]object | 针对每个工具(原始名称)的设置:name、description,以及用于显式指定原始工具名称的 tool。键会保留配置文件中的大小写,因此 getPetById: 会匹配工具 getPetById(仅大小写不同的键会被拒绝) |
agents | map[string]object | 附加到此服务的 A2A Agent;其技能会以 <agent>__<skill> 的形式添加到该服务的工具中。不适用于 transport: reverse(参见 mcpServers.<name>.agents.<agent>) |
apps | bool | 将每个请求中未声明支持 MCP Apps 的主机视为支持该功能。仅适用于 http / stdio(参见 MCP Apps) |
prompts.url / prompts.file | string | OpenAPI 模式服务器的提示词文档来源;必须且只能设置其中一项(参见 OpenAPI 后端的提示词) |
authValue / oauth2 / tokenExchange / googleServiceAccount 互斥;每次只能配置其中一项。auth.jwt 与它们相互独立:它控制客户端到网关这一侧,其余配置控制网关到后端这一侧。 |
来自 ConfigMap 的 spec
spec: configmap://<namespace>/<name>/<key> 会通过集群内 Kubernetes API(client-go、集群内配置——无需单独设置 kubeconfig)从 Kubernetes ConfigMap 的 data[<key>] 读取 spec。网关的 ServiceAccount 需要对此 ConfigMap 拥有 get RBAC 权限:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: my-namespace
name: manifold-spec-reader
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["my-specs"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: my-namespace
name: manifold-spec-reader
subjects:
- kind: ServiceAccount
name: manifold
roleRef:
kind: Role
name: manifold-spec-reader
apiGroup: rbac.authorization.k8s.io
如果启动时获取或解析 spec 失败——无论它是文件、URL,还是 configmap:// 引用——受影响的服务仍会启动,但不包含任何工具,并记录一条警告日志,其中注明服务名称和错误,而不是导致整个网关启动失败。specRefreshInterval / gateway.specRefresh.interval 会按常规计划持续重试,待 spec 可以获取后,工具就会出现。此规则不适用于 tools.file:过期或无法读取的已生成工具文件仍会导致启动失败(见下文)。
mcpServers.<name>.tools
tools.file 指向一个已生成的工具文件(由 manifold openapi generate 写入,参见检查并生成 MCP 工具)。设置后,网关在启动时或 specRefresh 期间都不会获取 spec,而是直接从文件加载工具和(已解析的)spec,不会访问网络。
mcpServers:
petstore:
description: Swagger Petstore
spec: https://petstore3.swagger.io/api/v3/openapi.json # optional here — needed only for generate/--check/--from-spec
baseURL: https://petstore3.swagger.io/api/v3
tools:
file: ./generated/petstore.yaml
- 仍然必须设置
baseURL。设置tools.file后,spec为可选项——网关不会读取它,但manifold openapi generate(以及--check)需要它来重新生成文件,因此如果要使用这些命令,请在配置中保留它。 - 设置
tools.file后,manifold openapi tools会读取已生成的文件。--from-spec则会读取实时 spec;如果未配置spec,就会报错。 - 启动时,Manifold 会根据文件中嵌入的 spec 重建工具目录,并将其与文件中的
tools部分进行比较。如果两者不匹配(文件相对于自身嵌入的 spec 已过期,或曾被手动编辑),启动就会失败,例如:server "petstore": generated tools are stale: tool "addpet" description differs (run "manifold openapi generate")。 - 生成文件时会应用
tools.exclude,网关根据文件重建目录时也会再次应用,因此更改此配置也会使文件过期——编辑列表后请重新生成。 tools.file不能与正数值的specRefreshInterval同时使用;设置了tools.file的服务也不会纳入gateway.specRefresh——因为没有可供刷新的实时 spec。tools.file必须是本地路径;不接受 URL。- 第一阶段仅支持 OpenAPI 3.x 规范。
tools.file不能与 Swagger 2.xspec一起使用。 - 生成的文件会嵌入完整的已解析 spec,包括其中包含的任何内部主机名或示例值。提交到公开仓库前,请先检查文件。
mcpServers.<name>.oauth2
| 字段 | 类型 | 说明 |
|---|---|---|
clientID | string | 共享上游客户端的客户端 ID(当有效的 unknownClient 为 default 时必填,见下文) |
clientSecret | string | 共享上游客户端的客户端密钥(要求与 clientID 相同) |
authURL | string | 授权端点(必填;绝对 URL) |
tokenURL | string | 令牌端点(必填;绝对 URL) |
scopes | []string | 要请求的作用域 |
clients | []object | 将下游 client_id 映射到为其使用的上游客户端(参见下游客户端注册) |
unknownClient | string | 如何处理 clients 中不存在的下游客户端:reject 或 default |
authParams | map[string]string | 添加到上游授权请求中的额外查询参数 |
每个 clients 条目包含 downstreamClientID、clientID 和 clientSecret。系统会将 downstreamClientID 与下游 client_id 进行精确比对,不做任何规范化处理,且不得重复。authParams 不得设置 Manifold 自行构建的参数(client_id、redirect_uri、response_type、scope、state、code_challenge、code_challenge_method)。
如果省略 unknownClient,当 clients 非空时其值为 reject,当 clients 为空时其值为 default,因此不配置 clients 时会保持原有行为。只有当有效值为 default 时,才必须提供共享 clientID / clientSecret——包括显式写入 unknownClient: default、同时在 clients 中映射每个客户端的情况。此时若缺少共享客户端,服务将在启动时失败。
unknownClient | clients | 共享 clientID / clientSecret |
|---|---|---|
default(显式) | 任意 | 必填 |
reject(显式) | 任意 | 非必填 |
| 省略 | 非空 | 非必填(有效值为 reject) |
| 省略 | 空 | 必填(有效值为 default) |
也可以通过单个环境变量以 JSON 数组的形式整体提供 clients,并以 JSON 对象的形式提供 authParams:
mcpServers:
my-api:
oauth2:
clients: ${UPSTREAM_CLIENTS_JSON}
注意 配置文件中的
authParams参数名称会被配置加载器转换为小写,因此请使用小写名称(OAuth 2.0 和 OpenID Connect 定义的名称即为小写)。若要保持名称大小写完全不变,请通过环境变量以 JSON 的形式提供整个映射。
mcpServers.<name>.tokenExchange
将从客户端收到的 API 密钥兑换为指定令牌兑换端点提供的 OAuth 令牌,并将其用于后端请求。兑换结果会被缓存,并遵守速率限制(429)。
| 字段 | 类型 | 说明 |
|---|---|---|
url | string | 令牌兑换端点的绝对 URL(必填) |
mcpServers.<name>.googleServiceAccount
使用 Google 访问令牌验证发往后端的请求:令牌可以来自服务账号密钥(网关使用该密钥签署 JWT,并在密钥的 token_uri 处兑换令牌),也可以在未配置密钥时来自应用默认凭据(ADC)。令牌会缓存至过期,并通过 Authorization: Bearer <token> 发送。不会转发调用方自己的令牌。适用于 OpenAPI 模式和 http 传输(不适用于 stdio、reverse 或 A2A Agent)。这是第一个云服务提供商选项;后续选项会作为同级键添加。
mcpServers:
ga4:
description: Google Analytics Data API
spec: https://raw.githubusercontent.com/googleapis/google-api-go-client/main/analyticsdata/v1beta/analyticsdata-api.json
baseURL: https://analyticsdata.googleapis.com
googleServiceAccount:
credentialsJSON: ${GA4_SERVICE_ACCOUNT_JSON} # or credentialsFile: /path/to/key.json
scopes:
- https://www.googleapis.com/auth/analytics.readonly
| 字段 | 类型 | 描述 |
|---|---|---|
credentialsJSON | string | 服务账号密钥(JSON)本身,例如来自环境变量。不能与 credentialsFile 同时设置;如果两者均未设置,则使用 ADC |
credentialsFile | string | 服务账号密钥(JSON 文件)的路径 |
scopes | []string | 要申请的 OAuth 2.0 作用域(必填) |
加载配置时会解析显式提供的密钥,因此密钥格式错误或 credentialsFile 无法读取时会导致启动失败,而不是等到首次请求时才失败。
应用默认凭据(推荐用于 GKE / GCE / Cloud Run)。 不设置 credentialsJSON 和 credentialsFile,只设置 scopes;网关会像 Google 客户端库一样解析凭据:依次尝试 GOOGLE_APPLICATION_CREDENTIALS(服务账号密钥或工作负载身份联合配置文件)、gcloud 用户凭据,然后是 GCE/GKE 元数据服务器(包括 GKE Workload Identity)。只要运行环境能提供身份,就优先使用此方式,而非长期有效的密钥:无需存储或轮换密钥。
mcpServers:
ga4:
baseURL: https://analyticsdata.googleapis.com
spec: https://raw.githubusercontent.com/googleapis/google-api-go-client/main/analyticsdata/v1beta/analyticsdata-api.json
googleServiceAccount:
scopes:
- https://www.googleapis.com/auth/analytics.readonly # credentials come from ADC / Workload Identity
ADC 会在首次请求时延迟解析,而不是在加载配置时解析(启动时不会访问元数据服务器或网络),因此缺少凭据时会显示为请求错误。
令牌只会发送至配置的后端主机(baseURL,或 http 传输的 url;也允许使用 prompts.url),且仅通过 https 发送(仅回环地址接受纯 http;配置非回环地址的 http:// URL 会被拒绝)。发往任何其他主机(例如重定向目标)的请求都会在不带 Authorization 标头的情况下转发。
mcpServers.<name>.auth.jwt
默认情况下,网关只会检查发往 /mcp/<name> 的请求是否携带 Bearer 令牌,并将其传递给后端进行判断。配置 auth.jwt 后,网关会在转发前验证令牌,使用与 identities(source: jwt)相同的验证器:根据 JWKS 验证签名,以及验证 iss、aud 和 exp(没有 exp 的令牌会被拒绝)。验证失败的令牌会收到 401 响应,并附带常见的 WWW-Authenticate: Bearer resource_metadata=... 挑战。未配置 auth.jwt 的服务仍会按原样传递令牌。JWKS 会在启动时获取(无法访问 jwksURL 会导致启动失败),并由验证器缓存和刷新。transport: reverse 服务通过 identity 验证身份,并会拒绝 auth。
mcpServers:
ga4:
auth:
jwt:
issuer: https://issuer.example.com
jwksURL: https://issuer.example.com/keys
audience: manifold
| 字段 | 类型 | 描述 |
|---|---|---|
issuer | string | 预期的 iss(绝对 URL,必填) |
jwksURL | string | 用于验证签名的 JWKS URL(绝对 URL,必填) |
audience | string | 预期的 aud(必填,因此不会接受为其他资源签发的令牌) |
agents.<name>
A2A Agent(参见连接到 A2A Agent)。名称与 mcpServers 共用命名空间,并遵循相同的字符规则;两者使用相同名称属于配置错误。
| 字段 | 类型 | 描述 |
|---|---|---|
description | string | 调用 Agent 的说明(必填)。会添加到每个技能的工具描述前,并由 /mcp/list 返回 |
service.code | string | 服务代码,与 mcpServers.<name> 相同(默认值:Agent 名称) |
service.name | string | 服务显示名称,与 mcpServers.<name> 相同(默认值:服务代码) |
url | string | 获取 Agent Card 的基础 URL(必填)。消息会发送到 Card 中声明的端点 |
agentCardPath | string | 相对于 url 的 Agent Card 路径(默认值 /.well-known/agent-card.json) |
headers | map[string]string | 添加到 Agent Card 和消息请求中的额外标头 |
authValue | object | 静态身份验证设置(header、prefix、value) |
oauth2 | object | OAuth 2.0 设置(与 mcpServers.<name>.oauth2 相同)。只有消息请求会携带调用方的令牌;Agent Card 仅通过 headers / authValue 获取 |
tokenExchange | object | 令牌交换设置(与 mcpServers.<name>.tokenExchange 相同) |
skills | []string | 要作为工具公开的 Agent Card 技能 ID,按此顺序排列;Card 中缺失的 ID 会跳过并显示警告。不设置则公开所有技能 |
timeout | duration | 单次 message/send 的超时时间(默认值 60s) |
authValue / oauth2 / tokenExchange 三者互斥。
mcpServers.<name>.agents.<agent>
附加到服务的 A2A Agent(参见将 Agent 附加到服务)。<agent> 遵循服务名称的字符规则(字母数字字符、_ 和 -),且不得包含 __,因为工具名称使用它来分隔 Agent 和技能(<agent>__<skill>)。Agent 名称的作用域限定于服务,因此可以在不同服务间重复,也可以与顶层名称相同。
| Field | Type | Description |
|---|---|---|
description | string | 调用 Agent 的说明(必填)。会添加到每个技能的工具说明前,并由 /mcp/list 返回 |
url | string | 解析 Agent Card 的基础 URL(必填)。消息会发送到卡片中声明的端点 |
agentCardPath | string | 相对于 url 的 Agent Card 路径(默认值 /.well-known/agent-card.json) |
headers | map[string]string | 添加到 Agent Card 和消息请求中的额外标头 |
authValue | object | 静态身份验证设置(header、prefix、value) |
tokenExchange | object | Token Exchange 设置(与 mcpServers.<name>.tokenExchange 相同) |
skills | []string | 要作为工具公开的 Agent Card 技能 ID,按此顺序排列;卡片中不存在的 ID 会跳过并发出警告。未设置时会公开所有技能 |
timeout | duration | 单次 message/send 的超时时间(默认值 60s) |
这与 agents.<name> 相同,但不包含 oauth2,此处不接受该字段:OAuth 流程按服务配置,因此请使用 authValue、tokenExchange 或 headers,或者在顶层配置 agents 条目。此处会忽略 service,因为 Agent 的技能属于该服务。authValue 和 tokenExchange 不能同时使用。
oauth.cimd
接受下游客户端提供的 HTTPS client_id,该 ID 会解析为客户端 ID 元数据文档;无需通过 DCR 注册(参见下游客户端注册)。默认禁用。
| Field | Type | Description |
|---|---|---|
enabled | bool | 启用 CIMD 客户端注册(默认值:false) |
allowedOrigins | []string | 非空时,只接受来自这些源的 client_id URL。在获取文档前应用此限制 |
cacheTTL | duration | 已解析客户端的缓存时长上限(默认值:1h)。若 Cache-Control: max-age 设置的时长更短,则以其为准 |
maxDocumentSize | int | 从文档读取的最大字节数(默认值:65536) |
redis
| Field | Type | Description |
|---|---|---|
url | string | Redis URL(例如 redis://user:pass@localhost:6379/0) |
addrs | []string | 主机:端口对列表(用于 Cluster/Sentinel) |
user | string | 用户名 |
password | string | 密码 |
db | int | 数据库编号 |
master_name | string | Sentinel 主节点名称 |
tls | bool | 启用 TLS |
cluster_mode | bool | 启用 Cluster 模式 |
key_prefix | string | 添加到存储在 Redis 中的每个键前面的前缀(默认:空) |
sqlite
| Field | Type | Description |
|---|---|---|
path | string | 数据库文件路径(内存数据库使用 :memory:) |
memory
| Field | Type | Description |
|---|---|---|
enabled | bool | 即使配置了 redis,也将会话和令牌保留在进程内存中 |
存储的选择顺序为:sqlite.path → memory.enabled → redis → 内存。内存存储会在重启时丢失会话和 OAuth 令牌,且不会在副本之间共享;生产环境请使用 redis 或 sqlite。
storage
将 OpenAPI/Swagger 工具响应中包含的内容(图像、二进制数据等)存储到外部存储,并返回资源链接(下载 URL)。JSON 响应中的 format: binary 字段也会上传,并替换为其下载 URL(参见二进制字段和响应)。未设置时,不使用存储。
| Field | Type | Description |
|---|---|---|
type | string | 存储类型。目前仅支持 s3 |
hostURL | string | 下载 URL 的主机(设置后,内容通过 Manifold 的 /media/download/{id} 提供) |
s3.bucket | string | S3 存储桶名称(type: s3 时必填) |
s3.keyPrefix | string | S3 对象键前缀(type: s3 时必填) |
storage:
type: s3
hostURL: https://manifold.example.com
s3:
bucket: my-bucket
keyPrefix: manifold/media
audit
| Field | Type | Description |
|---|---|---|
enabled | bool | 为每次 tools/call 写入一行 JSON(参见审计日志) |
output | string | stdout、stderr(默认)或文件路径(追加写入) |
includeArguments | bool | 同时记录调用参数(默认:false) |
redact.keys | []string | 除默认值之外,值将被隐藏的参数键名称(参见隐藏敏感信息) |
redact.patterns | []string | 从字符串参数值和错误消息中隐藏的正则表达式 |
guardrail
| Field | Type | Description |
|---|---|---|
enabled | bool | 使用 guardrail 检查每次 tools/call(参见Guardrail) |
url | string | 通用 Guardrail API 端点(启用时必填;绝对 http(s) URL) |
headers | map[string]string | 添加到每个 guardrail 请求中的标头 |
timeout | duration | 单次 guardrail 调用的超时时间(默认 5s) |
mode | []string | pre_call 和/或 post_call(默认:两者均启用) |
fileFetch
当 URL 传入 OpenAPI/Swagger 工具的文件输入字段(或 A2A 的 files[] 条目)时,Manifold 会代替调用方从该 URL 下载文件。作为 SSRF 防护措施,下载会经过三层检查:
- 协议:仅允许
https://,除非设置了allowLocal。 - 目标 IP(检查每个连接解析出的地址,因此解析到内部地址的 DNS 名称也会受到检查):
- 始终拒绝回环、链路本地地址(包括
169.254.169.254等云元数据端点)、未指定地址、0.0.0.0/8、多播地址和广播地址; - 默认拒绝私有地址段(RFC 1918、
100.64.0.0/10、fc00::/7),但设置环境变量SKIP_SECURE_CLIENT=true后允许。官方 Docker 镜像会设置该变量,因为在 Kubernetes 中,签名 URL 通常指向集群内服务(ClusterIP),而这些地址段实际上无法逐一枚举; - 无论
SKIP_SECURE_CLIENT的设置如何,都会拒绝deniedCIDRs中的地址。
- 始终拒绝回环、链路本地地址(包括
- 主机:
allowedHosts(允许列表)和deniedHosts(拒绝列表)会在请求发出前以及每次重定向时进行检查。
SKIP_SECURE_CLIENT 也会跳过对运维人员配置的上游 URL(OAuth 令牌/发现端点)的所有 IP 检查。它不会影响 CIMD client_id URL,这些 URL 仍会完整阻止访问私有地址段。
| 字段 | 类型 | 说明 |
|---|---|---|
allowLocal | bool | 允许连接到任何 IP(包括回环地址)以及 http://(用于测试本地堆栈;默认值:false) |
allowedHosts | []string | 主机允许列表(主机名或 host:port)。为空时允许所有主机(但仍会进行 IP 检查) |
deniedHosts | []string | 主机拒绝列表(主机名或 host:port),在 allowedHosts 之后应用,并在每次重定向时检查 |
deniedCIDRs | []string | 下载器绝不可连接的 CIDR 地址段,即使设置了 SKIP_SECURE_CLIENT=true 也不例外(例如内部管理网络) |
maxSize | int64 | 下载内容(文件/base64/文本)的最大字节数。设置为 0 或未设置时,默认为 524288000(500 MiB) |
每个字段也可以通过环境变量覆盖(FILEFETCH_MAXSIZE、FILEFETCH_ALLOWLOCAL、FILEFETCH_ALLOWEDHOSTS、FILEFETCH_DENIEDHOSTS、FILEFETCH_DENIEDCIDRS)。
fileFetch:
allowLocal: false
maxSize: 524288000 # 500MiB
# allowedHosts:
# - example.com
# - files.example.com:8443
# deniedHosts:
# - vault.internal
# deniedCIDRs:
# - 10.100.0.0/16
telemetry
通过 OpenTelemetry 输出跟踪、指标和日志的设置。
| Field | Type | Description |
|---|---|---|
serviceName | string | 服务名称 |
environment | string | 环境名称(deployment.environment 属性) |
gzipCompression | bool | OTLP 导出的 Gzip 压缩 |
trace | object | 跟踪设置(enabled、http、grpc) |
metrics | object | 指标设置(enabled、exporterType:push / pull、http、grpc) |
logs | object | 日志设置(enabled、http、grpc) |
对于 http / grpc 导出器,请指定 addr(主机:端口)或 url,并可选地提供一个 headers 映射来设置额外的请求标头(例如,需要 Authorization 标头的 SaaS OTLP 端点)。grpc 还接受 insecure。设置 metrics.exporterType: pull 时,Prometheus 格式的指标会通过 /metrics 端点公开,而不是通过 OTLP 推送。
也可以通过一个包含 JSON 对象的环境变量提供 headers,而不是使用嵌套的 YAML 映射——当值(例如 bearer 令牌)是在部署时注入,而非提交到 config.yaml 中时,这种方式很有用:
telemetry:
trace:
http:
url: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT}
headers: ${OTEL_EXPORTER_OTLP_HEADERS_JSON}
export OTEL_EXPORTER_OTLP_HEADERS_JSON='{"Authorization":"Basic xxxxx"}'
telemetry:
serviceName: manifold
trace:
enabled: true
grpc:
addr: localhost:4317
insecure: true
metrics:
enabled: true
exporterType: push
grpc:
addr: localhost:4317
insecure: true
logs:
enabled: true
grpc:
addr: localhost:4317
insecure: true
用于自动扩缩的指标
除了 Go 运行时指标和 otelhttp 请求指标(http_server_request_duration_seconds_*)外,网关还会输出以下指标(名称与 Prometheus /metrics 端点中显示的一致;OTLP 使用带点的名称):
| Metric | Type | Description |
|---|---|---|
http_server_active_requests | UpDownCounter | 正在处理的 HTTP 请求数(http.server.active_requests)。SSE / WebSocket 长连接在打开期间会计入;/healthz、/readyz 和 /metrics 除外 |
manifold_tool_calls_total | counter | tools/call 请求,标签为 server、tool、status(ok / error) |
manifold_tool_call_duration_seconds | histogram | tools/call 时长,使用相同标签 |
网关受 I/O 限制:大部分时间都花在等待后端上,因此并发量上升时,CPU 和内存占用仍然较低,基于 CPU / 内存的 HPA 扩缩容会滞后于实际负载。应改为基于请求速率或正在处理的请求数进行扩缩容。例如,使用 KEDA 及其 Prometheus 触发器:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: manifold
spec:
scaleTargetRef:
name: manifold
minReplicaCount: 2
maxReplicaCount: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
# average in-flight requests per pod
query: sum(http_server_active_requests{job="manifold"})
threshold: "50"
- type: prometheus
metadata:
serverAddress: http://prometheus.monitoring.svc:9090
# request rate across all pods
query: sum(rate(http_server_request_duration_seconds_count{job="manifold"}[2m]))
threshold: "100"
KEDA 会将 threshold 视为每个副本的目标值,因此所需的副本数等于查询结果除以该值。请根据你的抓取配置调整标签选择器(job),并参阅下文“在 Kubernetes 上运行 / 多个副本”,了解多个副本所需的条件。
在 Kubernetes 上运行/运行多个副本
运行多个副本(或执行滚动更新且不丢失请求)的检查清单:
- 使用 Redis。
memory和sqlite存储按进程隔离,因此会话、OAuth 状态和令牌不会在 Pod 之间共享。它们仅适用于单个 Pod。网关使用内存存储启动时会记录警告。 - 设置
gateway.encryptKey,并在所有 Pod 之间共享该密钥(例如,从 Secret 中获取)。如果未设置,使用内存存储的每个 Pod 都会生成自己的随机密钥,一个 Pod 签发的令牌无法被另一个 Pod 读取。使用redis时必须设置该密钥。 - WebMCP 反向网关(
transport: reverse)仅支持单副本。 浏览器扩展的 WebSocket 连接和路由到该网关的/mcp调用必须落到同一进程上;请将其作为独立的单副本 Deployment 运行。 - 探针:存活探针使用
/healthz,就绪探针使用/readyz。一旦开始关闭,/readyz就会返回503。 - 优雅关闭:使用原生的
lifecycle.preStop.sleep钩子(镜像采用 distroless,因此运行sleep的exec钩子不起作用)。无论 Pod 是否就绪,Kubernetes 都会将正在终止的 Pod 从 Service 端点中移除,并且只有在preStop返回后才会发送 SIGTERM,因此休眠可以给端点留出停止向该 Pod 路由请求的时间。然后,将网关自身的等待时间设为gateway.shutdown.drainDelay: -1s;收到 SIGTERM 后,网关会等待最多gateway.shutdown.timeout(默认值为30s),以处理正在进行的请求。将terminationGracePeriodSeconds设为至少preStop休眠时间 +timeout,并留出余量(例如60)。如果没有preStop钩子,则将drainDelay保持为默认值(5s):网关会将/readyz标记为失败,并在停止接受连接前继续提供服务这么长时间。 - 超时:
gateway.timeouts.readHeader(10s)和idle(120s)有默认值。默认情况下,read/write不限时,因为 MCP Streamable HTTP/SSE 和边缘 WebSocket 都是长连接;设置这些超时也会中断这些连接。默认情况下,gateway.maxRequestBodySize处于关闭状态;如果设置了该项,当调用方在工具参数中传递 Base64 文件内容时,请确保其值大于fileFetch.maxSize× 4/3。 - 内存:将
GOMEMLIMIT设为容器内存限制的约 80-90%,使 Go GC 在 Pod 因内存不足被终止前更积极地回收内存。 - 自动扩缩容:根据进行中的请求数或请求速率扩缩容,而不是根据 CPU(参见自动扩缩容指标)。
spec:
terminationGracePeriodSeconds: 60
containers:
- name: manifold
lifecycle:
preStop:
sleep: { seconds: 10 } # with gateway.shutdown.drainDelay: -1s
env:
- name: GOMEMLIMIT
value: 900MiB # container limit: 1Gi
readinessProbe:
httpGet: { path: /readyz, port: 8081 }
livenessProbe:
httpGet: { path: /healthz, port: 8081 }
resources:
limits:
memory: 1Gi
下游客户端注册
Manifold 作为其前端 MCP 客户端的 OAuth 2.1 授权服务器,同时作为 OAuth 客户端连接其代理的后端。下游客户端可通过以下两种方式之一在 Manifold 中注册:
- 动态客户端注册(RFC 7591) — 客户端向
/{server_name}/auth/clients提交其元数据,并获得生成的client_id。始终可用。 - 客户端 ID 元数据文档(CIMD) — 客户端将 HTTPS URL 作为其
client_id提供,Manifold 会从该 URL 获取元数据文档。通过oauth.cimd.enabled启用。
flowchart LR
C[MCP client] -->|client_id| L[Authorization endpoint]
L --> R{Resolve client}
R -->|registered via DCR| OK[Client registration]
R -->|HTTPS URL and CIMD enabled| D[Fetch metadata document]
D --> OK
R -->|otherwise| E[401 invalid_client]
OK --> U{Resolve upstream client}
U -->|mapped in clients| A[Redirect to upstream authorization endpoint]
U -->|unmapped and unknownClient is default| A
U -->|unmapped and unknownClient is reject| E
客户端 ID 元数据文档
oauth:
cimd:
enabled: true
allowedOrigins:
- https://client-a.example.com
cacheTTL: 1h
maxDocumentSize: 65536
启用后,/.well-known/oauth-authorization-server/mcp/{server_name} 会声明 client_id_metadata_document_supported: true,而未注册为 DCR 客户端的 client_id 会被视为文档 URL。只有满足以下所有条件时才会接受该 URL:
- 使用
https方案,主机名既不是 IP 字面量也不是localhost,路径不是/,且没有片段或用户信息 - 其源位于
allowedOrigins中(当该列表非空时) - 响应状态为
200,Content-Type为application/json,大小不超过maxDocumentSize,且未通过重定向到达 - 文档的
client_id与请求的client_id逐字节相同(不进行规范化) redirect_uris非空,且每个条目都使用https,或使用主机为环回地址(localhost、127.0.0.1或[::1];端口不限)的httptoken_endpoint_auth_method未设置或为none(CIMD 客户端是公共客户端)- 若存在
grant_types,其中包含authorization_code
已解析的客户端会被缓存,缓存时长取 cacheTTL 与响应中 Cache-Control: max-age 的较小值;no-store / no-cache 会禁用缓存。其他情况均会被拒绝,并返回 invalid_client,拒绝原因仅记录在日志中。CIMD 客户端不绑定到单个 MCP 服务,因此必须访问包含服务名称的授权端点(/{server_name}/auth/login),而不能访问 /authorize 别名。
不支持 private_key_jwt 和 jwks_uri。
将下游客户端映射到上游客户端
若不进行映射,所有下游客户端都会共用一个上游客户端,因此上游同意页面始终显示 Manifold。如果用户已拥有该客户端的上游会话,另一个下游客户端就可以在用户未同意的情况下获取授权码(困惑代理问题)。Manifold 没有自己的同意页面;相反,每个下游 client_id 都可以映射到自己的上游客户端,以便上游授权服务器以该客户端的注册名称显示同意页面,并按客户端跟踪同意状态。
mcpServers:
my-api:
spec: https://example.com/api/openapi.json
baseURL: https://example.com
oauth2:
authURL: https://example.com/oauth/authorize
tokenURL: https://example.com/oauth/token
scopes: [read, write]
clients:
- downstreamClientID: "https://client-a.example.com/oauth-client.json"
clientID: client-a
clientSecret: ${CLIENT_A_SECRET}
- downstreamClientID: "https://client-b.example.com/.well-known/oauth-client"
clientID: client-b
clientSecret: ${CLIENT_B_SECRET}
unknownClient: reject
clients 是一个列表,而不是以 client_id 下游值为键的映射,因为配置加载器会将映射键转换为小写并按 . 拆分——CIMD URL 和 DCR 签发的 client_id 都无法经受这两种处理。将值保存在 downstreamClientID 中可以逐字节保留原值。
该映射同时也充当白名单:设置 clients 后,unknownClient: reject(默认值)会拒绝没有映射的下游客户端,并返回 invalid_client;被拒绝的 client_id、客户端名称和服务名称都会记录到日志中以供审计。Manifold 始终会执行上游重定向,因此同意始终由上游决定。
无论 clients 和 unknownClient 如何设置,通过 DCR 注册的客户端都只能使用其注册时指定的 MCP 服务,而 CIMD 客户端仍可跨服务使用(参见 docs/design/dcr-client-server-binding.md)。
unknownClient: default 会保留原有行为:未映射的客户端回退到共用的 clientID / clientSecret:
unknownClient: default
clientID: manifold
clientSecret: ${MANIFOLD_SECRET}
authParams:
prompt: consent
请注意,default 无法完全避免困惑代理问题——未映射的客户端在上游仍会显示为 Manifold。通过 authParams 添加 prompt: consent 可以缓解这一问题,但 prompt 是 OpenID Connect 参数,纯 OAuth 2.0 授权服务器可能会忽略它。在生产环境中,建议使用 reject 并明确配置 clients 白名单。
自动 OAuth 2.1 发现(用于未配置 oauth2 块的 MCP 后端)会通过 DCR 注册 Manifold 本身,并始终使用这一个共用客户端;clients 不适用于它。
工具授权(OPA sidecar)
Manifold 可以在调用 tools/call 和 tools/list 时,强制限制调用方可使用的 server/tool 组合,并将每项决策委托给外部 OPA sidecar。默认禁用(authz.enabled: false,保持原有行为);身份验证、组解析和策略存储不在 Manifold 的职责范围内——它信任上游层注入的身份标头,并向 OPA 查询决策。
authz:
enabled: true
opaURL: http://localhost:8181
timeout: 3s
decisionPath:
list: /v1/data/mcp/authz/allowed_tools
call: /v1/data/mcp/authz/allow
catalog: /v1/data/mcp/authz/allow_catalog
headers:
userID: x-user-id
userGroups: x-user-groups
input:
user: user
groups: groups
server: server
service: service
tool: tool
tools: tools
toolName: name
fromHeaders:
tenant:
header: x-tenant-id
required: true
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
enabled | bool | false | 启用 authz 中间件。仅当此项为 true 时,才会读取下面的其他字段 |
opaURL | string | http://localhost:8181 | OPA sidecar 的基础 URL(http 或 https) |
timeout | duration | 3s | 每项决策的 HTTP 超时时间 |
decisionPath.list | string | /v1/data/mcp/authz/allowed_tools | 每次 tools/list 时查询的 OPA 数据路径 |
decisionPath.call | string | /v1/data/mcp/authz/allow | 每次 tools/call 时查询的 OPA 数据路径 |
decisionPath.catalog | string | /v1/data/mcp/authz/allow_catalog | 每次 GET /mcp/list?tools=true 时查询的 OPA 数据路径(见下文“用于编写策略的工具目录”) |
headers.userID | string | x-user-id | 携带调用方用户 ID 的入站标头 |
headers.userGroups | string | x-user-groups | 携带调用方所属组的入站标头,组之间以逗号分隔 |
headers.bypass | string | x-authz-bypass | 入站标头;若其值恰为 true,则对此次请求禁用 authz 强制检查(见下文“按租户禁用授权”) |
input.user | string | user | 每项决策输入中表示调用方用户 ID 的 JSON 键 |
input.groups | string | groups | 每项决策输入中表示调用方所属组的 JSON 键 |
input.server | string | server | tools/call 输入以及 tools/list 中每个数组元素里表示服务名称的 JSON 键 |
input.service | string | service | tools/call 输入以及 tools/list 中每个数组元素里表示服务代码(service.code;若未设置,则为服务名称)的 JSON 键 |
input.tool | string | tool | tools/call 输入中表示工具名称的 JSON 键 |
input.tools | string | tools | tools/list 输入中表示工具数组的 JSON 键 |
input.toolName | string | name | tools/list 中每个数组元素里表示工具名称的 JSON 键 |
input.fromHeaders | map[string]object | {} | 将决策输入字段名映射到其值所读取的入站 HTTP 标头。默认为空,不添加任何字段。见下文“多租户策略数据” |
input.fromHeaders.<field>.header | string | — | 携带字段值的入站标头。必填,且必须是有效的 HTTP 标头字段名 |
input.fromHeaders.<field>.required | bool | true | 为 true 时(默认值,包括省略此键的情况),缺少标头或标头值为空都会拒绝请求。为 false 时,则不将该字段纳入决策输入 |
input.fromHeaders.<field>.type | string | string | 原始标头值转换为 JSON 值的方式:string、list 或 number。为空时表示 string;其他值会在启动时被拒绝 |
Manifold 将 headers.userID 的值视为不透明字符串:它不会对其进行解析,只会将其原样传递给决策输入中 authz.input.user 指定的键(默认是 user)。在多租户部署中,请使用包含租户信息的格式(例如 {tenant}:{user}),以便策略区分不同租户;也可以改用 input.fromHeaders(见下文“多租户策略数据”),此时 headers.userID 无需包含租户信息。同样,headers.userGroups 的值应使用不可变的不透明 ID(例如 ULID),而非显示名称,因为显示名称可能会更改。
input 让策略作者能够匹配现有的决策输入契约,而不必将策略重命名为 Manifold 的默认名称。同一输入对象中同时出现的键必须两两不同:user / groups / server / service / tool(tools/call 输入)、user / groups / tools(tools/list 输入),以及 server / service / toolName(每个 tools/list 数组元素)——启动时的验证会拒绝这些组中出现的任何键冲突。每个键也都不能为空。input.fromHeaders 字段名同样不能为空,且不得与上述任何(可能已重命名的)顶层键冲突——user / groups / server / service / tool / tools。比较区分大小写,因为 OPA 输入键区分大小写:使用默认名称时,字段名为 User 可以被接受,因为 input.user 是不同的键。toolName 并非保留名称:它只用于命名 tools 数组元素内部的键,不会用于顶层键。同一个标头可以分配给多个字段。
前提条件
Manifold 会信任每个请求中的 headers.userID / headers.userGroups,以及(如果已配置)headers.bypass 和 input.fromHeaders 中列出的每个标头,而不会自行验证它们;这与 WebMCP 反向网关的 forwardAuth 模式存在相同的注意事项(请参阅 docs/design/webmcp-reverse-gateway.md 中的 Trust boundary 部分)。启用 authz.enabled 之前:
- 前置代理必须剥离或覆盖客户端提供的任何同名标头,以防调用方伪造自己的身份
- 必须在网络层阻止绕过该代理直接访问 Manifold(例如使用 Kubernetes
NetworkPolicy) headers.bypass比身份标头更敏感:能够将其设为true的调用方可以完全禁用其自身请求的授权,无论其身份或组成员资格如何。前置代理必须以同等严格的方式剥离或覆盖该标头,并且必须在网络层封闭所有无需经过该代理即可访问 Manifold 的网络路径——不能仅通过单独的身份验证来处理
决策契约
对于每个 tools/call,Manifold 都会向 opaURL + decisionPath.call POST {"input": ...};对于每个 tools/list,会向 opaURL + decisionPath.list POST 一次(对所有工具批量查询,而非逐个工具查询);对于每个 GET /mcp/list?tools=true,会向 opaURL + decisionPath.catalog POST。提示词会经过相同的两项工具决策,并使用相同的输入,因为提示词可能包含有关如何使用工具的指令:prompts/get 会使用提示词名称作为 tool 查询 decisionPath.call,而 prompts/list 会将每个提示词作为 tools 中的一个元素来查询 decisionPath.list。提示词名称会带有 prompt: 前缀(提示词 summarize_pet 的决策名称为 prompt:summarize_pet),这样策略就能区分提示词和同名工具;: 不属于 MCP 推荐用于工具名称的字符。使用 <server>/prompt:<name> 规则授权提示词,例如,使用 petstore/prompt:* 授权 petstore 的所有提示词。以下示例使用默认的 authz.input 键名;每个键都可重命名(参见上文的 input 表):
// tools/call
{"input": {"user": "user-042", "groups": ["team-finance"], "server": "billing-svc", "service": "billing", "tool": "create_invoice"}}
// → {"result": true}
// tools/list
{"input": {"user": "user-042", "groups": ["team-finance"], "tools": [{"server": "billing-svc", "service": "billing", "name": "create_invoice"}, ...]}}
// → {"result": [{"server": "billing-svc", "service": "billing", "name": "create_invoice"}, ...]}
// GET /mcp/list?tools=true
{"input": {"user": "user-042", "groups": ["team-finance"]}}
// → {"result": true}
service 是服务端的 service.code(若未设置,则为服务端名称,因此对于未配置 service 的配置,它等于 server)。匹配 service 而非 server 的策略会一次性授权某项服务的所有服务端和 Agent——参见将服务端归入某项服务。tools/list 的结果仅根据 server 和 name 与请求匹配,因此策略可以原样返回输入条目,也可以仅返回 {server, name}。
注意:已有策略中的提示词 工具决策保持不变,但提示词现在也会经过相同的两项决策,因此已有策略也会对它们进行决策:
- 授权整个服务端的规则(例如
petstore/*,因为*会匹配prompt:summarize_pet)或忽略名称的规则(例如“授权此组的所有内容”)也会授权该服务端的提示词。工具名称列表(例如petstore/getpetbyid、*/getpetbyid)不会授权任何提示词,因此在<server>/prompt:<name>规则允许之前,提示词仍会隐藏。- MCP 后端现在会公布提示词,因此客户端会调用
prompts/list/prompts/get,OPA 也会收到更多查询(后端没有提示词时不会收到查询)。- 提示词名称在
tool/tools[].name中会显示为prompt:<name>,OPA 的决策日志中也一样。
Manifold 不会规定 OPA 的 data 文档结构;策略可以自由采用任意结构——参见 examples/opa/ 中可正常运行的 policy.rego 和 data.json(data.policies[<group id>].tools 是 <server>/<tool> glob 模式列表,data.policies[<group id>].catalog 是布尔值)。
多租户策略数据
input.fromHeaders 将决策输入字段名映射到传入的 HTTP 标头,因此上游身份层已知的值(租户 ID、区域)无需编码到 headers.userID 中,就能传递给策略。每个已配置的字段都会针对每种决策类型(tools/call、tools/list 和 GET /mcp/list?tools=true)进行解析,并作为顶层字段与 user / groups / 等字段一同添加:
authz:
input:
fromHeaders:
tenant:
header: x-tenant-id
required: true
roles:
header: x-roles
required: false
type: list
seat_count:
header: x-seat-count
type: number
// tools/call
{"input": {"user": "user-042", "groups": ["team-finance"], "server": "billing-svc", "tool": "create_invoice", "tenant": "acme", "roles": ["admin", "auditor"], "seat_count": 42}}
type 控制原始标头值转换成的 JSON 类型:
type | 决策输入值 | 备注 |
|---|---|---|
string(默认) | 未经修改的原始标头值 | |
list | 字符串数组 | 按 , 分割,修剪每个元素并丢弃空元素——与 headers.userGroups 使用相同的规则 |
number | JSON 数字 | 原始数字不经舍入直接传递。若值不是数字,则拒绝请求,无论该字段是否必填 |
required 默认为 true——省略此键会保留身份标头的失败关闭行为。设置 required: false 时,若标头缺失或为空(或 list 中没有非空元素),则该字段会完全不包含在决策输入中,而不是传递一个空值,因此策略应对其进行保护:
# input.roles is absent on requests that carried no x-roles header, so read
# it through a default instead of indexing it directly.
roles := object.get(input, "roles", [])
通过 tenant 字段,可以按租户组织 data,而不是采用扁平结构,这样一个 bundle 就能服务所有租户,无需在 user 中嵌入命名约定:
package mcp.authz
default allow := false
allow if {
tenant_policies := data.tenants[input.tenant].policies
some group in input.groups
some pattern in tenant_policies[group].tools
glob.match(pattern, ["/"], sprintf("%s/%s", [input.server, input.tool]))
}
这取代了上文针对 headers.userID 介绍的 {tenant}:{user} 约定——使用 input.fromHeaders 明确解析租户后,headers.userID 只需标识该租户内的用户。
分发每个租户的数据
Manifold 只知道 opaURL 和 decisionPath.*;策略和 data 如何传递到 sidecar 由 OPA 负责(关于通过 HTTP 将它们作为 bundle 提供,请参阅下文“运行建议”)。将 data 按租户设为键后,你可以选择拆分的粒度:
flowchart LR
M[Manifold] -->|"POST /v1/data/mcp/authz/allow<br/>input.tenant = acme"| O[OPA sidecar]
O -.->|poll| B[(bundle service)]
B -.->|"mcp-authz/policy.tar.gz<br/>roots: mcp/authz"| O
B -.->|"tenants/acme/bundle.tar.gz<br/>roots: tenants/acme"| O
B -.->|"tenants/globex/bundle.tar.gz<br/>roots: tenants/globex"| O
一个 OPA 可以加载多个 bundle,每个 bundle 负责 data 中一个互不重叠的子树,因此可以独立发布和回滚某个租户的策略数据,而不影响其他租户。OPA 端的配置如下:
services:
bundles:
url: https://bundles.example.com
bundles:
policy:
service: bundles
resource: mcp-authz/policy.tar.gz
tenant-acme:
service: bundles
resource: tenants/acme/bundle.tar.gz
tenant-globex:
service: bundles
resource: tenants/globex/bundle.tar.gz
每个 bundle 的 .manifest 都会声明其负责的子树;上面的 Rego 仍可照常读取 data.tenants[input.tenant]。
// mcp-authz/policy.tar.gz
{"revision": "2026-08-29-01", "roots": ["mcp/authz"]}
// tenants/acme/bundle.tar.gz
{"revision": "2026-08-29-01", "roots": ["tenants/acme"]}
根据 OPA 合并 bundle 的方式,有三个限制:
- 根路径不得重叠。 OPA 会拒绝激活根路径与其他 bundle 冲突的 bundle(例如,同时使用
["tenants"]和["tenants/acme"]),因此拆分时必须为每个租户分别拆分,共享数据也不能与租户专属数据放在同一个子树中 - 拆分不等于隔离。 所有 bundle 仍会加载到同一个 OPA 进程的同一棵
data树中,因此策略可以读取data.tenants.globex。租户边界由策略通过input.tenant进行索引来强制执行;bundle 边界只限定更新范围和影响范围 - 添加租户需要更改 OPA 配置。
bundles:是静态的,因此每增加一个租户,就需要重新配置 sidecar。OPA 的 discovery 功能可以分发 bundle 列表本身,但会增加一个变动因素;而且每个 bundle 都会独立轮询,因此这种方式无法很好地扩展到数量非常多的租户
另一种做法是不共享 sidecar:为每个租户运行一组 Manifold + OPA。这样 sidecar 本身就是租户,data 无需按租户分层,也无需通过 input.fromHeaders 解析租户。
| 部署方式 | 通过 input.fromHeaders 获取 tenant |
|---|---|
| 一组 Manifold + OPA 为多个租户提供服务 | 必须——决策输入是区分租户的唯一方式 |
| 每个租户各运行一组 Manifold + OPA | 不需要——sidecar 会隐式标识租户 |
用于编写策略的工具目录
编写策略需要知道所有现存的 <server>/<tool> 对,但 tools/list 只会显示调用方当前已获准查看的内容。GET /mcp/list?tools=true 返回的是未经筛选的目录:当 authz.enabled 为 false 时,任何人都可以访问;当其为 true 时,它会像 tools/call 查询 decisionPath.call 一样查询 decisionPath.catalog——通过 headers.userID / headers.userGroups 标识调用方,并在身份缺失、策略拒绝或 Decider 出错时拒绝请求(403 {"error": "forbidden"}),绝不会回退到静态允许列表。
{
"mcp": [
{
"name": "petstore",
"description": "Swagger Petstore sample API",
// service.code / service.name, defaulting to the server name.
"service": {"code": "pets", "name": "Pet Store"},
"tools": [
{"name": "getpetbyid", "summary": "Find pet by ID.", "description": "Returns a single pet."}
]
},
// A WebMCP reverse server's tools only exist per-browser-connection, so
// it reports "dynamic" instead of a tool list.
{"name": "billing-svc", "description": "browser app", "service": {"code": "billing-svc", "name": "billing-svc"}, "dynamic": true},
// A backend that failed to connect still lists (with "error" instead of
// "tools") rather than dropping out of the response.
{"name": "crm", "description": "CRM MCP backend", "service": {"code": "crm", "name": "crm"}, "error": "connect: dial tcp: connection refused"}
]
}
按租户禁用授权
负责转发请求的代理可以让多个租户共用一个 Manifold 部署,并在不全局切换 authz.enabled 的情况下,为单个请求禁用 authz:将 headers.bypass(默认为 x-authz-bypass)设为精确字符串 true。任何其他值——True、1、空值或缺少此标头——都会执行正常的 authz 检查(失败时拒绝)。
对绕过检查的请求:
tools/call会跳过 OPA,直接调用工具tools/list会返回后端的完整工具列表,不做筛选GET /mcp/list?tools=true会返回200和完整目录,不查询decisionPath.catalog
这相当于仅对该请求设置 authz.enabled: false。Manifold 会记录 decision: bypass(包含 server / method,不含身份信息——因为未解析身份),以便在审计记录中区分绕过检查的请求与 allow / deny 请求。
失败时拒绝
对于任何模糊不清或出错的情况,都会拒绝请求,而不是放行:
- 如果缺少
headers.userID/headers.userGroups,或其值为空,则不查询 OPA,直接拒绝 - 如果
input.fromHeaders中配置为必填的字段缺少对应标头,或标头值为空,则同样不查询 OPA,直接拒绝。required默认为true;对于required: false的字段,会将其从输入中省略,而不是拒绝请求 - 如果
input.fromHeaders的值无法解析为所配置的type(例如,标头值不是数字,但配置了type: number),则不查询 OPA,直接拒绝,无论required的值是什么 - 如果 OPA 返回非
200响应、响应中缺少预期的result字段,或连接超时、连接失败,都会拒绝请求 tools/list筛选是一项便利功能——它会隐藏调用方无权使用的工具,以免这些工具挤满客户端的工具选择器——但它并非强制执行权限的环节。权限在tools/call时强制执行;已经知道工具名称的客户端(例如,名称来自过期列表)仍会在此处遭到拒绝- 反向(WebMCP)
mcpServers条目始终会注册一个create_pairing_code工具(参见docs/design/webmcp-reverse-gateway.md),且authz.enabled会像处理其他工具一样对其进行授权。若某个组需要能够与此类服务配对,其策略中必须包含<server>/create_pairing_code,否则配对本身也会被拒绝 - 这在 OPA 内部更低一层也同样适用:如果 bundle 获取失败,OPA 会继续使用最近一次激活的 bundle 执行策略——bundle 服务中断会阻止策略更新,但不会影响决策。不过,如果 OPA 启动后从未激活过 bundle(例如,启动时无法连接 bundle 服务),
data将保持为空,每个决策都会返回false/[],并以同样的方式在失败时拒绝。bundle 获取失败仍值得触发告警——请参阅下文的“运行建议”
运维建议
-
启用 OPA 的决策日志,为每次
allow/allowed_tools/allow_catalog查询保留审计记录。每个事件都应包含决策、Manifold 在该决策输入中发送的相同字段,以及生成该决策的策略数据修订版本——如果没有数据修订版本,就无法判断某个决策是在什么策略版本下作出的。不同决策类型的输入字段各不相同(见上文“决策约定”);下表中的名称是authz.input默认值,均可重命名:决策 查询 输入字段 allowtools/calluser、groups、server、service、toolallowed_toolstools/listuser、groups,以及包含{server, service, name}条目的tools数组allow_catalogGET /mcp/list?tools=trueuser、groups所有已解析的
input.fromHeaders字段都会出现在这三种输入中,且位于顶层。如果某个字段的required: false,那么在请求头缺失或为空的请求中,该字段不会出现在输入里,因此决策日志中没有该字段是正常情况,并非字段被丢弃。 -
通过 HTTP 提供 OPA 捆绑包来分发策略和数据,而不是挂载本地文件,这样更新策略就无需重启边车。捆绑包模式还会在每个决策日志事件中写入
bundles.<name>.revision,该值即为修订版本的来源。 -
监控 OPA 的捆绑包获取状态(失败对强制执行的影响见上文“失败时默认拒绝”):OPA 的 Health API(
GET /health?bundles=true)会在所有已配置捆绑包至少激活一次之前报告不健康,因此也可用作就绪探针。状态 API 和决策日志也会显示获取失败情况。
参见 examples/opa/,了解包含示例策略和数据、可运行的 OPA 边车。
HTTP 端点
Manifold 暴露的 HTTP 端点。
MCP
| 方法 | 路径 | 描述 |
|---|---|---|
POST | /mcp/{server_name} | MCP 请求(Streamable HTTP)。{server_name} 是一个 mcpServers 或 agents 条目 |
GET | /mcp/list | 列出已注册的服务(名称、描述和服务)。添加 ?tools=true 可获取工具目录(见上文“用于编写策略的工具目录”) |
OAuth 2.1
| 方法 | 路径 | 描述 |
|---|---|---|
GET | /.well-known/oauth-authorization-server/mcp/{server_name} | 授权服务器元数据 |
GET | /.well-known/oauth-protected-resource/mcp/{server_name} | 受保护资源元数据 |
GET | /{server_name}/auth/login | 重定向到登录页面 |
GET | /{server_name}/auth/callback | OAuth 回调 |
POST | /{server_name}/auth/token | 颁发令牌 |
POST | /{server_name}/auth/clients | 动态客户端注册(RFC 7591) |
GET | /authorize、/callback | 不带服务名称的别名 |
POST | /token、/register | 不带服务名称的别名 |
其他
| 方法 | 路径 | 描述 |
|---|---|---|
GET | /media/download/{id} | 下载已存储的内容(仅在设置了 storage.hostURL 时) |
GET | /healthz | 存活探针。始终返回 200 |
GET | /readyz | 就绪探针。返回 200;优雅关闭开始后返回 503 |
GET | /metrics | Prometheus 指标(仅当 telemetry.metrics.exporterType: pull 时) |
开发
有关如何设置开发环境和提交更改,请参阅 CONTRIBUTING.md。
测试
ConfigMap 规范加载测试使用 envtest,它需要通过 setup-envtest 获取一组 kube-apiserver/etcd 二进制文件。运行一次 mise install 即可(setup-envtest 已在 mise.toml 中声明)。make test 会在运行测试前下载这些二进制文件,测试会在 setup-envtest 的默认位置找到它们,无需设置 KUBEBUILDER_ASSETS。如果直接运行 go test,请先使用 setup-envtest use 1.36.2 获取这些文件;如果缺少文件,测试将失败。
make test
端到端(Postman CLI)
make postman 会构建网关、启动 OPA 和一个 Petstore API 存根,并运行 tests/postman/ 中的 Postman 集合,一并检查工具筛选、工具授权和审计日志。Postman CLI 通过 mise install 安装(mise.toml 中的 postman-cli)。CI 会在每个拉取请求中运行它。
代码检查
make lint
灵感来源
本项目的灵感来自 LiteLLM 的 Agent / MCP Gateway。
与 LiteLLM 的 MCP Gateway 为多个 MCP 服务提供统一访问入口类似,Manifold 旨在成为一个网关,将单一 MCP 接口连接到多个 MCP 服务 / REST API。
许可证
MIT 许可证
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。