Skip to main content
Glama

aiMCPGate

Русская версия — README_RU.md

一个用 Go 编写的 MCP 服务器(模型上下文协议)网关/代理。 它向 MCP 客户端(Claude Code、Cursor 等)呈现为一个 MCP 服务器,而在底层,它多路复用多个上游 MCP 服务器的调用,将它们的工具、提示词和资源聚合到一个目录中,并记录每次调用。

状态:MVP 已完成(阶段 0–6)+ MVP 后阶段 7–18 已发布,最新版本 v0.5.0。 第一阶段——在 stdio 端点后面多路复用 stdio 上游,并带有调用日志;第二阶段——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/createsampling/createMessageroots/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 错误代码 -32029,带有机器可读的 data: {"retryable":true,"reason":...},而不是无法区分的 -32603,而绕过 max_result_bytes 的非文本结果带有 result._meta 标记(content 保持逐字节不变)。最后,引用未设置环境变量的 auth_token 现在拒绝启动网关,而不是静默禁用 HTTP 认证。

升级到 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

Related MCP server: mcpstead

从 MCP 注册表安装

除了原始发布二进制之外,网关还以 GitHub Container Registry 上的 OCI 镜像和 npm 包装包的形式发布——这是 MCP 注册表安装的两种格式。

Docker:

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

-i 是必需的:网关通过 stdio 进行 MCP 通信,因此客户端必须保持 stdin 打开(没有它,容器会看到 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 二进制——没有 stdio 上游的运行时(没有 node/npx、python、shell)。如果你的配置启动 stdio 上游服务器,请自行扩展镜像并安装它们所需的内容;HTTP 上游开箱即用(包含 CA 证书)。

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

在容器内运行 CLI 命令

doctorcatalogcalllogs 是运维人员检查部署的方式。三个事实决定了它们在容器内必须如何调用:

  1. 二进制是 /mcp-gate,并且不在 $PATH 中。 Dockerfile 执行 COPY mcp-gate /mcp-gateENTRYPOINT ["/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 可以用来查看。将管道、通配符和重定向保留在命令的 HOST 侧。

  3. docker exec 启动一个新进程;它不会查询正在运行的 serve doctorcatalogcall 构建自己的注册表,打开自己的上游连接,报告并退出。因此它们的输出是上游当前的可达性,而不是活动网关的状态:如果正在运行的进程丢失了一个上游并将其从目录中删除,这些命令不会显示这一点。它们也保持调用日志干净——它们在禁用日志记录的情况下运行,因此你以这种方式进行的 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 用户通常配置了几个服务器(文件系统、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(两个阶段)

  • 第一阶段——在一个 stdio 端点(Claude Code 所说的同一种传输)后面多路复用 2+ 个 stdio 上游,外加基本日志记录。

  • 第二阶段——HTTP/SSE 传输、HTTP 上游服务器、日志查看器(CLI 版已构建;Web 视图被有意放弃)、可选访问策略——那个被考虑过但被拒绝了

构建

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 --generatecompletionskill(回退到内置指南)之外的所有命令都会加载配置:传递 --config,或者在二进制旁边放一个 config.yaml(见下面的配置)。

servedoctorcallcatalog 也接受 --env-file ./.env——一个在加载配置之前应用的最小 KEY=VALUE 解析器,因此配置中的 ${VAR} 引用会从该文件解析。真实的进程环境总是优先于文件。

HTTP 会话(Mcp-Session-Id

在 http 模式下,网关运行 Streamable HTTP 会话:对 initialize 的回复带有 Mcp-Session-Id 头部,之后的每个请求——POST、GET SSE 流、DELETE——都必须将该头部发回。没有它,答案是 400;使用未知或过期的 ID,则是 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 下审计,因此多个 HTTP 客户端在 calls.jsonl 中被区分开来,而不是共享一个空白的 client 字段。

通过 HTTP 的服务器→客户端请求(elicitationsamplingroots

当上游在通话中提出请求时——elicitation/createsampling/createMessageroots/list——该问题会作为 SSE 事件在某个会话的 GET /mcp上送达,客户端则通过一个普通的 POST 携带具有相同 id 和相同 Mcp-Session-Id 的 JSON-RPC 响应来作答。只有被提问的那个会话才能回答;来自任何其他会话的答案都会被忽略。如果没有任何人声明该能力且没有打开的流,上游会立即以规范规定的方式被拒绝(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 携带 JSON-RPC 响应以服务器自己的请求 id 发送回去。这样的上游被告知网关的客户端能力时,采用与 stdio 上游相同的诚实策略——只有当网关自己的客户端声明了某项能力时才会提供该能力,而 doctor/call/catalog 根本没有客户端,它们继续声明恰好为 {}。答案 POST 不会重试:没有收到它的上游会回退到自己的超时机制。

日志中的操作者事件

log_file 处的日志包含两种行:每次工具调用一行,每次操作者事件一行——后者是你在其他情况下永远不会了解到的网关状态。在 stdio 模式下,MCP 客户端拥有终端,因此网关的 stderr 对你不可见,而且这些条件中的若干此前只在调试级别记录。现在它们被写入 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 但没有可截断的文本(例如仅包含图像),因此它完整通过。

事件与调用一起内联显示,标记为 EVTmcp-gate logs --events 只显示它们,--stats 增加了一个按事件分类的表。--tool--status 是仅限调用的过滤器,因此设置其中任何一个时事件都会被排除(--upstream 对两者都适用)。一个值得了解的后果:notification_dropped 不指明任何上游——丢弃是缓冲区已满的订阅者的属性,而不是发送通知者的属性——因此 --upstream X 永远不会显示它。在没有该过滤器的情况下查找它。重复的丢弃会被合并——第一次立即写入,一分钟内的后续丢弃被计入该键的下一行的 count= 中,剩余部分在关闭时刷新。携带此类积压的行会在其 detail= 中说明,指明其折叠的最早发生时间——该行自身的时间戳是最新的,因此两者共同界定了突发实际发生的时间范围。

两个实用说明:

  • 设置 log_file 如果为空,日志会写入 stderr,在 stdio 模式下这属于 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_limitmax_concurrentmax_result_bytescall_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 处理器并行运行是安全的。

监视器比较文件的修改时间和大小,并等待该指纹在下一个周期重复后才读取文件。这就是两步保存(先截断,再填充)在实践中安全的原因:写入者必须将文件保持在半写入状态超过一个完整的轮询间隔才能骗过检查。代价是延迟——重载最多在两个轮询间隔内完成(默认 2 秒时最多 4 秒)。

在 stdio 模式下,上游在客户端的第一个请求到来时启动,因此在任何客户端连接之前所做的编辑还无法应用。监视器保留该编辑并在每次轮询时重试,直到网关启动,然后应用它——你永远不必第二次保存文件来使其生效。因正当理由被拒绝的编辑(无法解析的 YAML,或下面的无 upstreams 保护)会被报告一次且不再重试。

作为两种触发方式的兜底,重载时新配置未声明任何 upstreams 会被拒绝并记录:这是半写入文件的特征,应用它会导致所有正在运行的上游被拆除。要故意移除所有上游,请重启网关。显式的 enabled: false 不受影响——禁用最后一个上游仍然会应用。

配置

如果没有 --config,网关会在其自身二进制文件旁边查找 config.yaml(例如,如果 mcp-gate 安装在 /etc/gate/,它会查找 /etc/gate/config.yaml——无论它从哪个工作目录启动)。如果该文件不存在且也未传递 --config,它会显式报错而不是启动一个空网关。配置中的相对路径(log_fileskill_filedebug_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 形式显现。网关会提前报告 — 日志(mcp-gate logs)中的 unresolved_secret_var 事件以及 mcp-gate doctor 中的 WARN 行。

  • 在 stdio 模式下,mcp-gate client-config 会(在 stderr 上)警告:操作员的环境变量不会被 MCP 客户端继承,客户端在其自身环境中启动网关 — 请在客户端运行网关的位置设置这些变量。

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

另外,此功能始终开启:一旦网关展开了一个 ${VAR} 机密,它就知道了确切的值,并会在面向操作员的自由文本中将该值编辑为 *** — 包括日志(mcp-gate logs)和崩溃日志。未配置机密时,此功能不产生任何开销。

机密通过明文、未加密的 HTTP 传输到非回环主机时,会在启动时产生 WARN 并在 mcp-gate doctor 中输出一行(绝不会是硬性失败 — 在可信基础设施(如 VPN 或隔离的 LAN)上使用明文可能是有意为之,网关无法仅凭配置做出判断):非空的 auth_token 绑定到回环之外,或上游的 headers 发送到非 https:// 的非回环 url。网关本身不内置 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 -32029, 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 -32029, retryable)
    # max_concurrent: 4                 # cap on simultaneous in-flight calls
    #                                   # (refusal → client error -32029, 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 客户端(代理),而不仅仅是操作员日志:

  • 守卫拒绝(rate_limit / max_concurrent)。 当网关因每上游速率限制器或并发上限无法接纳而拒绝 tools/call 时,客户端会收到一个 JSON-RPC 错误,包含网关自身的代码 -32029 和机器可读的 data: {"retryable": true, "reason": "rate_limit" | "concurrency_limit"}。该调用从未到达上游,因此代理可以等待并重试,而无需担心重复执行。普通的传输/路由故障保留原有的 -32603,而上游自身返回的错误则原样转发,代码和数据均不修改 — 来自上游的 -32029 不是网关信号。

  • 无法截断的超大结果(max_result_bytes)。 文本结果会通过内容内的 [truncated by mcp-gate: …] 标记进行缩减。超过限制但没有可截断文本的非文本/非标准结果(例如仅包含图像)会完整地、逐字节地通过 — 其 content[] 绝不会被修改 — 但结果的 _meta 会获得网关键 io.github.akomyagin.aimcpgate/result-over-limit,值为 {"limitBytes": N, "resultBytes": M},以便代理能够判断限制已被绕过。不认识该键的客户端会直接忽略它。操作员的 result_truncation_skipped 日志事件仍照常触发。

许可证

MIT — 参见 LICENSE

Available Tools

2 tools
demo-echo__echoB

Echo the given text back verbatim.

ParametersJSON Schema
NameRequiredDescriptionDefault
textYesText to echo back verbatim.

TDQS

B3.4/5.0
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations exist, so the description bears full responsibility. It merely repeats the parameter description ('Echo the given text back verbatim') without adding behavioral details such as side effects, error handling, or output format.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with no extraneous information. Every word serves the purpose.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity (one required parameter, no output schema), the description is largely complete. It could explicitly mention the return format, but the behavior is trivially inferable.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema coverage is 100%, so the baseline is 3. The description adds no new meaning beyond the parameter's description, which already states 'Text to echo back verbatim.'

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

Description clearly states the action ('Echo') and the resource ('the given text back verbatim'), making the purpose unmistakable. The sibling tool 'ping' likely serves a different function (connectivity test), so this description distinguishes well.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance is provided on when to use this tool versus the sibling 'ping' or any alternatives. The description simply states what it does without contextual usage advice.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

demo-echo__pingA

Health check: always returns "pong".

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations provided, but the description fully discloses the output ('always returns pong'), leaving no ambiguity about its behavior for a simple ping tool.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

One concise sentence that is front-loaded with purpose and output, no unnecessary words.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

For a simple health check tool with no parameters and no output schema, the description is complete and sufficient for an agent to use correctly.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters4/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

No parameters in the schema; description adds no parameter info, but baseline for 0-param tools is 4.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states it is a health check that always returns 'pong', which is specific and distinguishes it from the sibling tool 'echo'.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines4/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies usage for health checks, and with only one sibling ('echo'), the purpose is clear without explicit alternatives. Slightly lacking explicit when-not-to-use guidance.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

TDQS

A4.1/5.0
Disambiguation5/5

The two tools serve completely different purposes: echo returns input text, ping returns a fixed health response. No ambiguity.

Naming Consistency5/5

Both tools follow the same pattern: prefix 'demo-echo__' followed by a verb. The naming is uniform and predictable.

Tool Count4/5

With only 2 tools, the set is minimal but appropriate for a simple demonstration server. It covers the core functionality without being overly sparse.

Completeness5/5

For the stated purpose of a demo echo server, the tools provide exactly what is needed: echo text and health check. No obvious gaps.

Maintenance

ActivityMaintained
ResponsivenessSyncing

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Local-first MCP proxy with BM25 tool discovery, quarantine security, Docker isolation, OAuth support, activity logging, and web UI. Routes multiple upstream MCP servers through a single endpoint.
    9
    332
    MIT
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP Gateway that aggregates multiple upstream MCP servers into a single endpoint with persistent connections, tool registry, and authentication.
    11
    2
    MIT
  • F
    license
    A
    quality
    D
    maintenance
    Universal MCP proxy server that discovers, searches, and executes tools across all configured MCP servers from a single entry point.
    7
  • A
    license
    A
    quality
    A
    maintenance
    A zero-dependency MCP gateway: host your own tools, forward and curate tools from other MCP servers, expose them leanly to cut agent token cost, and gate every call through your own policy hooks before it runs.
    2
    4
    25
    35
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/akomyagin/aiMCPGate'

If you have feedback or need assistance with the MCP directory API, please join our Discord server