mcpField
A
aimcpgate

aimcpgate

本地服务
github.com/akomyagin开发工具01 周前更新

关于 aimcpgate

aimcpgate 是面向需要统一接入多个 MCP 服务的运维人员的 Go 网关。它汇总上游服务的工具、提示词和资源,转发调用并记录日志。

主要功能

  1. 01汇总多个上游服务目录
  2. 02支持 stdio 与 HTTP/SSE
  3. 03筛选和重命名工具
  4. 04调用日志与查看命令
  5. 05上游限流、并发限制和重启
  6. 06汇总提示词与资源

适用场景

  1. 01通过一个客户端接入多个 MCP 服务
  2. 02检查转发调用和网关事件
  3. 03限制上游速率与并发

MCP 相关

它向客户端呈现单个 MCP 服务,并将请求复用转发至上游 MCP 服务。支持汇总工具、提示词和资源,以及 stdio 和 HTTP/SSE 传输。

配置

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

mcp.json · 通用配置
{
"mcpServers": {
"akomyagin-aimcpgate": {
"command": "npx",
"args": [
"-y",
"aimcpgate"
]
}
}
}
启动命令npx -y aimcpgate

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
ocighcr.io/akomyagin/aimcpgate:0.5.0
npmaimcpgate0.5.0

工具

还没有获取到工具列表

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

详细介绍

aiMCPGate

俄语版 — README_RU.md。

用 Go 编写的 MCP 服务(模型上下文协议)网关/代理。 对于 MCP 客户端(Claude Code、Cursor 等),它表现为一个 MCP 服务;在底层,它会将调用分发到多个上游 MCP 服务, 将它们的工具、提示词和资源汇总到一个目录中,并记录 每次调用。

状态:MVP 已完成(阶段 0–6),MVP 后阶段 7–19 也已发布,最新 版本为 v0.6.0。 阶段 1——在 stdio 端点后复用多个 stdio 上游,并记录调用日志;阶段 2——面向客户端的 HTTP/SSE 传输、HTTP 上游、CLI 日志查看器(mcp-gate logs);发布流水线 (goreleaser,为 linux/darwin/windows × amd64/arm64 交叉编译,不使用 CGO)。MVP 后新增了上游自动重启、配置热重载、工具过滤/重命名、doctor,以及在 v0.3.0 中新增的完整 prompts/resources/ resources/templates/completion 聚合、ping、进度转发和真正的取消、logging/setLevel 扇出、按上游设置调用限制(速率限制/并发限制/结果截断/超时)、延迟加载的目录和 tools/list 分页,以及客户端和上游两侧的 SSE 服务端→客户端流。v0.4.0 完成了服务端→客户端方向:所有三种由服务端发起的方法——elicitation/create、sampling/createMessage 和 roots/list——均可在全部四种传输组合中代理(客户端侧使用 stdio 或 HTTP × 上游侧使用 stdio 或 HTTP);网关现在会向上游声明与其客户端声明完全一致的能力,而不是一概声明 {};HTTP 传输新增了服务端 Mcp-Session-Id 会话,并支持通过 DELETE /mcp 终止会话。v0.5.0 新增了运维可观测性(阶段 18):八类事件——上游启动失败和监管器放弃重试、被丢弃的通知和服务端→客户端请求、没有 GET SSE 的 HTTP 上游、目录冲突和错误的 URI 模板,以及悄然绕过 max_result_bytes 的结果——现在会写入调用日志(mcp-gate logs),而不是写入通常由 MCP 客户端掌管的 stderr;配置解析也改为严格模式(未知或拼写错误的键会导致致命错误)。此外还补全了面向客户端的防护/截断机制:被速率限制或并发限制拦截的 tools/call 现在会返回专属 JSON-RPC 错误代码 -32009,并附带机器可读的 data: {"retryable":true,"reason":...},而不再返回难以区分的 -32603;绕过 max_result_bytes 的非文本结果会带有 result._meta 标记(content 保持逐字节不变)。最后,若 auth_token 引用的环境变量未设置,网关现在会拒绝启动,而不是悄然禁用 HTTP 身份验证。v0.6.0 让网关兼容两个时代:同一端点可同时服务两个 MCP 协议修订版,并按请求选择。使用 2026-07-28 规范的客户端可获得现代无状态接口(server/discover、resultType/ttlMs/cacheScope 等结果装饰字段、HTTP Mcp-Method/Mcp-Name/ MCP-Protocol-Version 请求头验证,以及通过 -32601 拒绝已移除的方法);旧版客户端的行为则逐字节保持 v0.5.0 不变。MRTR 桥接机制允许现代客户端回答上游在调用过程中提出的问题(elicitation/create、sampling/createMessage、roots/list),方法是使用 inputResponses 重试调用——输入窗口时长等于上游的 call_timeout(默认 30 s)。此次发布还带来一批安全改进:为 stdio 上游提供可选的 env_isolation: strict,始终将已知密钥值脱敏为 *** 并写入崩溃日志和日志记录,serve/doctor 会针对明文 HTTP 传输密钥发出警告,doctor 会提示文件权限过弱,并将每个 HTTP 会话的 GET SSE 流数量限制为 16。工具链升级至 Go 1.26.7,修复了已发布的 v0.5.0 二进制文件中存在的 34 个可触达标准库 CVE;发布产物还新增了每个归档文件对应的软件物料清单(SPDX JSON)和 cosign 无密钥签名。

升级到 v0.6.0——一项契约变更和两项有条件的变更:

  • 网关繁忙错误代码从 -32029 重新编号为 -32009。 速率限制/并发限制拦截仍会附带机器可读的 data: {"retryable":true,"reason":...},但任何硬编码旧数值代码的地方都必须更新:-32029 属于 2026-07-28 规范为协议错误保留的 -32020..-32099 范围。
  • HTTP 会话现在最多允许 16 个并发 GET SSE 流。 第 17 次尝试会在发送任何响应头之前收到 HTTP 503;已打开的流不会被逐出,关闭一个流即可释放名额。此变更只会影响泄漏流的客户端(例如重新连接时未关闭旧流的客户端)。
  • 新增可选配置标志 env_isolation: "strict"(全局)。默认行为不变——stdio 上游仍会继承网关的完整 环境。启用 strict 后,子进程只会获得最小化的跨平台基础环境(Unix 上的 PATH、HOME、TMPDIR;Windows 上的 SystemRoot、 PATHEXT、TEMP、TMP 等),以及自身的 env: 块,因此依赖继承变量的上游必须显式列出这些变量。
  • 仅添加功能,无需采取任何操作:已知密钥值会在崩溃日志的 stderr 尾部、日志记录的 err 字段和事件详情中脱敏为 *** (始终启用;短于 8 字节的值不会脱敏);serve 和 doctor 会警告密钥通过明文 HTTP 传输;如果配置文件或 --env-file 可被组用户或所有用户读取,doctor 会发出警告。旧版客户端不会出现其他行为变化——黄金测试确保字节级稳定性。

升级到 v0.5.0——有三项行为变更,但均不涉及配置文件格式本身:

  • 配置解析现在采用严格模式。 以前会被静默忽略的未知或拼写错误的顶层键或上游级键,现在会导致配置加载失败。请修正键名(错误信息会指出该键)或将其删除。
  • 若 auth_token: ${VAR} 中的 VAR 未设置,现在会拒绝启动,并指出该变量。此前它会静默变成空令牌——这会导致 HTTP 网关完全禁用 bearer 验证,且不发出任何警告。请设置该变量(或传入 --env-file),或者删除 auth_token,以明确选择不启用身份验证。
  • 调用日志(log_file / calls.jsonl)新增了第二种记录类型 "kind":"event",与现有调用记录并存。使用 v0.4.0 或更早版本的二进制文件读取 v0.5.0 日志时,事件行会显示为精简的 ERR 条目,而不会导致读取失败——读取日志时请使用与写入日志的二进制文件版本相同或更新的版本。

升级到 v0.4.0: 无需更改配置文件,但 HTTP 模式下有两项可观察到的行为变化——initialize 之后,POST /mcp 现在必须携带会话 ID(该请求头由 initialize 响应返回);上游注册表现在会在收到第一个实际 MCP 请求时延迟启动,而不是在进程启动时启动。

尚未实现: 按客户端设置访问策略。

发布

跨平台二进制文件通过 goreleaser 构建 (.goreleaser.yaml):linux/darwin/windows × amd64/arm64,不使用 CGO, 版本通过 -ldflags -X main.version=... 写入,校验和保存在 SHA256SUMS 中。本地试运行:goreleaser release --snapshot --clean。 从 v0.6.0 开始,每个归档还会附带一份 SBOM(SPDX 2.3 JSON),并且 SHA256SUMS 带有 cosign 无密钥签名(.sig + .pem);ghcr.io 镜像清单直接在注册表中签名(不存储密钥—— 身份通过 GitHub Actions OIDC 创建,并记录在 Rekor 中)。

从 MCP 注册表安装

除了原始发布二进制文件外,该网关还以 OCI 镜像的形式发布在 GitHub Container Registry 上,并提供 npm 包装包——这是 MCP 注册表安装所用的两种格式。

Docker:

docker run --rm -i -v $(pwd)/config.yaml:/config.yaml ghcr.io/akomyagin/aimcpgate serve

必须使用 -i:网关通过标准输入/输出与 MCP 通信,因此客户端必须保持标准输入打开(否则容器会收到 EOF 并立即退出)。镜像本身没有配置文件,因此请挂载自己的配置——上面的示例将其挂载到默认路径 /config.yaml;使用 serve -c 时也可以指定其他路径。

如需复现注册表沙箱检查(如 Glama.ai),且不连接任何真实上游服务,请使用镜像中内置的演示配置——沙箱应运行的命令如下:

docker run --rm -i ghcr.io/akomyagin/aimcpgate serve -c /demo.config.yaml

npx(首次安装时会下载适用于你平台的预构建二进制文件,并验证其 SHA256 校验和):

npx aimcpgate serve -c ./config.yaml

镜像策略: OCI 镜像仅包含 mcp-gate 二进制文件——不包含供标准输入/输出上游服务使用的运行时(没有 node/npx、python 或 shell)。如果你的配置会启动标准输入/输出上游服务,请自行扩展镜像并安装它们所需的组件;HTTP 上游服务开箱即用(镜像中包含 CA 证书)。

演示配置: demo.config.yaml 和隐藏的 __demo-echo 子命令仅用于让注册表沙箱(Glama.ai)在不连接任何真实上游服务的情况下检查网关——切勿在真实部署中使用。

在容器内运行 CLI 命令

doctor、catalog、call 和 logs 是运维人员检查部署的方式。在容器内调用这些命令时,有三点事实决定了正确的调用方式:

  1. 二进制文件位于 /mcp-gate,且不在 $PATH 中。 Dockerfile 执行 COPY mcp-gate /mcp-gate 和 ENTRYPOINT ["/mcp-gate"]——没有任何操作会将其加入搜索路径(如果这里看起来有问题,请检查 Dockerfile)。因此,最直观的写法会失败:

    $ docker exec mcp-gate mcp-gate catalog -c /config.yaml
    OCI runtime exec failed: exec failed: unable to start container process: exec: "mcp-gate": executable file not found in $PATH
    

    请改用绝对路径——这就是唯一的区别。

  2. 镜像是 distroless,因此完全没有 shell。 基础镜像是 gcr.io/distroless/static-debian12:nonroot,其中只有二进制文件和 CA 证书,没有其他内容。docker exec mcp-gate sh -c '…' 同样会失败,因为根本没有 sh,也没有 ls/cat 可供查看。管道、通配符和重定向都应放在命令的主机端。

  3. docker exec 会启动一个新进程;它不会查询正在运行的 serve。 doctor、catalog 和 call 会各自构建注册表、与上游建立自己的连接、报告结果并退出。因此,它们显示的是当前上游的可达性,而不是运行中网关的状态:如果运行中的进程失去了某个上游连接并将其从目录中移除,这些命令不会显示这一情况。它们也会保持调用日志干净——运行时会禁用日志记录,因此通过这种方式发起的 call 不会出现在 logs 中。

docker exec mcp-gate /mcp-gate version
docker exec mcp-gate /mcp-gate doctor  -c /config.yaml
docker exec mcp-gate /mcp-gate catalog -c /config.yaml
docker exec mcp-gate /mcp-gate call demo__echo '{"text":"hi"}' -c /config.yaml
docker exec mcp-gate /mcp-gate logs    -c /config.yaml --tail 50

这些命令假设容器是以分离模式启动并指定名称的,例如 docker run -d --name mcp-gate …——与上面的前台 docker run --rm -i … 示例不同,后者会在 stdio 客户端断开连接时立即退出,导致没有容器可供 docker exec 访问。配置文件假设挂载在默认路径 /config.yaml,与同一示例中的设置相同;demo__echo 代表你自己目录中的某个工具。请注意以下几点:

  • logs 是事实 3 的例外:它会读取运行中的网关写入的日志文件,因此反映的是实时进程状态。这要求挂载的配置中的 log_file 指向容器内可见的路径,并且在该路径挂载卷——否则日志会写入容器的 stderr(即 docker logs),而 mcp-gate logs 将无内容可读。-c 用于指定日志文件的位置;--file 可覆盖该设置。
  • 这实际上针对的是 HTTP 模式。 在 stdio 模式下,MCP 客户端会启动并管理容器,因此通常没有可供 exec 进入的长期运行容器。可供检查的网关是单独启动的(docker run -d --name mcp-gate …),并设置了 transport: http。
  • HTTP 模式需要设置非默认的 listen_addr。 默认值是 127.0.0.1:28080——这是容器内的环回地址,即使使用 -p,主机也无法访问。请在配置中设置 listen_addr: 0.0.0.0:<port>;此时网关会有意拒绝在未设置 auth_token 的情况下启动(“HTTP 端点将在未经身份验证的情况下从网络访问”)。

为什么

活跃的 MCP 用户通常会配置多个服务(filesystem、GitHub、搜索服务和自定义服务),而每个服务都要在每个客户端各自的配置中重复添加。aiMCPGate 为你提供:

  • 一个入口——一个 MCP 端点,而不是客户端配置中的 N 个条目。
  • 一个目录——合并所有上游服务的工具和提示词(命名空间格式为 <upstream>__<tool>,确保名称不会冲突),以及它们的资源和资源模板(通过 URI 访问,因此名称不会变更)。
  • 调用日志——记录上游、工具、时间以及成功或失败情况。这是它超越“只是一个代理”的附加价值。

个人业余项目:重点是学习 Go(并发、os/exec、JSON-RPC 2.0、stdio 和 HTTP/SSE 传输)。默认成本为 $0/月(本地进程),无遥测。

工作原理(简要)

MCP client ──stdio/HTTP──▶ aiMCPGate ──JSON-RPC──▶ upstream A (stdio)
                              │        ├─────────▶ upstream B (stdio)
                          call log     └─────────▶ upstream C (http, Phase 2)

MVP(两个阶段)

  • Phase 1 — 在一个 stdio 端点后复用 2 个或更多 stdio 上游(Claude Code 使用的同一种传输方式),并提供基本日志记录。
  • Phase 2 — HTTP/SSE 传输、HTTP 上游服务、日志查看器(已构建 CLI 版本;网页视图则被有意移除),以及可选的访问策略——该策略曾被考虑,但最终未采用。

构建

export PATH="$HOME/sdk/go/bin:$PATH"   # if go isn't already on PATH
go build ./...
go vet ./...
go test -race ./...

go run ./cmd version

用法

# stdio mode (the client launches the gateway as a subprocess):
mcp-gate serve --config ./config.yaml

# http mode (transport: http in the config) — endpoint at http://<listen_addr>/mcp;
# every request after initialize carries the issued Mcp-Session-Id (see below):
mcp-gate serve --config ./config-http.yaml

# check every enabled upstream once (launch → handshake → tools/list) and print
# a per-upstream OK/FAIL table; exit code is non-zero if any upstream failed
# (scriptable for CI/cron), no auto-restart, no call logging — one pass then exit.
# It also WARNs (POSIX only) if config.yaml or --env-file is readable by
# group/others — either may hold a secret literal, and nothing else checks:
mcp-gate doctor --config ./config.yaml

# call one aggregated tool once from the shell (single bring-up, no supervisor —
# the fastest way to debug a config, a filter or a rename without a live client):
mcp-gate call github__search_repositories '{"query":"mcp"}' --config ./config.yaml

# report the aggregated catalog size per upstream (tools / bytes / ~tokens) plus
# the heaviest individual tools — the data behind allow-list / strip decisions:
mcp-gate catalog --config ./config.yaml --top 20

# view the journal — tool calls AND operator events (last 50 lines; filter by
# upstream/tool/status):
mcp-gate logs --file ./logs/calls.jsonl --tail 50
mcp-gate logs --config ./config.yaml --upstream github --status err
# show ONLY the operator events (see "Operator events" below):
mcp-gate logs --config ./config.yaml --events
# keep watching the log as it grows, or aggregate it instead of listing records
# (--follow and --stats are mutually exclusive):
mcp-gate logs --config ./config.yaml --follow
mcp-gate logs --config ./config.yaml --stats

# generate a random auth token (for the HTTP transport) and see how to wire it in:
mcp-gate token --generate
# print the auth token currently set in the config:
mcp-gate token --config ./config-http.yaml

# print ready-to-paste MCP client config snippets (Claude Code / Cursor / Claude
# Desktop) for whichever transport the config uses: a launch command for stdio, or
# the endpoint URL plus the Bearer header (when auth_token is set) for http:
mcp-gate client-config --config ./config.yaml

# print a SKILL.md teaching an agent how to use the aggregated catalog
# (built-in text by default; overridable via skill_file in the config):
mcp-gate skill > .claude/skills/mcp-gate/SKILL.md

# shell completions (cobra's built-in command; the release archives also ship
# pre-generated ones):
mcp-gate completion bash > /etc/bash_completion.d/mcp-gate

除 token --generate、completion 和 skill(会回退到内置指南)之外,所有命令都会加载配置:传入 --config,或将 config.yaml 放在二进制文件旁边(参见下文的配置)。

serve、doctor、call 和 catalog 还接受 --env-file ./.env——这是一个最简的 KEY=VALUE 解析器,会在加载配置之前应用,因此配置中的 ${VAR} 引用可以从该文件解析。实际进程环境变量始终优先于文件中的值。

HTTP 会话(Mcp-Session-Id)

在 http 模式下,网关运行 Streamable HTTP 会话:对 initialize 的响应会携带 Mcp-Session-Id 标头,此后的每个请求——POST、GET SSE 流、DELETE——都必须带回该标头。没有该标头时,响应为 400;如果标识符未知或已过期,则响应为 404,这会告知客户端重新执行 initialize。会话可通过 DELETE /mcp(204)释放,或在 30 分钟没有请求后自动释放——打开的 SSE 流也算作活动,会使会话保持有效。

MCP 客户端会为你处理这些操作。对于手动编写的 curl 调用,请从 initialize 响应中获取该标头并在请求中回传:

SID=$(curl -sD - -o /dev/null -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}' \
  | tr -d '\r' | awk -F': ' '/^[Mm]cp-[Ss]ession-[Ii]d/{print $2}')

curl -s -X POST http://127.0.0.1:28080/mcp \
  -H 'Content-Type: application/json' -H "Mcp-Session-Id: $SID" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

curl -s -X DELETE http://127.0.0.1:28080/mcp -H "Mcp-Session-Id: $SID"

会话也能确保调用日志准确:每次调用都会记录在发起调用的会话的 clientInfo 下,因此 calls.jsonl 中可以区分多个 HTTP 客户端,而不是让它们共用一个空白的 client 字段。

通过 HTTP 发送从服务端到客户端的请求(elicitation、sampling、roots)

当上游在调用过程中提出问题时——elicitation/create、 sampling/createMessage、roots/list——问题会作为 SSE 事件发送到某个会话的 GET /mcp 流中,客户端则发送一个普通 POST 请求,携带 JSON-RPC 响应,并使用相同的 id 和相同的 Mcp-Session-Id 作答。只有收到问题的会话可以作答;来自任何其他会话的回答都会被忽略。如果没有任何已声明该能力且保持流打开的会话,上游会立即收到规范规定格式的拒绝响应(对 elicitation 返回 {"action":"decline"},对另外两项返回 -32601),而不会一直等待超时;如果会话在问题尚未答复时终止,也会发生同样的情况。

有三点值得了解:

  • 上游获知的是第一个完成初始化的客户端所声明的能力,且在进程生命周期内保持不变。MCP 2025-06-18 不支持重新协商,因此,第二个客户端即使声明了更多能力,也无法改变已经完成的握手——绝不会向上游承诺客户端未声明的能力。
  • 上游会在第一个需要它们的请求到来时启动,而不是在网关绑定端口时启动。只有这样才能实现上述能力声明:握手必须在客户端说明自身支持的能力之后进行。如果上游无法启动,客户端会收到 JSON-RPC -32603,网关也会报错退出,就像以前在启动时立即启动上游那样。
  • 问题会发送给声明了该能力的客户端,但不一定是触发该问题的调用所属的客户端。 路由依据已声明的能力;在符合条件的会话中,最近活动的会话优先。上游请求中不包含任何信息来表明它属于哪个调用。只有一个客户端时(这是常见情况),这一点不会显现;但如果有两个客户端,一个客户端的 tools/call 所触发的表单可能会显示在另一个客户端的界面中。

同一交互中 上游 一侧也通过 HTTP 工作:通过 url: 连接的远程 MCP 服务可以将问题作为 SSE 帧发送——既可以发到它的长连接 GET 流中,也可以插入到为网关自身某个 POST 提供响应的流中;SDK 服务会在这里发送由 tools/call 内部触发的 elicitation/create。网关会通过相同的处理流程代理该请求,并将客户端的回答作为一个普通 POST 发回,携带使用 服务端自身请求 id 的 JSON-RPC 响应。网关会以同样如实的策略告知这类上游其客户端的能力:只有网关自己的客户端声明了某项能力,才会提供该能力;没有客户端的 doctor/call/catalog 则仍会准确地声明 {}。回答 POST 不会重试:上游如果没有收到,就会依靠自身的超时机制处理。

日志中的运维事件

log_file 中保存着两种行:一种对应工具调用,另一种对应运维事件——这是网关状态,否则你永远不会知道。在 stdio 模式下,MCP 客户端占用终端,因此你看不到网关的 stderr,而且其中一些情况以前只会以 debug 级别记录。现在,它们会写入 mcp-gate logs 读取的同一个文件:

事件含义
upstream_start_failed某个上游从未成功启动;其工具不会出现在目录中。
upstream_gave_up监督程序停止重启某个上游(重试次数耗尽、重新加载时禁用了重启,或没有存活检测通道),并将其从目录中移除。
notification_dropped某个订阅者的缓冲区已满,因此转发的通知被丢弃——转发在设计上不会阻塞。
server_request_dropped某个上游请求只有客户端才能回答的内容(elicitation/sampling/roots),但没有传输通道接收该请求,因此代替客户端拒绝了工具调用。
sse_stream_unavailable某个 HTTP 上游不提供 GET SSE 流,因此在网关重启前,它发出的 tools/list_changed 通知都不会到达。
catalog_collision两个条目声明了相同的、面向客户端的工具/提示词名称或资源 URI;优先保留先出现的条目,另一个对客户端不可见。
catalog_bad_template某个资源 URI 模板无法编译:它会列出给客户端,但读取时永远无法匹配。
result_truncation_skipped某个结果超出 max_result_bytes,但不含可截断的文本(例如只有图像),因此会完整传递。

事件会与调用记录一起显示,并标记为 EVT;mcp-gate logs --events 只显示事件,--stats 则会增加一张按事件分类的表格。--tool 和 --status 只筛选调用,因此只要设置其中任一筛选器,事件就会被排除(--upstream 同时适用于二者)。有一点值得注意:notification_dropped 不会标明上游——丢弃是缓冲区已满的订阅者所导致,而不是发送通知的一方所致——因此 --upstream X 永远不会显示它。请勿使用该筛选器来查找此事件。重复的丢弃事件会合并——首次丢弃会立即写入;一分钟内后续的丢弃会计入该键的下一行中的 count=,剩余事件则会在关闭时写入。包含此类积压计数的行会在其 detail= 中说明,并标明合并的最早事件的发生时间——该行自身的时间戳是最新时间,因此二者共同限定了这次突发事件实际发生的时间范围。

两点实用说明:

  • 设置 log_file。 留空时,日志会写入 stderr;在 stdio 模式下,stderr 归 MCP 客户端所有——事件会写入你看不到的位置。
  • 使用写入日志的同一版本或更新版本的二进制文件读取日志。 事件包含较早版本不识别的 "kind" 字段,因此 ≤ v0.4.0 版本的 mcp-gate logs 会将它们显示为内容稀少、基本为空的记录。

MCP 客户端看不到任何变化:错误代码、结果内容或能力都没有改变——这些事件只会写入日志。

网关无法路由的调用不是事件——它是一条普通的失败 CALL 行。 如果客户端请求的工具名称没有任何上游提供,网关会像处理其他调用一样生成一条 CallRecord,其 upstream 列设为哨兵值 (unrouted);mcp-gate logs --upstream '(unrouted)' 只会筛选出这些行。另有一种不同的情况看起来几乎一样,但会标明实际的上游:路由存在(该工具在目录中),但上游连接已断开(正在重启或已被移除)——这类行会带有实际的上游名称,因此应像往常一样使用 --upstream <name> 进行筛选,而不是使用哨兵值。

重新加载配置(SIGHUP)

网关会在收到 SIGHUP 时实时重新加载配置,无需重启,也不会断开客户端连接。编辑 config.yaml 并发送信号:

kill -HUP $(pgrep -f 'mcp-gate serve')

重新加载时,网关会将新配置与正在运行的上游进行比较,并应用最小改动:启动新增的上游,关闭已移除(或 enabled: false)的上游,重新启动启动字段(command/args/url/env/headers)发生变化的上游;如果上游仅工具筛选器(allow/deny/rename)或目录投影规则(strip_annotations/strip_output_schema/max_description/describe)发生变化,则会重新投影,无需重启。调用限制(rate_limit、max_concurrent、max_result_bytes、call_timeout——全局或按上游设置)也会实时应用:它们永远不需要重新启动,上一次调用后,下一次调用便会使用新值。未发生变化的上游会继续运行,不受影响。如果编辑有误(YAML 无效、验证失败),网关会记录日志并忽略该编辑——当前运行中的配置会继续生效,因此拼写错误绝不会导致网关停止运行。

**行为说明:**由于网关安装了 SIGHUP 处理程序,SIGHUP 不再像操作系统默认行为那样终止进程。要停止网关,请使用 Ctrl-C、SIGINT 或 SIGTERM。

SIGHUP 仅适用于 Unix。在 Windows 上,或任何不便发送信号的环境中,可以改用可选的轮询方式:

mcp-gate serve --config ./config.yaml --watch-config        # bare flag = poll every 2s
mcp-gate serve --config ./config.yaml --watch-config=10s    # note the "=", not a space

它会按该间隔为配置文件生成指纹,并采用与 SIGHUP 相同的重新加载流程。与 SIGHUP 处理程序同时运行也是安全的。

监视器会比较文件的 mtime 和大小,并等待下一次轮询时指纹保持一致后再读取文件。因此,即使分两步保存(先截断,再写入),实际使用中也很安全:只有写入方让文件处于半写入状态超过一个完整轮询间隔,才能骗过检查。代价是延迟——重新加载最多会在两个轮询间隔内生效(默认间隔为 2s 时,最长为 4s)。

对于 stdio,上游会在客户端发出首次请求时启动,因此在任何客户端连接之前所做的编辑暂时无法应用。监视器会保留该编辑,并在每次轮询时重试,直到网关启动后再应用——无需再次保存文件即可使更改生效。如果编辑因永久性问题被拒绝(YAML 无法解析,或触发下文的无 upstreams 保护),网关会报告一次,且不再重试。

作为对两种触发方式的保护,如果重新加载的新配置完全没有声明 upstreams,网关会拒绝并记录日志:这表明文件可能只写了一半,而应用这种配置会关闭所有正在运行的上游。若要有意移除所有上游,请重启网关。显式设置 enabled: false 不受影响——禁用最后一个上游仍会生效。

配置

如果未指定 --config,网关会在自身二进制文件所在目录查找 config.yaml(例如,如果 mcp-gate 安装在 /etc/gate/,就会查找 /etc/gate/config.yaml——与启动时所在的工作目录无关)。如果该文件不存在,且也未传入 --config,网关会明确报错,而不是以空配置启动。配置中的相对路径(log_file、skill_file、debug_payload_log)相对于配置文件所在目录解析,而不是相对于当前工作目录。

未知键会导致启动错误。配置会被严格解析:拼写错误或无法识别的键会连同键名和行号一起导致网关停止,而不再像以前那样被静默忽略。实际好处是:enabled 中的拼写错误不再会导致上游悄然保持运行。自定义的 x- 临时键也会被拒绝——若要共享配置块,请在第一个实际的上游配置中添加 YAML 锚点,并将其合并(<<: *anchor)到其他配置中;锚点和合并键照常可用。 上游默认已启用:完全省略 enabled:,它就会像其他上游一样启动。如果不想从网关中移除某个上游,但又不想删除其配置,请显式将其禁用,即设置为 enabled: false——此后它既不会出现在 tools/list 中,也不会出现在 mcp-gate doctor 的表格中。请注意:没有值的 enabled:(或 enabled: null)会被视为省略,因此注释掉该值会让上游继续运行——只有字面值 false 才会禁用它。

注意:查找“二进制文件旁边”的配置时,会使用正在运行的可执行文件的路径。 使用 go run ./cmd ... 时,该可执行文件是临时目录中的一次性构建,因此默认查找不会找到你的 config.yaml——使用 go run 时请显式传入 --config,或运行已构建的二进制文件。

包含所有字段的完整示例——config.example.yaml。 上游服务列表在 YAML 中声明;密钥(令牌)通过 env/.env 传入(加载时展开 ${VAR}),绝不会提交到配置文件中。每个上游只能设置 command(stdio 子进程)或 url(HTTP 服务,Streamable HTTP)中的一个——连接类型会自动推断。

未设置的 ${VAR} 引用在不同字段中的处理方式不同:

  • auth_token 引用未设置的变量时,启动会失败,并指出变量名称——空的 auth_token 会静默禁用 HTTP bearer 验证,因此绝不会悄无声息地发生这种情况。若要在不使用身份验证的情况下运行,请完全移除 auth_token 键。
  • 上游的 env/headers 中存在未设置的变量时,不会报错:其值会变为空,缺失的密钥会在之后以该上游返回的 401 错误暴露出来。网关会提前报告此情况——日志记录中会出现 unresolved_secret_var 事件(mcp-gate logs),mcp-gate doctor 中会出现一行 WARN。
  • 在 stdio 模式下,mcp-gate client-config 会发出警告(写入 stderr),说明操作员的环境变量不会被 MCP 客户端继承;MCP 客户端会在自己的环境中启动网关——请在客户端运行网关的环境中设置这些变量。

默认情况下,每个 stdio 上游都会继承网关进程的完整环境,包括通过 --env-file 为其他上游加载的密钥和 auth_token。设置全局选项 env_isolation: strict 后,每个 stdio 上游只会收到最小基础环境(PATH、HOME、TMPDIR 以及 Windows 对应项),再加上其自身的 env: 块;这些变量始终会传递给子进程。如果你从开放注册表运行上游(例如运行不完全可信的 npx 包),建议使用此设置。若确实希望严格模式下的上游看到某个网关变量,请在其 env: 中显式列出(例如 PYTHONPATH: ${PYTHONPATH})。 变量值会在启动时读取:运行中的上游会一直保留启动时的环境,直到它被(重新)启动。

此外,以下机制会始终启用:网关展开 ${VAR} 密钥后,就会知道其确切值,并会在面向操作员的自由文本中将该值脱敏为 ***——包括日志记录(mcp-gate logs)和崩溃日志。未配置密钥时,此机制不会带来任何开销。

如果密钥通过未加密的纯 HTTP 发送到非环回主机,启动时会出现 WARN,且 mcp-gate doctor 中会显示一行提示(不会导致硬性失败——例如通过 VPN 或隔离 LAN 在可信基础设施上传输明文,可能是有意为之的选择,仅凭配置网关无法判断):绑定到环回地址之外的非空 auth_token,或发送到非环回 url(且不是 https://)的上游 headers。网关本身不提供 TLS——如果需要 TLS,请在前面放置一个负责终止 TLS 的反向代理,这与之前为双向 TLS 建议的做法相同。

transport: stdio            # stdio (Phase 1) | http (Phase 2)
listen_addr: "127.0.0.1:28080"  # only used for transport: http; loopback by default
# auth_token: ${AIMCPGATE_TOKEN}  # required if you widen listen_addr past loopback;
#                                 # the variable must be set or startup fails
log_file: ./logs/calls.jsonl
# debug_payload_log: ./logs/payloads.jsonl  # OPT-IN, off by default: logs raw
#                                   # arguments AND results — can contain secrets
# Optional global call limits (each can be overridden per upstream):
# rate_limit: { rps: 5, burst: 2 }  # token bucket per upstream for tools/call
#                                   # (refusal → client error -32009, retryable)
# max_result_bytes: 65536           # truncate oversized textual results (0 = off;
#                                   # non-text over-limit results get a _meta marker)
# call_timeout: 30s                 # bounds one upstream request
# How the catalog is presented to the client (both hot-reloadable):
# catalog_mode: lazy                # normal (default) | lazy: the client sees only
#                                   # gate_search_tools / gate_describe / gate_call
# page_size: 50                     # paginate tools/list (0/omitted = whole catalog;
#                                   # ignored in lazy mode)
# Auto-restart policy for crashed stdio upstreams (defaults: on, 1s→30s, 5 tries):
# restart: { enabled: true, initial_backoff: 1s, max_backoff: 30s, max_attempts: 5 }
# env_isolation: strict             # opt-in ("" default = inherit the gateway's full
#                                   # env): a stdio upstream then gets only a minimal
#                                   # base (PATH/HOME/...) plus its own env: block —
#                                   # NOT other upstreams' secrets or the auth token
upstreams:
  - name: filesystem        # stdio upstream
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "/home/user"]
    enabled: true
  - name: github
    command: github-mcp-server
    env:
      GITHUB_TOKEN: ${GITHUB_TOKEN}   # from the environment, not hardcoded
    enabled: true
    # Optional per-upstream tool filter / catalog projection (keys are ORIGINAL
    # tool names; all editable live via SIGHUP with no upstream restart):
    # tools:
    #   allow: ["search_repositories"]  # if non-empty, only these survive
    #   deny: ["delete_repository"]     # always subtracted, even from allow
    #   rename: { search_repositories: "gh_search" }
    #   strip_annotations: true         # drop heavyweight catalog fields
    #   strip_output_schema: true
    #   max_description: 200            # truncate descriptions to N runes
    #   describe: { get_issue: "Fetch one issue." }   # replace wholesale
    # Optional per-upstream call limits (override the globals for this upstream):
    # rate_limit: { rps: 1, burst: 1 }  # rps: 0 disables the global limit here
    #                                   # (refusal → client error -32009, retryable)
    # max_concurrent: 4                 # cap on simultaneous in-flight calls
    #                                   # (refusal → client error -32009, retryable)
    # max_result_bytes: 32768           # 0 disables the global cap here
    # call_timeout: 120s                # this upstream is slow — give it longer
  - name: remote            # http upstream (Phase 2)
    url: https://mcp.example.com/mcp
    headers:
      Authorization: "Bearer ${REMOTE_MCP_TOKEN}"   # secret, never logged
    enabled: true
调用触及限额时客户端会看到什么

上述两种调用限额会向 MCP 客户端(Agent)反馈,而不只是记录在运维日志中:

  • 守卫拒绝(rate_limit / max_concurrent)。 当网关因上游级速率限制器或并发上限无法接纳请求,而拒绝 tools/call 时,客户端会收到 JSON-RPC 错误,其中包含网关自己的错误码 -32009,以及机器可读的 data: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"}。调用尚未到达上游,因此 Agent 可以等待后重试,无需担心重复执行。普通传输/路由故障仍使用历史错误码 -32603;上游自身返回的错误则会原样转发,错误码和数据均不变——上游返回的 -32009 并不代表网关信号。
  • 无法截断的超大结果(max_result_bytes)。 文本结果会缩减,并在内容中加入 [truncated by mcp-gate: …] 标记。若非文本/非标准结果超出限额,但没有可截断的文本(例如只有图像),则会完整、逐字节原样传递——其 content[] 绝不会被修改——但结果的 _meta 会新增网关键 io.github.akomyagin.aimcpgate/result-over-limit,其值为 {"limitBytes": N, "resultBytes": M},以便 Agent 判断限额已被绕过。不认识该键的客户端只会忽略它。运维日志中的 result_truncation_skipped 事件仍会照常触发。

许可证

MIT — 详见 LICENSE。

评论

支持 Markdown

第一次接触 MCP?

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

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