关于 WHOOP — MissingMCP
WHOOP — MissingMCP 将 WHOOP 健康数据通过 OAuth 接入 Claude,面向希望在 MCP 客户端查看恢复、睡眠、训练等信息的用户。它在进程内使用 WHOOP 官方 v2 API,并以加密的只读令牌访问数据。
主要功能
- 01通过 WHOOP 官方 OAuth 登录
- 02查看恢复与睡眠数据
- 03查看应变、日周期与训练
- 04读取身体测量数据
- 05加密保存只读令牌
适用场景
- 01在 Claude 中查看 WHOOP 恢复情况
- 02回顾睡眠、应变和每日周期
- 03检查训练与身体测量记录
MCP 相关
提供 `/whoop/mcp` 远程 MCP 服务,客户端通过 OAuth 登录后即可使用 WHOOP 数据工具。该连接器在进程内调用 WHOOP 官方 v2 API。
配置
远程服务无需本地安装。选择你使用的客户端,复制配置或使用一键安装。
https://missingmcp.com/whoop/mcpclaude mcp add --transport http com-missingmcp-whoop https://missingmcp.com/whoop/mcp{"mcpServers":{"com-missingmcp-whoop":{"type":"http","url":"https://missingmcp.com/whoop/mcp"}}}在 Cursor 中打开{"servers":{"com-missingmcp-whoop":{"type":"http","url":"https://missingmcp.com/whoop/mcp"}}}在 VS Code 中打开{"mcpServers":{"com-missingmcp-whoop":{"type":"http","url":"https://missingmcp.com/whoop/mcp"}}}{"mcpServers":{"com-missingmcp-whoop":{"serverUrl":"https://missingmcp.com/whoop/mcp"}}}命令在终端中运行;JSON 粘贴到对应客户端的 MCP 配置后重启。需要授权的服务会在首次连接时提示登录。
工具
详细介绍
MissingMCP
MCP 服务已经存在。连接它们不应该如此复杂。
一个受 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 连接
- 在任何 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)。 - Claude 会打开网关的登录页面——输入该服务的邮箱和密码(Garmin 会在需要时提示输入 MFA 代码);对于 WHOOP,则在 WHOOP 自己的 OAuth 页面上登录。
- 完成——现在即可在 Claude 中使用该服务的工具。
配置
通过环境变量(或 .env)设置。请参阅 .env.example。
| 变量 | 必需 | 默认值 | 描述 |
|---|---|---|---|
GATEWAY_SECRET | 是 | — | 用于令牌加密的密钥,长度至少为 32 个字符。若仍使用占位符,则拒绝启动。使用 openssl rand -base64 48 生成。 |
PUBLIC_URL | 是 | http://localhost:8080 | 用于 OAuth 元数据和重定向的公共 URL。 |
PORT | 否 | 8080 | 监听端口。 |
DATA_DIR | 否 | /data | SQLite 数据库和每位用户的令牌目录所在位置。 |
DB_PATH | 否 | $DATA_DIR/gateway.db | 覆盖数据库路径。 |
GARMIN_MCP_CMD | 否 | garmin-mcp | 用于启动工作进程的命令。若 garmin-mcp 不在 PATH 中,请使用 uvx … 调用。 |
GARMIN_MCP_REF | 否 | Dockerfile 中固定的 SHA | Docker 构建参数:要安装的 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 | 否 | 90 | Bearer 令牌的有效期;到期后用户需要重新验证身份。0 表示禁用过期机制。 |
OPERATOR_NAME / OPERATOR_EMAIL | 否 | — | 显示在着陆页上。 |
OPERATOR_URL | 否 | — | 运营者名称的链接目标(页脚、信任说明)。未设置时显示为纯文本。 |
WHOOP_CLIENT_ID / WHOOP_CLIENT_SECRET | 否 | — | WHOOP 开发者应用的凭据(参见 WHOOP 连接器设置);两项均未设置 ⇒ whoop 连接器将被禁用。 |
WHOOP_API_BASE | 否 | https://api.prod.whoop.com | WHOOP 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-host | virtual-host(Railway 存储桶)或 path。 |
BACKUP_INTERVAL_HOURS | 否 | 6 | 备份上传的间隔小时数。首次备份会在启动后立即运行。 |
POSTHOG_API_KEY | 否 | — | 公开的 phc_ 项目密钥。未设置 ⇒ 所有 PostHog 遥测数据(事件、日志转发、Web 分析)均关闭。 |
POSTHOG_LOGS | 否 | false | true 表示通过 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_HOST | posthog-js 的 api_host——可将其设置为托管的反向代理(例如 https://j.missingmcp.com),以增强抵御广告拦截器的能力。 |
POSTHOG_UI_HOST | 否 | https://eu.posthog.com | PostHog 应用主机;若 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 | 否 | info | debug|info|warning|error|critical。debug 会输出大量日志(包括 garminconnect/urllib3 内部日志),生产环境中应避免使用。 |
WHOOP 连接器设置
- 在 https://developer-dashboard.whoop.com 创建应用(可即时自助完成)。
- 重定向 URI:
https://<your-domain>/whoop/oauth/callback— 必须完全匹配。 - 权限范围:
read:recovery read:cycles read:workout read:sleep read:profile read:body_measurement offline(必须包含offline— 否则 WHOOP 不会发放刷新令牌,会话会在一小时后失效)。 - 将应用的 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 + 各表计数)——因此,日志安静就表示没有内容需要清理。一个小时的阈值是固定常量,按设计并非环境变量。
工作原理
- Claude 注册客户端(DCR)并启动 OAuth 2.1(Authorization Code + PKCE)。
- 用户在授权页面使用 Garmin 登录(电子邮件 + 密码;如果提示,还需进行 MFA)。网关通过
garminconnect登录,仅存储生成的令牌(加密),并丢弃密码。 - Claude 使用授权码换取 Bearer 令牌。
- 每次调用
/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 的认可。
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。