mcpField
M
Manifold

Manifold

本地服务
github.com开发工具54 天前更新

关于 Manifold

Manifold 是连接 MCP 客户端与多个外部 MCP 服务、OpenAPI/Swagger REST API 及 A2A Agent 的网关。它可将 API 规范转换为 MCP 工具,并提供路由、工具控制、身份验证和可观测性功能。

主要功能

  1. 01将 OpenAPI 和 Swagger 规范转为工具
  2. 02聚合外部 MCP 服务
  3. 03将 A2A Agent 技能作为工具提供
  4. 04筛选并重命名工具
  5. 05支持 OAuth 和后端身份验证
  6. 06导出遥测数据和审计日志

适用场景

  1. 01通过一个 MCP 端点接入多个后端
  2. 02生成并审查 API 工具目录
  3. 03控制客户端可调用的后端工具
  4. 04跟踪网关活动和遥测数据

MCP 相关

Manifold 作为 MCP 服务面向客户端,将请求路由到外部 MCP 服务、REST API 和 A2A Agent。它可转发后端工具和提示词,也可透传 MCP Apps 界面。

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"nonchan7720-manifold": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"ENCRYPT_KEY",
"-e",
"SERVER_NAME",
"ghcr.io/nonchan7720/manifold:v1.24.1"
],
"env": {
"ENCRYPT_KEY": "<ENCRYPT_KEY>",
"SERVER_NAME": "<SERVER_NAME>"
}
}
}
}
启动命令docker run -i --rm -e ENCRYPT_KEY -e SERVER_NAME ghcr.io/nonchan7720/manifold:v1.24.1
需要配置的环境变量
ENCRYPT_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+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
ocighcr.io/nonchan7720/manifold:v1.24.1

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

Manifold

一个接口。多种连接。Manifold。

CI Release License: MIT

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 会作为 A2A contextId 传递,响应上下文则通过 _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 会原样返回。

推荐工作流:

  1. 将 tools.file 添加到服务器配置中(参见 mcpServers.<name>.tools),然后运行 manifold openapi generate -c config。
  2. 提交生成的文件。文件中的 tools 部分使上游规范变更可以像普通 PR 差异一样进行审查。
  3. 启动网关(manifold gateway -c config)——它会从文件读取工具,启动时不会通过网络访问 spec。
  4. 上游规范变更后,重新运行 manifold openapi generate -c config 并提交更新。如果文件已过期(规范已变更但文件未重新生成),网关启动会失败,并提示你重新生成文件。
  5. 将 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 工具注释:

方法类型readOnlyHintdestructiveHintidempotentHint
GET / HEAD / OPTIONS / TRACEreadOnlytruefalsetrue
POST / PUT / PATCH(及其他方法)writefalsefalsefalse
DELETEdestructivefalsetruefalse

操作可以通过 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-onERR如果有任何变更达到或超过此级别,则以非零状态退出:ERR、WARN 或 INFO。"" 或 NONE 表示变更不会导致失败(加载错误仍会导致失败)
--formattexttext、json 或 markdown
--by-toolfalse按 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,会作为 A2A contextId 转发)、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,而 Agent translator 有一项技能 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
字段类型描述
portint监听端口(默认值:8081)
keystringTLS 私钥文件路径(可选)
certstringTLS 证书文件路径(可选)
encryptKeystring令牌加密密钥。Base64 编码的 32 字节 AES-256 密钥。使用 openssl rand -base64 32 生成。使用 redis 或 sqlite 时必填;使用内存存储时,若未设置,则会在启动时生成随机密钥
specRefresh.intervalduration重新获取 OpenAPI 模式规范的间隔(例如 5m)。未设置或设为 0 时禁用刷新
specRefresh.rejectOnstring当刷新后的规范变更达到此级别(ERR、WARN 或 INFO)时拒绝该规范,并继续提供当前工具。未设置、设为 "" 或 NONE 时永不拒绝(默认值)。参见刷新期间的破坏性变更
shutdown.drainDelayduration收到 SIGTERM 后,/readyz 返回 503,网关继续提供服务指定时长,然后停止接受连接,以便 Kubernetes 先从 Service 端点中移除该 Pod。如果 preStop 睡眠钩子已执行此操作,请设为 -1s(默认值:5s;负值表示禁用等待)
shutdown.timeoutduration优雅关闭等待正在处理的请求完成的最长时间(默认值:30s)
timeouts.readHeaderduration读取请求标头的最长时间(默认值:10s;负值 = 不限时)
timeouts.idledurationkeep-alive 连接的最长空闲时间(默认值:120s;负值 = 不限时)
timeouts.readduration读取完整请求(包括请求体)的最长时间(默认值:不限时)。请勿设置:MCP Streamable HTTP / SSE 和边缘 WebSocket 都是长连接
timeouts.writeduration写入响应的最长时间(默认值:不限时)。原因同 timeouts.read,请勿设置
maxRequestBodySizeint/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 路径,因此只能包含字母数字字符、_ 和 -。

FieldTypeDescription
descriptionstring服务描述(必填;包含在 /mcp/list 响应中)
service.codestring将此条目与其他条目归为一组的服务代码(默认为服务名称)。作为 input.service 传递给 authz(参见将服务器归入服务)
service.namestring在 UI 中显示的服务名称(默认为服务代码)。由 /mcp/list 返回
transportstringMCP 后端的传输方式(http 或 stdio)
urlstringHTTP 传输方式的端点
commandstringstdio 传输方式的命令
args[]stringstdio 命令的参数
envmap[string]stringstdio 进程的环境变量
specstringOpenAPI/Swagger 规范的路径、URL 或 configmap://<namespace>/<name>/<key> 引用。除非设置了 tools.file,否则 OpenAPI 模式下必填;设置后网关不会读取此项,但 manifold openapi generate、--check 和 openapi tools --from-spec 仍需要它
baseURLstringOpenAPI 模式的 API 基础 URL。设置 spec 后,默认为规范中第一个 servers 条目(相对 URL 会根据规范 URL 解析);如果由此得出的 URL 不是绝对 http(s) URL,则配置验证(以及网关启动)会失败,因此当本地规范文件没有 servers 或仅包含 /api/v1 等相对 URL 时,请显式设置 baseURL(1.19 中的行为变更)。仅设置 tools.file 时必填
headersmap[string]string添加到 API 请求中的额外标头
authValueobject静态身份验证设置(header、prefix、value)
oauth2objectOAuth 2.0 设置(见下文)
tokenExchangeobjectToken Exchange 设置(见下文)
googleServiceAccountobjectGoogle 服务账号设置,仅适用于 OpenAPI 模式和 http 传输方式(见下文)
auth.jwtobject在网关验证客户端的 Bearer JWT(见下文)
specRefreshIntervalduration覆盖此服务器的 gateway.specRefresh.interval。0 会禁用此服务器的刷新
specRefreshRejectOnstring覆盖此服务器的 gateway.specRefresh.rejectOn(ERR、WARN、INFO)。NONE(或 "")表示此服务器永不拒绝
tools.filestring生成的工具文件路径(参见 mcpServers.<name>.tools)。设置后,网关会从此文件启动,而不是获取 spec
tools.include / tools.exclude[]string用于选择要公开的工具的 Glob 模式。在 OpenAPI / Swagger 服务器上,exclude 也会匹配 operationId,并将匹配的操作完全排除在目录之外(参见选择要公开的工具)
tools.overridesmap[string]object针对每个工具(原始名称)的设置:name、description,以及用于显式指定原始工具名称的 tool。键会保留配置文件中的大小写,因此 getPetById: 会匹配工具 getPetById(仅大小写不同的键会被拒绝)
agentsmap[string]object附加到此服务的 A2A Agent;其技能会以 <agent>__<skill> 的形式添加到该服务的工具中。不适用于 transport: reverse(参见 mcpServers.<name>.agents.<agent>)
appsbool将每个请求中未声明支持 MCP Apps 的主机视为支持该功能。仅适用于 http / stdio(参见 MCP Apps)
prompts.url / prompts.filestringOpenAPI 模式服务器的提示词文档来源;必须且只能设置其中一项(参见 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.x spec 一起使用。
  • 生成的文件会嵌入完整的已解析 spec,包括其中包含的任何内部主机名或示例值。提交到公开仓库前,请先检查文件。
mcpServers.<name>.oauth2
字段类型说明
clientIDstring共享上游客户端的客户端 ID(当有效的 unknownClient 为 default 时必填,见下文)
clientSecretstring共享上游客户端的客户端密钥(要求与 clientID 相同)
authURLstring授权端点(必填;绝对 URL)
tokenURLstring令牌端点(必填;绝对 URL)
scopes[]string要请求的作用域
clients[]object将下游 client_id 映射到为其使用的上游客户端(参见下游客户端注册)
unknownClientstring如何处理 clients 中不存在的下游客户端:reject 或 default
authParamsmap[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 中映射每个客户端的情况。此时若缺少共享客户端,服务将在启动时失败。

unknownClientclients共享 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)。

字段类型说明
urlstring令牌兑换端点的绝对 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
字段类型描述
credentialsJSONstring服务账号密钥(JSON)本身,例如来自环境变量。不能与 credentialsFile 同时设置;如果两者均未设置,则使用 ADC
credentialsFilestring服务账号密钥(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
字段类型描述
issuerstring预期的 iss(绝对 URL,必填)
jwksURLstring用于验证签名的 JWKS URL(绝对 URL,必填)
audiencestring预期的 aud(必填,因此不会接受为其他资源签发的令牌)
agents.<name>

A2A Agent(参见连接到 A2A Agent)。名称与 mcpServers 共用命名空间,并遵循相同的字符规则;两者使用相同名称属于配置错误。

字段类型描述
descriptionstring调用 Agent 的说明(必填)。会添加到每个技能的工具描述前,并由 /mcp/list 返回
service.codestring服务代码,与 mcpServers.<name> 相同(默认值:Agent 名称)
service.namestring服务显示名称,与 mcpServers.<name> 相同(默认值:服务代码)
urlstring获取 Agent Card 的基础 URL(必填)。消息会发送到 Card 中声明的端点
agentCardPathstring相对于 url 的 Agent Card 路径(默认值 /.well-known/agent-card.json)
headersmap[string]string添加到 Agent Card 和消息请求中的额外标头
authValueobject静态身份验证设置(header、prefix、value)
oauth2objectOAuth 2.0 设置(与 mcpServers.<name>.oauth2 相同)。只有消息请求会携带调用方的令牌;Agent Card 仅通过 headers / authValue 获取
tokenExchangeobject令牌交换设置(与 mcpServers.<name>.tokenExchange 相同)
skills[]string要作为工具公开的 Agent Card 技能 ID,按此顺序排列;Card 中缺失的 ID 会跳过并显示警告。不设置则公开所有技能
timeoutduration单次 message/send 的超时时间(默认值 60s)

authValue / oauth2 / tokenExchange 三者互斥。

mcpServers.<name>.agents.<agent>

附加到服务的 A2A Agent(参见将 Agent 附加到服务)。<agent> 遵循服务名称的字符规则(字母数字字符、_ 和 -),且不得包含 __,因为工具名称使用它来分隔 Agent 和技能(<agent>__<skill>)。Agent 名称的作用域限定于服务,因此可以在不同服务间重复,也可以与顶层名称相同。

FieldTypeDescription
descriptionstring调用 Agent 的说明(必填)。会添加到每个技能的工具说明前,并由 /mcp/list 返回
urlstring解析 Agent Card 的基础 URL(必填)。消息会发送到卡片中声明的端点
agentCardPathstring相对于 url 的 Agent Card 路径(默认值 /.well-known/agent-card.json)
headersmap[string]string添加到 Agent Card 和消息请求中的额外标头
authValueobject静态身份验证设置(header、prefix、value)
tokenExchangeobjectToken Exchange 设置(与 mcpServers.<name>.tokenExchange 相同)
skills[]string要作为工具公开的 Agent Card 技能 ID,按此顺序排列;卡片中不存在的 ID 会跳过并发出警告。未设置时会公开所有技能
timeoutduration单次 message/send 的超时时间(默认值 60s)

这与 agents.<name> 相同,但不包含 oauth2,此处不接受该字段:OAuth 流程按服务配置,因此请使用 authValue、tokenExchange 或 headers,或者在顶层配置 agents 条目。此处会忽略 service,因为 Agent 的技能属于该服务。authValue 和 tokenExchange 不能同时使用。

oauth.cimd

接受下游客户端提供的 HTTPS client_id,该 ID 会解析为客户端 ID 元数据文档;无需通过 DCR 注册(参见下游客户端注册)。默认禁用。

FieldTypeDescription
enabledbool启用 CIMD 客户端注册(默认值:false)
allowedOrigins[]string非空时,只接受来自这些源的 client_id URL。在获取文档前应用此限制
cacheTTLduration已解析客户端的缓存时长上限(默认值:1h)。若 Cache-Control: max-age 设置的时长更短,则以其为准
maxDocumentSizeint从文档读取的最大字节数(默认值:65536)
redis
FieldTypeDescription
urlstringRedis URL(例如 redis://user:pass@localhost:6379/0)
addrs[]string主机:端口对列表(用于 Cluster/Sentinel)
userstring用户名
passwordstring密码
dbint数据库编号
master_namestringSentinel 主节点名称
tlsbool启用 TLS
cluster_modebool启用 Cluster 模式
key_prefixstring添加到存储在 Redis 中的每个键前面的前缀(默认:空)
sqlite
FieldTypeDescription
pathstring数据库文件路径(内存数据库使用 :memory:)
memory
FieldTypeDescription
enabledbool即使配置了 redis,也将会话和令牌保留在进程内存中

存储的选择顺序为:sqlite.path → memory.enabled → redis → 内存。内存存储会在重启时丢失会话和 OAuth 令牌,且不会在副本之间共享;生产环境请使用 redis 或 sqlite。

storage

将 OpenAPI/Swagger 工具响应中包含的内容(图像、二进制数据等)存储到外部存储,并返回资源链接(下载 URL)。JSON 响应中的 format: binary 字段也会上传,并替换为其下载 URL(参见二进制字段和响应)。未设置时,不使用存储。

FieldTypeDescription
typestring存储类型。目前仅支持 s3
hostURLstring下载 URL 的主机(设置后,内容通过 Manifold 的 /media/download/{id} 提供)
s3.bucketstringS3 存储桶名称(type: s3 时必填)
s3.keyPrefixstringS3 对象键前缀(type: s3 时必填)
storage:
  type: s3
  hostURL: https://manifold.example.com
  s3:
    bucket: my-bucket
    keyPrefix: manifold/media
audit
FieldTypeDescription
enabledbool为每次 tools/call 写入一行 JSON(参见审计日志)
outputstringstdout、stderr(默认)或文件路径(追加写入)
includeArgumentsbool同时记录调用参数(默认:false)
redact.keys[]string除默认值之外,值将被隐藏的参数键名称(参见隐藏敏感信息)
redact.patterns[]string从字符串参数值和错误消息中隐藏的正则表达式
guardrail
FieldTypeDescription
enabledbool使用 guardrail 检查每次 tools/call(参见Guardrail)
urlstring通用 Guardrail API 端点(启用时必填;绝对 http(s) URL)
headersmap[string]string添加到每个 guardrail 请求中的标头
timeoutduration单次 guardrail 调用的超时时间(默认 5s)
mode[]stringpre_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 仍会完整阻止访问私有地址段。

字段类型说明
allowLocalbool允许连接到任何 IP(包括回环地址)以及 http://(用于测试本地堆栈;默认值:false)
allowedHosts[]string主机允许列表(主机名或 host:port)。为空时允许所有主机(但仍会进行 IP 检查)
deniedHosts[]string主机拒绝列表(主机名或 host:port),在 allowedHosts 之后应用,并在每次重定向时检查
deniedCIDRs[]string下载器绝不可连接的 CIDR 地址段,即使设置了 SKIP_SECURE_CLIENT=true 也不例外(例如内部管理网络)
maxSizeint64下载内容(文件/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 输出跟踪、指标和日志的设置。

FieldTypeDescription
serviceNamestring服务名称
environmentstring环境名称(deployment.environment 属性)
gzipCompressionboolOTLP 导出的 Gzip 压缩
traceobject跟踪设置(enabled、http、grpc)
metricsobject指标设置(enabled、exporterType:push / pull、http、grpc)
logsobject日志设置(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 使用带点的名称):

MetricTypeDescription
http_server_active_requestsUpDownCounter正在处理的 HTTP 请求数(http.server.active_requests)。SSE / WebSocket 长连接在打开期间会计入;/healthz、/readyz 和 /metrics 除外
manifold_tool_calls_totalcountertools/call 请求,标签为 server、tool、status(ok / error)
manifold_tool_call_duration_secondshistogramtools/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];端口不限)的 http
  • token_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
字段类型默认值说明
enabledboolfalse启用 authz 中间件。仅当此项为 true 时,才会读取下面的其他字段
opaURLstringhttp://localhost:8181OPA sidecar 的基础 URL(http 或 https)
timeoutduration3s每项决策的 HTTP 超时时间
decisionPath.liststring/v1/data/mcp/authz/allowed_tools每次 tools/list 时查询的 OPA 数据路径
decisionPath.callstring/v1/data/mcp/authz/allow每次 tools/call 时查询的 OPA 数据路径
decisionPath.catalogstring/v1/data/mcp/authz/allow_catalog每次 GET /mcp/list?tools=true 时查询的 OPA 数据路径(见下文“用于编写策略的工具目录”)
headers.userIDstringx-user-id携带调用方用户 ID 的入站标头
headers.userGroupsstringx-user-groups携带调用方所属组的入站标头,组之间以逗号分隔
headers.bypassstringx-authz-bypass入站标头;若其值恰为 true,则对此次请求禁用 authz 强制检查(见下文“按租户禁用授权”)
input.userstringuser每项决策输入中表示调用方用户 ID 的 JSON 键
input.groupsstringgroups每项决策输入中表示调用方所属组的 JSON 键
input.serverstringservertools/call 输入以及 tools/list 中每个数组元素里表示服务名称的 JSON 键
input.servicestringservicetools/call 输入以及 tools/list 中每个数组元素里表示服务代码(service.code;若未设置,则为服务名称)的 JSON 键
input.toolstringtooltools/call 输入中表示工具名称的 JSON 键
input.toolsstringtoolstools/list 输入中表示工具数组的 JSON 键
input.toolNamestringnametools/list 中每个数组元素里表示工具名称的 JSON 键
input.fromHeadersmap[string]object{}将决策输入字段名映射到其值所读取的入站 HTTP 标头。默认为空,不添加任何字段。见下文“多租户策略数据”
input.fromHeaders.<field>.headerstring—携带字段值的入站标头。必填,且必须是有效的 HTTP 标头字段名
input.fromHeaders.<field>.requiredbooltrue为 true 时(默认值,包括省略此键的情况),缺少标头或标头值为空都会拒绝请求。为 false 时,则不将该字段纳入决策输入
input.fromHeaders.<field>.typestringstring原始标头值转换为 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 使用相同的规则
numberJSON 数字原始数字不经舍入直接传递。若值不是数字,则拒绝请求,无论该字段是否必填

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、tool
    allowed_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/callbackOAuth 回调
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/metricsPrometheus 指标(仅当 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 许可证

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。