mcpField
M
MissingMCP

MissingMCP

远程服务
missingmcp.com数据与分析17今天更新

关于 MissingMCP

MissingMCP 是面向 Claude 用户的托管多用户网关,通过 OAuth 将 Garmin Connect 和 WHOOP 数据接入远程 MCP 服务。它分别以独立 worker 和 WHOOP 官方 API 提供连接,并隔离、加密账户令牌。

主要功能

  1. 01OAuth 2.1 身份验证
  2. 02连接 Garmin Connect 与 WHOOP
  3. 03按用户隔离账户令牌
  4. 04加密存储令牌
  5. 05支持加密数据库备份

适用场景

  1. 01在 Claude 中查询 Garmin Connect 数据
  2. 02查看 WHOOP 恢复、睡眠和训练数据
  3. 03为多个用户托管健康数据连接

MCP 相关

提供可通过 HTTP 连接的远程 MCP 服务,含 `/garmin/mcp` 与 `/whoop/mcp` 端点。使用 OAuth 登录后,MCP 客户端可调用对应服务的工具。

配置

远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。

服务地址https://missingmcp.com/garmin/mcp
一键复制安装命令
Claude CodeTerminal
Cursor.cursor/mcp.json
VS Code.vscode/mcp.json
Claude Desktopclaude_desktop_config.json
Windsurf~/.codeium/windsurf/mcp_config.json

命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。

工具

详细介绍

MissingMCP

MCP 服务已经存在。连接它们不应该如此复杂。

smithery badge

一个受 OAuth 2.1 保护的多用户网关,托管 Claude 所缺少的连接器,让一小群受信任的用户可以从任何 Claude 客户端(iOS、Android、Web、Desktop)连接自己的账户。旗舰连接器是 Garmin Connect:网关封装了未经修改的 garmin_mcp 工作进程,并添加 OAuth、按用户隔离令牌和反向代理。WHOOP 也以相同方式提供服务,但在进程内通过 WHOOP 自有的官方 OAuth v2 API 实现。核心采用适配器架构,支持三种转发策略——worker(garmin)、local(whoop,在进程内运行,无子进程)和 remote(MCP + 请求头注入,适用于托管了 MCP 但没有自带 OAuth 的服务)——请参阅连接器。参考部署运行于 missingmcp.com。

Claude → POST /garmin/mcp (Bearer) → Gateway → 127.0.0.1:<port>/mcp (per-user garmin_mcp) → connect.garmin.com

为什么

garmin_mcp 是一个出色的 MCP 服务,但它是单用户且仅支持 stdio:每个人都必须在本地运行它,并使用自己的 Garmin 令牌。此网关将它变成一个远程 MCP 服务,任何 Claude 客户端都可以通过 HTTP 连接,并提供完善的 OAuth 登录流程——非技术用户只需点击“连接”并使用 Garmin 凭据登录,无需接触终端或令牌文件。

功能

  • OAuth 2.1 — 授权码 + PKCE(S256),支持动态客户端注册。可从任何 Claude 客户端连接;无需手动处理令牌。
  • 绝不存储 Garmin 密码 — 密码仅用于登录一次(支持 MFA);只会持久化由此获得的会话令牌。
  • 静态加密 — 使用 AES-256-GCM 封装令牌;没有 GATEWAY_SECRET,数据库就毫无用处。Bearer 令牌仅以 SHA-256 哈希形式存储。
  • 按用户隔离 — 每个账户都有独立的 garmin_mcp 工作进程,绑定到 127.0.0.1,按需启动,空闲时回收。
  • 强化安全 — 一次性、有效期 10 分钟的授权码,表单 CSRF 防护,按 IP/令牌限流,garmin_mcp 工作进程固定到经过审查的提交。
  • 异机备份 — 定期将加密的 SQLite 快照备份到兼容 S3 的存储桶(请参阅备份)。
  • 在 / 提供说明性落地页,并为未知路径提供友好的回退页面。

部署

Railway(missingmcp.com 的运行方式): 从 Dockerfile 构建一个服务,将卷挂载到 /data,设置单个副本(网关按设计使用进程本地状态),并在 Railway 边缘终止 TLS。在服务上设置配置中的环境变量;连接 GitHub 仓库后,推送到 main 会自动部署。可选的 Railway 存储桶可启用备份(运行 railway bucket create backups,然后将凭据配置为 BACKUP_S3_* 变量)。

自托管(普通 Docker):

cp .env.example .env          # set GATEWAY_SECRET, PUBLIC_URL, pin GARMIN_MCP_REF
docker build -t missingmcp .
docker run -d --name missingmcp --restart unless-stopped --env-file .env \
  -p 127.0.0.1:8080:8080 -v missingmcp-data:/data missingmcp

在前面配置任意 TLS 终止代理(Caddy、nginx、Traefik)。有一项不太明显的要求:/<adapter>/mcp 会以 SSE 流式传输,因此需要禁用响应缓冲并延长读取超时时间(nginx:proxy_buffering off; proxy_read_timeout 3600s;)。然后在 Claude 中将 https://<your-domain>/garmin/mcp 添加为远程 MCP 服务。

本地开发

uv pip install -e ".[dev]"
uv run --extra dev pytest -q                 # run the test suite

# Run the gateway locally (no Garmin account needed to exercise the OAuth surface).
# garmin-mcp isn't on PATH locally, so point GARMIN_MCP_CMD at uvx.
GATEWAY_SECRET="$(openssl rand -base64 48)" \
PUBLIC_URL=http://localhost:8088 PORT=8088 DATA_DIR=./.localdata \
GARMIN_MCP_CMD="uvx --python 3.12 --from git+https://github.com/Taxuspt/garmin_mcp garmin-mcp" \
  uv run missingmcp

工作目录中的 .env 文件会自动加载(真实环境变量优先),因此你也可以将相同的值写入该文件。

连接器

每个连接器都挂载在各自的路径前缀下,并拥有自己的 OAuth 流程和 Bearer 令牌(不存在裸路径 /mcp)。/ 上的落地页会列出可用连接器。

Garmin — /garmin/mcp

使用 Garmin Connect 邮箱和密码登录(支持 MFA)。密码仅用于 Garmin 登录一次,随后即被丢弃;只会存储生成的 Garmin 会话令牌(使用 AES-256-GCM 加密)。每个账号都会获得独立的 garmin_mcp 工作进程,绑定到 127.0.0.1,按需启动,并在空闲时回收。

WHOOP — /whoop/mcp

在 WHOOP 自己的 OAuth 页面上登录——网关永远不会看到你的 WHOOP 密码,只会接收生成的(加密、只读)令牌。支持恢复、睡眠、负荷和每日周期、锻炼及身体测量数据。通过 WHOOP 官方 v2 API 在进程内提供服务(无工作进程子进程,也不共享上游)。运营者必须注册 WHOOP 开发者应用——请参阅“配置”中的 WHOOP 连接器设置。

Rohlík — 不再缺失

网关曾短暂提供 Rohlík 连接器(远程策略:将凭据标头注入托管的 Rohlík MCP)。2026-07,Rohlík 发布了自己的 OAuth 保护型 MCP,该连接器随之退役——直接在 Claude 中将 https://mcp.rohlik.cz/mcp 添加为自定义连接器,并在浏览器中登录(参阅 Rohlík 的指南)。远程转发策略仍是核心功能的一部分,并经过测试(tests/test_remote_forward.py),可供下一个需要此功能的服务使用。

从 Claude 连接

  1. 在任何 Claude 客户端中:设置 → 连接器 → 添加自定义连接器;或在 CLI 中运行:claude mcp add --transport http garmin https://<your-domain>/garmin/mcp (whoop:claude mcp add --transport http whoop https://<your-domain>/whoop/mcp)。
  2. Claude 会打开网关的登录页面——输入该服务的邮箱和密码(Garmin 会在需要时提示输入 MFA 代码);对于 WHOOP,则在 WHOOP 自己的 OAuth 页面上登录。
  3. 完成——现在即可在 Claude 中使用该服务的工具。

配置

通过环境变量(或 .env)设置。请参阅 .env.example。

变量必需默认值描述
GATEWAY_SECRET是—用于令牌加密的密钥,长度至少为 32 个字符。若仍使用占位符,则拒绝启动。使用 openssl rand -base64 48 生成。
PUBLIC_URL是http://localhost:8080用于 OAuth 元数据和重定向的公共 URL。
PORT否8080监听端口。
DATA_DIR否/dataSQLite 数据库和每位用户的令牌目录所在位置。
DB_PATH否$DATA_DIR/gateway.db覆盖数据库路径。
GARMIN_MCP_CMD否garmin-mcp用于启动工作进程的命令。若 garmin-mcp 不在 PATH 中,请使用 uvx … 调用。
GARMIN_MCP_REF否Dockerfile 中固定的 SHADocker 构建参数:要安装的 garmin_mcp 提交。更新此值属于有意且经过审核的操作;之后运行 python scripts/gen_garmin_tools.py,以刷新 /garmin 页面的工具列表。
WORKER_PORT_START / WORKER_PORT_END否9000 / 9099每位用户的工作进程端口范围。
WORKER_IDLE_TTL否900空闲工作进程被回收前等待的秒数。
WORKER_STARTUP_TIMEOUT否20等待工作进程就绪的秒数。
MAX_WORKERS否10每位用户可并发运行的工作进程上限。
GARMIN_MCP_STATELESS_HTTP否true运行 garmin 工作进程的 Streamable HTTP 时不使用 MCP 会话(明确传递给每个工作进程)。这样,替换后的工作进程(因驱逐、空闲回收、崩溃或部署而替换)仍会为客户端旧的 Mcp-Session-Id 提供服务,而不是返回 404「未找到会话」;网关本身会对 /garmin/mcp 上的 GET/DELETE 请求返回 405。false 是回滚开关(工作进程使用会话,转发 GET/DELETE 请求)。接受 true/false、1/0、yes/no、on/off(不区分大小写;空值 = 默认值);其他任何值都会导致网关在启动时停止。
SSO_PROBE_INTERVAL否0(关闭)对每个登录出口,每隔多少秒使用浏览器模拟的 GET 请求探测一次 Garmin 的 SSO 嵌入式组件(事件 sso-probe,字段 via、egress_ip——出口的实际出站 IP,通过 cdn-cgi/trace 获取;此外,每个出口还有 egress-health:自上次运行以来的实际登录结果,因为 GET 请求无法检测被拦截的凭据 POST;当出口的 IP 发生变化时记录 egress-ip-changed,间隔至少 60 秒)——用于诊断基于出口 IP 的速率限制的、独立于登录尝试的拦截时间线(可靠性工单 12)。
GARMIN_SSO_PROXY否—(直连)以逗号分隔的代理 URL(http://user:pass@host:port),使用专用 IP 处理网关自身的 Garmin 流量——登录、MFA、令牌验证——而不使用 Garmin SSO 会施加速率限制的共享主机出口(工单 12)。每个账户固定使用一个代理(根据账户哈希选择)。登录节流(failing-logins 映射):每个登录邮箱同时只允许一次正在进行的登录(并发登录会被拒绝);成功建立连接后等待 2 分钟;被拦截的登录只会让对应账户冷却 5 分钟,调用过程中不会重试;如果某个出口上的至少 2 个不同账户均被拦截,且期间没有任何登录成功,或者一小时内被拦截至少 10 次,则该出口的断路器会跳闸,并依次休眠 30 分钟 → 2 小时 → 6 小时 → 24 小时(有登录成功后恢复为 30 分钟)。若出口断路器跳闸后一直没有登录成功,则该出口为可疑状态:只要再次发生一次拦截,就会进入下一级休眠时长。若某次登录被拦截并导致其出口断路器跳闸,只要首次尝试耗时少于 35 秒,就会在同一次调用中通过下一个路由重试一次;重试时若被拦截,只会让对应账户冷却。休眠级别、休眠结束时间、出口 IP,以及最近一次成功/拦截时间会持久化到 egress_state 表中,因此部署不会重置已被限流的 IP;探测发现某个路由从新的 IP 发出请求时,其状态会重置(在 hydra 上运行 sudo mmcp egress 可查看)。若代理完全无法承载请求(端口连接被拒绝、CONNECT 被拒绝——curl 返回 CONNECT tunnel failed / (5)/(7)/(97);Garmin 未收到请求),结果记为 egress,而非 blocked:账户不会进入冷却状态,出口休眠 5 分钟,并在同一次调用中将同一登录请求切换到下一个路由;只有所有路由都不可用时,用户才会看到「我们这边暂时无法提供服务」的消息。直连会保留为最后的备用方式(仅在所有代理都处于冷却状态时使用)。通过代理登录时会跳过移动端 SSO 策略(即使使用干净 IP,数据中心指纹也会导致 429)。工作进程和其他所有流量仍使用直连。事件:garmin-login-attempt(account、egress、egress_ip、outcome、message)、login-breaker-open / login-breaker-reject(scope:account
ACCESS_TOKEN_TTL_DAYS否90Bearer 令牌的有效期;到期后用户需要重新验证身份。0 表示禁用过期机制。
OPERATOR_NAME / OPERATOR_EMAIL否—显示在着陆页上。
OPERATOR_URL否—运营者名称的链接目标(页脚、信任说明)。未设置时显示为纯文本。
WHOOP_CLIENT_ID / WHOOP_CLIENT_SECRET否—WHOOP 开发者应用的凭据(参见 WHOOP 连接器设置);两项均未设置 ⇒ whoop 连接器将被禁用。
WHOOP_API_BASE否https://api.prod.whoop.comWHOOP API 源;仅用于测试时才覆盖。
BACKUP_S3_ENDPOINT / BACKUP_S3_BUCKET / BACKUP_S3_ACCESS_KEY / BACKUP_S3_SECRET_KEY否—用于将数据库备份到外部设备的 S3 兼容存储桶(参见备份)。除非四项均已设置,否则备份功能会被禁用。
BACKUP_S3_REGION否auto存储桶的 SigV4 区域。
BACKUP_S3_URL_STYLE否virtual-hostvirtual-host(Railway 存储桶)或 path。
BACKUP_INTERVAL_HOURS否6备份上传的间隔小时数。首次备份会在启动后立即运行。
POSTHOG_API_KEY否—公开的 phc_ 项目密钥。未设置 ⇒ 所有 PostHog 遥测数据(事件、日志转发、Web 分析)均关闭。
POSTHOG_LOGS否falsetrue 表示通过 OTLP 将每一行结构化标准输出日志转发到 PostHog Logs。默认关闭,原因是截至 2026-10-19,每月 2,500 WAU 会产生超过 4,300 万行(约 9 GB)日志,增加 PostHog 账单;而 Railway 本身也会保留日志。仅接受 true/false。
POSTHOG_HOST否https://eu.i.posthog.com服务端数据接收主机(SDK 事件和 OTLP 日志转发)。
POSTHOG_WEB_HOST否$POSTHOG_HOSTposthog-js 的 api_host——可将其设置为托管的反向代理(例如 https://j.missingmcp.com),以增强抵御广告拦截器的能力。
POSTHOG_UI_HOST否https://eu.posthog.comPostHog 应用主机;若 POSTHOG_WEB_HOST 是代理,posthog-js 需要此项。
MAIL_API_TOKEN否—用于邮件营销活动的 Cloudflare API 令牌——权限范围仅限 Account → Email Sending → Edit。除非已设置此项、MAIL_ACCOUNT_ID 和 MAIL_FROM,否则营销活动会被禁用。
MAIL_ACCOUNT_ID否—发件域名所在的 Cloudflare 账户 ID。
MAIL_FROM否—已接入 Cloudflare Email Sending 的(子)域名上的发件人地址,例如 [email protected]。
MAIL_FROM_NAME否—发件人显示名称。
MAIL_REPLY_TO否—回复邮件的接收地址;也是 List-Unsubscribe 标头中 mailto: 的目标地址。
MAIL_DAILY_CAP否180滚动 24 小时内营销邮件的最大发送量(Cloudflare 的配额窗口)。请保持低于 Cloudflare 账户的每日配额(新账户为 200,由所有发件域名共享)。
GATEWAY_LOG_FILE否—若已设置,则将结构化日志和标准库日志同时写入此文件。
GATEWAY_LOG_LEVEL否infodebug|info|warning|error|critical。debug 会输出大量日志(包括 garminconnect/urllib3 内部日志),生产环境中应避免使用。
WHOOP 连接器设置
  1. 在 https://developer-dashboard.whoop.com 创建应用(可即时自助完成)。
  2. 重定向 URI:https://<your-domain>/whoop/oauth/callback — 必须完全匹配。
  3. 权限范围:read:recovery read:cycles read:workout read:sleep read:profile read:body_measurement offline (必须包含 offline — 否则 WHOOP 不会发放刷新令牌,会话会在一小时后失效)。
  4. 将应用的 Client ID/Secret 填入 WHOOP_CLIENT_ID / WHOOP_CLIENT_SECRET。

注意:未经批准的 WHOOP 应用最多只能供 10 位 WHOOP 会员使用。若要解除限制,请提交应用以供审批:https://developer.whoop.com/docs/developing/app-approval/ (要求:遵守 API 使用条款、 至少使用 1 位会员完成测试、在控制面板中填写准确的应用名称 / 联系邮箱 / 隐私政策 URL、遵守品牌指南, 并提交他们的 Typeform 表单)。

WHOOP API 使用条款规定的运营义务(网关的设计已涵盖技术层面的要求 — 不存储 WHOOP 数据、令牌加密、 自动清除已撤销账户):

  • 如发生涉及 WHOOP 会员数据的安全事件,须在无不当延迟的情况下向 [email protected] 和受影响用户报告。
  • 未经 WHOOP 事先书面批准,不得发布提及 WHOOP 的新闻稿或公开公告。
  • 不要将应用的 Client ID/Secret 存入代码仓库(仅使用环境变量),也不得 将其用于其他应用。
  • 当会员在 WHOOP 撤销应用授权后,网关会在下次刷新时检测到这一情况 (invalid_grant),删除已存储的令牌,并撤销该会员的网关访问令牌 (日志事件 whoop-account-revoked)。

备份

设置 BACKUP_S3_* 变量后,网关会在启动后立即将一致的 SQLite 快照(通过 SQLite 备份 API,支持 WAL)上传到存储桶,之后每隔 BACKUP_INTERVAL_HOURS 上传一次。备份键按星期几轮换 (db/gateway-mon.db … db/gateway-sun.db),无需清理逻辑即可保留七天。 请留意 backup-ok / backup-failed 日志事件。

仅备份数据库 — DATA_DIR/users/ 下每位用户的令牌目录会按需 根据数据库重新生成。

没有 GATEWAY_SECRET,备份就毫无用处(其中的账户数据仍以 AES-256-GCM 加密)— 这正是设计目的。请将 GATEWAY_SECRET 的副本保存在 存储桶和 Railway 之外的地方(例如密码管理器)。丢失该密钥 = 丢失所有账户。

**恢复:**下载最新的对象,将其放到 $DATA_DIR/gateway.db (删除其旁边任何过期的 gateway.db-wal / -shm),设置相同的 GATEWAY_SECRET,然后启动网关。Bearer 令牌和登录信息会保留; 工作进程会按需重新启动。

监控

三个辅助脚本可直接操作网关数据库(网关运行期间也可安全执行):

python scripts/status.py          # summary counts only (safe to paste/share)
python scripts/status.py --detail # + per-account devices (token prefixes),
                                  #   usage summary, OAuth clients, running workers
python scripts/revoke.py --list                       # accounts + token counts
python scripts/revoke.py --account [<adapter>:]<key>  # kill-switch: revoke ALL the
                                                      #   account's tokens (bare key = garmin)
python scripts/revoke.py --account <key> --purge      # + delete stored account & usage
python scripts/revoke.py --device <hash-prefix>       # revoke ONE device (prefix from status.py)
python scripts/usage.py                               # per-account tool usage + leaderboard
python scripts/usage.py --account [<adapter>:]<key>   # one account's per-tool breakdown
python scripts/subscribers.py                         # newsletter signups + suggestions
python scripts/subscribers.py --emails                # subscriber emails, one per line
python scripts/daily_report.py                        # yesterday's new/active/total users (print)
python scripts/daily_report.py --post                 # + POST it to Slack ($SLACK_WEBHOOK_URL)
python scripts/add_beer.py --email <supporter>        # record a "buy me a beer" donation (1 beer, 5 EUR)
python scripts/add_beer.py --email <supporter> --beers 3 --at 2026-07-20  # 3 beers, backdated

scripts/session_loss.py 则处理 Railway 日志(railway logs --json 输出,详见其文档字符串):garmin 调用因 MCP 会话丢失(404)而失败的比例,分别统计整体情况和工作进程启动后 5 秒内的情况 — 用于衡量 GARMIN_MCP_STATELESS_HTTP 启用前后的差异。

如果设置了 SLACK_WEBHOOK_URL,网关还会在每天早晨自行向 Slack 发送用户统计报告(DAILY_REPORT_HOUR,默认时间为 DAILY_REPORT_TZ 的 08:00); 测试时可运行 scripts/daily_report.py` 按需生成同一份报告。

邮件活动 — 通过 Cloudflare Email Sending(上文的 MAIL_* 环境变量) 向用户和新闻邮件订阅者发送的一次性公告。运营者创建并启动活动; 网关会自行处理,每分钟发送一批,每天最多发送 MAIL_DAILY_CAP 封,并优先发送给最活跃的用户 — 列表较长时,只需分几天发送即可。

# text lives in the repo: campaigns/<slug>.txt ("Subject: ..." line, blank line, body)
python scripts/campaign.py create <slug>                  # snapshot text + enqueue everyone not unsubscribed
python scripts/campaign.py create <slug> --audience users # or: subscribers
python scripts/campaign.py test <slug> --to <you>         # one real send, not recorded
python scripts/campaign.py start <slug>                   # the gateway starts sending
python scripts/campaign.py pause <slug>
python scripts/campaign.py status [<slug>]                # counts + sent in last 24h + ETA (no addresses)
python scripts/campaign.py status <slug> --unknown        # + addresses whose send is unknown
python scripts/campaign.py requeue-unknown <slug>         # decided to send those again
python scripts/campaign.py requeue-failed <slug>          # retry the failed ones (attempts reset)
python scripts/campaign.py unsubscribe <email>            # manual opt-out (e.g. from a reply)

每位收件人都会记录在发送台账中,因此不会有人重复收到同一营销邮件:在调用 API 之前,发送状态会记录为 sending;如果发生崩溃或 API 错误含糊不清(超时、5xx),状态会设为 unknown,留待操作员处理,而不是盲目重试(并计入每日上限)。如果某个地址被拒绝(400/422),会对该地址重试最多 3 次,不会拖慢其他地址;如果是账户级错误(令牌无效、配额、速率限制),则不会发送给任何人,并暂停邮件发送器 15 分钟。每次发送前都会检查并移除已取消订阅的地址,包括重新排队的邮件。每封邮件都附带退订链接和符合 RFC 8058 标准的一键退订 List-Unsubscribe 标头(/unsubscribe — GET 仅确认,POST 执行退订);退订对所有营销邮件生效,并会从每个营销邮件活动中移除该地址。如果硬退信率超过 5%(发送 50 封后判定),营销邮件活动会自动暂停。进度仅通过日志事件报告 — campaign-batch、campaign-daily-cap、campaign-auto-paused、campaign-done、campaign-send-unknown、campaign-run-failed — 绝不会通过邮件报告。

每日分类 — GitHub Actions 工作流(.github/workflows/daily-triage.yml)每天早晨(布拉格时间约 07:45)运行 scripts/daily_triage.py:它从网关的 Railway 日志中提取过去 24 小时的错误/警告记录,以确定性方式对已知错误特征进行分类,并且 — 仅当仍有可采取行动的问题时 — 让 Claude 编写操作员的分类报告(按类别说明发生了什么 → 意味着什么 → 建议采取什么行动),并发布到 Slack。运行正常的日期会发布一行“all quiet”(每日心跳);如果分析失败,则改为发布确定性汇总表 — 绝不会保持沉默。分析通过无头 Claude Code 使用操作员的 Claude 订阅运行(仓库密钥 CLAUDE_CODE_OAUTH_TOKEN,通过 claude setup-token 创建);ANTHROPIC_API_KEY 密钥可用作备用后端。还需要 RAILWAY_API_TOKEN + SLACK_WEBHOOK_URL(小时级告警器已配置)。手动运行:python scripts/daily_triage.py --dry-run(需要 RAILWAY_* 环境变量;没有令牌时,分析步骤会优雅降级)。

每小时硬信号告警器 — GitHub Actions 工作流(.github/workflows/hourly-digest.yml)每小时运行 scripts/hourly_digest.py:它通过 Railway API 读取网关过去 60 分钟的日志,执行存活探测,并且仅在出现硬信号时发送告警(<!here>)— 探测失败(站点宕机)、一小时内至少 2 个不同账户出现 worker-start-failed(损坏图像特征)、任何 5xx、任何 critical,或者某个登录出口在一小时内的所有 sso-probe(至少 2 次)都连接失败(需要 SSO_PROBE_INTERVAL ≤ 1800 s;通过独立的日志筛选器获取)。除此之外完全静默 — 普通错误数量由每日分类处理。需要仓库密钥 RAILWAY_API_TOKEN + SLACK_WEBHOOK_URL(服务/环境 ID 已设置为工作流环境变量)。RAILWAY_API_TOKEN 可以是 Railway 项目令牌(权限范围最窄 — 只能访问此项目;通过 Project-Access-Token 标头发送),也可以是账户/工作区令牌(通过 Authorization: Bearer 发送);脚本会尝试两种方式。手动运行:python scripts/hourly_digest.py --dry-run(需要环境变量 RAILWAY_API_TOKEN、RAILWAY_SERVICE_ID、RAILWAY_ENVIRONMENT_ID)。 PostHog 遥测——设置 POSTHOG_API_KEY 后(见环境变量表),网关也会向 PostHog(欧盟云;设计文档:docs/superpowers/specs/2026-07-20-posthog-telemetry-design.md)发送分析数据:每个请求对应的 $mcp_* 事件(PostHog 内置的 MCP 分析)、精简的连接漏斗/转化事件集、将结构化日志流通过 OTLP 同步至 PostHog Logs(仅在设置 POSTHOG_LOGS=true 时启用——Railway 仍是持久化归档),以及网站上的 posthog-js(UTM 活动归因;OAuth 页面已禁用自动采集)。数据传出规则:仅限身份信息和元数据——绝不包含 MCP 正文、凭据或表单内容;账户邮箱仅作为 distinct_id 传输。所有操作均为发送后即忘:PostHog 故障绝不会阻塞请求。上述 Slack 报告照常运行——PostHog 是对它们的补充。将用户停用(revoke.py --purge)时,也要在 PostHog 中删除该用户(People → delete),以完成 GDPR 流程。

啤酒支持者——scripts/add_beer.py 会将“请我喝杯啤酒”捐款(buymeacoffee.com/venik)记录到本地 beers 审计表,并发送 beer_purchased PostHog 事件;如果邮箱是已知登录邮箱,则会尽力将事件关联到支持者的网关账户(matched)。此事件旨在用于运营者的连接→付费漏斗,以及 Growth 仪表板上的“本月啤酒数”指标——这些 PostHog 洞察需要手动设置,在创建之前并不存在。目前需手动录入(BMC 自动化暂缓;设计文档:docs/superpowers/specs/2026-07-24-beer-supporters.md)。数据传出规则与上文相同——邮箱仅作为 distinct_id 传输,绝不传输支持者姓名或备注。

使用 Docker 时,脚本会打包到镜像的 /app/scripts 中;请在容器内运行。status.py 会自动在 /data 下查找数据库:

docker exec missingmcp python /app/scripts/status.py
docker logs -f missingmcp                   # live structured-JSON events

在 Railway 上,通过 railway ssh 运行脚本;日志位于 Railway 仪表板中(使用 railway logs --service gateway 实时查看):

railway ssh --service gateway "python3 /app/scripts/status.py"
railway ssh --service gateway "python3 /app/scripts/revoke.py --account <email>"

所有日志均以结构化 JSON 输出到 stdout(每行一个事件),并包含规范的 level 属性——包括 uvicorn/标准库日志记录(事件 stdlib-log)以及各个工作进程自身的输出(事件 worker-log,包含 account 属性;匹配 ERROR/Traceback 的行会提升为 error 级别)。在 Railway 上,这样就能在日志浏览器中搜索所有内容——例如使用 @event:worker-log @account:<email> 筛选以追踪某个用户的工作进程,或使用 @level:error 查找问题。每次调用的请求延迟都记录在 mcp-response 事件中(ttfb_ms、total_ms、bytes、tool、account);登录/验证和工作进程启动事件会记录 ms 时长。网关还会在启动时以及相关计数发生变化时记录 stats 事件(账户/令牌/持有令牌的用户/客户端/活跃工作进程),status.py 则会列出正在运行的工作进程。

自动数据清理

网关会自行维护数据库整洁,无需手动清理。除了让旧授权码和访问令牌过期外,后台循环还会(大约每分钟一次):

  • 清理弃置的 OAuth 客户端——Claude 每次尝试连接时都会注册一个新客户端(DCR),而未完成流程的尝试会留下一个没有令牌的客户端。任何没有访问令牌且创建时间超过一小时的客户端都会被删除(这个时间远超授权码的 10 分钟有效期,因此不会影响正在进行的登录)。这就是为什么 status.py 中的 OAuth 客户端数量会与已连接设备数量相对应,而不是随着每次失败的尝试不断增长。撤销设备也会使其客户端不再持有令牌,因此会以相同方式清理。
  • 清除已停用适配器的数据——如果某个连接器被移除,其所有行(账户、令牌、客户端、授权码、用量)都会被全面删除。已停用连接器列表在代码中明确列出,绝不会根据配置推断,因此缺少环境变量不会意外清除仍在使用的连接器。

只有实际删除内容时才会记录日志——cleanup-orphan-clients(count)和 cleanup-dead-adapter(adapter + 各表计数)——因此,日志安静就表示没有内容需要清理。一个小时的阈值是固定常量,按设计并非环境变量。

工作原理

  1. Claude 注册客户端(DCR)并启动 OAuth 2.1(Authorization Code + PKCE)。
  2. 用户在授权页面使用 Garmin 登录(电子邮件 + 密码;如果提示,还需进行 MFA)。网关通过 garminconnect 登录,仅存储生成的令牌(加密),并丢弃密码。
  3. Claude 使用授权码换取 Bearer 令牌。
  4. 每次调用 /garmin/mcp 时,网关都会确保用户的 garmin_mcp 工作进程正在运行(使用其自身的令牌,绑定到 127.0.0.1),然后通过反向代理转发请求。

使用远程策略的适配器会将步骤 2 和 4 替换为针对上游 MCP 的探测验证,以及直接注入标头的转发——无需工作进程。

使用上游 OAuth 登录的适配器(whoop)会将步骤 2 替换为重定向到提供方自己的 OAuth 页面;提供方会携带令牌回调,网关以相同方式验证并持久化令牌(先验证再持久化的流程保持不变)。使用本地策略的适配器(whoop)会替换步骤 4:请求在进程内处理——无需工作进程,也不共享上游;网关会根据需要自行刷新账户不断轮换的 WHOOP 令牌。

安全性

  • Garmin 密码绝不会持久化;网关甚至不会接触 WHOOP 密码(登录在 WHOOP 自己的 OAuth 页面上完成,且仅请求只读作用域)。
  • 静态存储的令牌经过加密(AES-256-GCM);没有 GATEWAY_SECRET,数据库就毫无用处。
  • 如果成员在 WHOOP 撤销应用授权,系统会在下次刷新时检测到,并自动清除其存储的令牌。
  • Bearer 令牌仅以 SHA-256 哈希形式存储。
  • OAuth 2.1 PKCE(S256)、一次性 10 分钟授权码、表单 CSRF 防护、按 IP 和令牌实施速率限制。
  • 工作进程仅绑定到 127.0.0.1;garmin_mcp 固定在经过审查的提交版本上。

仅在你控制并信任的基础设施上部署。备份 DATA_DIR;将 GATEWAY_SECRET 单独保管。

部署前

  • 设置一个真正随机的 GATEWAY_SECRET(openssl rand -base64 48)——应用 会拒绝使用 .env.example 中的占位值启动。
  • 将 GARMIN_MCP_REF 固定到经过审核的提交 SHA——main 是一个浮动引用,可能 在未提前通知的情况下发生变化(供应链风险)。
  • 撤销访问权限——访问令牌会在 ACCESS_TOKEN_TTL_DAYS 后过期(默认为 90;用户只需在 Claude 中重新进行身份验证)。如需提前撤销——例如令牌泄露 或用户被移除——运行 python scripts/revoke.py --account [<adapter>:]<email>(可撤销 该账户的所有令牌)。可使用 --device <hash-prefix> 撤销单台设备(前缀可通过 status.py 查看)。
  • 在连接真实用户之前,使用真实 Garmin 账户进行一次手动端到端冒烟测试(包括 MFA 流程)——自动化测试中使用的是模拟的上游服务。

支持

如果这个网关对你有帮助,可以请我喝杯啤酒 🍺。

许可证

MIT © 2026 Vaclav Slajs

致谢

基于 Taxuspt 出色的 garmin_mcp 封装, 未作修改。Garmin 和 Garmin Connect 是 Garmin Ltd. 的商标;本项目与 Garmin 无关联,也未获得 Garmin 的认可。

评论

支持 Markdown

第一次接触 MCP?

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

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