mcpField
W
WHOOP — MissingMCP

WHOOP — MissingMCP

远程服务
missingmcp.com健康医疗17今天更新

关于 WHOOP — MissingMCP

WHOOP — MissingMCP 将 WHOOP 健康数据通过 OAuth 接入 Claude,面向希望在 MCP 客户端查看恢复、睡眠、训练等信息的用户。它在进程内使用 WHOOP 官方 v2 API,并以加密的只读令牌访问数据。

主要功能

  1. 01通过 WHOOP 官方 OAuth 登录
  2. 02查看恢复与睡眠数据
  3. 03查看应变、日周期与训练
  4. 04读取身体测量数据
  5. 05加密保存只读令牌

适用场景

  1. 01在 Claude 中查看 WHOOP 恢复情况
  2. 02回顾睡眠、应变和每日周期
  3. 03检查训练与身体测量记录

MCP 相关

提供 `/whoop/mcp` 远程 MCP 服务,客户端通过 OAuth 登录后即可使用 WHOOP 数据工具。该连接器在进程内调用 WHOOP 官方 v2 API。

配置

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

服务地址https://missingmcp.com/whoop/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 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。