从一台 VPS 和一个域名部署私人 Garmin Connect MCP从 DNS、HTTPS、Garmin session、Auth0 OAuth 到 ChatGPT/Codex 接入

这是一份面向个人用户的完整部署教程。它只假设你已经拥有一台可通过 SSH 管理的 VPS 和一个可以修改 DNS 的域名;从域名解析、Nginx/HTTPS、Node.js、Garmin 登录、Auth0 OAuth,一直到 ChatGPT 和 Codex 连接都会覆盖。

本文来自一套实际完成并验证的部署:Ubuntu 24.04、1 核 CPU、约 2 GB 内存、同机已有 Nginx/Hexo、Garmin 中国区账号、Auth0 OAuth、ChatGPT 个人插件和 Codex 远程 MCP。最终由客户端发现并成功调用全部十二个只读工具。没有网站的空白 VPS 也可以照做;已有博客只是本教程特别处理的一种兼容场景。文中的域名、账号、用户 ID、回调 ID 和令牌均为占位符。

适用范围:一个 Garmin 账号、一个所有者、只读查询。它不是面向公众注册的多租户 SaaS。Garmin 使用的是非官方 Web API,未来可能因 Garmin 接口变更而需要更新项目。

最终效果

完成后:

  • VPS 通过自己的域名和 HTTPS 对外提供 MCP;
  • 如果同机已有 Hexo 或其他网站,原站点继续正常使用 80/443;
  • Garmin MCP 仅监听 VPS 本机的 127.0.0.1:3100
  • Nginx 使用独立子域名或现有 HTTPS 站点转发 /mcp 和 OAuth 发现路径;
  • ChatGPT 和 Codex 都可以通过 Auth0 OAuth 登录并获得 garmin:read
  • Codex 也可以选择独立的静态 Bearer 作为兼容回退;
  • 你的电脑关机后,VPS 上的服务仍可使用;
  • 在任何新对话中重新选择 Garmin Connect 即可,不必永远留在同一个对话。

项目提供十二个只读工具:

工具 用途
garmin_activities 最近活动和活动指标
garmin_sleep 睡眠时长、评分和阶段
garmin_steps 每日步数
garmin_heart_rate 静息、最低和最高心率
garmin_weight 体重和身体成分
garmin_workouts Garmin Connect 中保存的训练计划
garmin_profile 精简的个人资料摘要
garmin_hrv 昨夜 HRV、7 日均值、个人基线和状态
garmin_body_battery Body Battery 当前值、充入/消耗和可选日内曲线
garmin_training_readiness 训练准备度、恢复时间及各影响因素
garmin_training_status 训练状态、急性/慢性负荷、负荷比和负荷平衡
garmin_vo2max 跑步/骑行 VO₂max 当前值及历史趋势

日期范围最多为 31 天。

后五项指标需要兼容的 Garmin 设备和足够的已同步历史。Garmin Connect 尚未计算某项指标时,工具会返回 hasData: false 或空字段,而不是把缺失值当成 0。garmin_body_battery 默认只返回每日摘要;只有查询单日时才可通过 include_samples=true 获取日内曲线。

架构和三种凭据

私人 Garmin Connect MCP 部署架构:ChatGPT 和 Codex 经 Auth0 授权后,通过 Nginx 访问 Garmin MCP Server,再由服务器读取 Garmin Connect 数据
ChatGPT 与 Codex 经 Auth0 授权后,通过 Nginx 访问仅监听本机端口的 Garmin MCP Server;原网站可以继续与 MCP 共用同一台 VPS。

部署中会遇到三种不同的凭据,绝对不要混用:

凭据 存放位置 用途
Garmin session token VPS 的受限环境文件 MCP 代表你读取 Garmin 数据
MCP static Bearer VPS 和使用它的客户端 可选的兼容回退,不是 OAuth 的必需项
Auth0 用户和 access token Auth0/ChatGPT OAuth 流程 确认是哪位用户正在访问 MCP

不要把 Garmin session token 当成 MCP Bearer,也不要把 Garmin 密码输入 Auth0 登录页。任何一种令牌都不要提交到 Git、粘贴进聊天或写进 Nginx 配置。

1. 准备 VPS、域名和 HTTPS

开始前只需要准备:

  • 一台 Ubuntu 22.04 或 24.04 VPS;
  • 1 核 CPU、2 GB 内存和约 1 GB 可用磁盘空间;
  • 一个可以添加 DNS 记录的域名;
  • 一个 Garmin Connect 账号;
  • 一个 Auth0 账号(第 4 节会创建或配置 tenant);
  • 可以启用开发者模式和个人 MCP 连接的 ChatGPT 账号或工作区;
  • 一台可信的本地电脑,用来完成 Garmin 密码和 MFA 登录。

单用户场景的日常资源占用很低。1 GB VPS 也可能运行,但安装依赖和 TypeScript 构建时余量较小,建议准备 swap;已有约 2 GB 内存的轻量博客 VPS 通常足够。

1.1 决定公网地址

推荐为 MCP 使用独立子域名,例如:

https://garmin.example.com/mcp

在域名服务商处创建 garmin 的 A 记录,指向 VPS 公网 IPv4。只有当 VPS 的 IPv6 已正确配置时才添加 AAAA 记录。等待解析后检查:

getent ahosts garmin.example.com

如果 VPS 上已经有博客,也可以直接复用现有域名,例如 https://www.example.com/mcp,这种情况不需要新增 DNS 或证书。专用子域名的隔离更清楚;复用现有域名则最接近本文的实际部署。后文统一用 https://your-domain.example/mcp 表示你最终选择的完整地址。

建议现在记下四个值,后续始终复制,不要手敲:

名称 示例
MCP 主机名 garmin.example.com
MCP 完整 URL https://garmin.example.com/mcp
Auth0 tenant domain your-tenant.us.auth0.com
Auth0 用户 ID auth0|xxxxxxxx

使用 Cloudflare 时,初次申请证书可以先设为 DNS only,部署成功后再决定是否开启代理。

1.2 检查和安装基础软件

先登录 VPS 检查环境:

cat /etc/os-release
nproc
free -h
df -h /
sudo ss -lntp

如果还没有 Git、Nginx 或 snapd:

sudo apt update
sudo apt install -y git nginx snapd
sudo systemctl enable --now nginx

再检查:

git --version
node -v
npm -v
sudo nginx -t
sudo systemctl status nginx --no-pager
command -v node

项目最低要求 Node.js 20。生产环境建议安装 Node.js 官方下载页列出的当前 LTS,并采用能够让 systemd 访问的系统级安装方式。如果 node -v 不存在、低于 20,或已经停止安全维护,请先升级。记下 command -v node 的结果;若不是 /usr/bin/node,第 6 节需要相应修改 systemd 的 ExecStart

应确认:

  • Node.js 为 v20 或更新版本;
  • Nginx 配置测试成功;
  • 80/443 仍由 Nginx 使用;
  • 计划使用的 3100 端口没有被其他进程占用。

如果系统提示大量安全更新或需要重启,先做 VPS 快照,在维护窗口更新并重启,然后确认 SSH 和已有网站恢复正常,再继续部署。云防火墙或 UFW 至少应允许你的 SSH 端口以及 TCP 80/443,不需要开放 3100。

1.3 为新子域名启用 HTTPS

如果复用一个已经有可信证书的 HTTPS 域名,跳过本节。

使用新子域名时,先创建一个最小 HTTP 站点:

sudoedit /etc/nginx/sites-available/garmin-mcp

写入:

server {
    listen 80;
    listen [::]:80;
    server_name garmin.example.com;

    location / {
        return 404;
    }
}

启用并测试:

sudo ln -s /etc/nginx/sites-available/garmin-mcp /etc/nginx/sites-enabled/garmin-mcp
sudo nginx -t
sudo systemctl reload nginx

然后按 Certbot 官方 Nginx 指南安装证书。Ubuntu 常用命令如下;如果 certbot 已经存在,不要重复创建链接:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/local/bin/certbot
sudo certbot --nginx -d garmin.example.com
sudo certbot renew --dry-run

浏览器访问 https://garmin.example.com/,看到可信证书即可;此时返回 404 没有关系,因为 MCP 路由还没有加入。

2. 在 VPS 上安装和构建项目

建议为 MCP 创建一个不能登录系统的独立用户。以下命令只需执行一次;如果 id garmin-mcp 已经能找到用户,就跳过 useradd

id garmin-mcp
sudo useradd --system --home-dir /opt/garmin-connect-mcp --shell /usr/sbin/nologin garmin-mcp

克隆项目并构建:

cd /opt
sudo git clone https://github.com/getsomecat/garmin-connect-mcp.git
sudo chown -R garmin-mcp:garmin-mcp /opt/garmin-connect-mcp
cd /opt/garmin-connect-mcp
sudo -u garmin-mcp npm ci
sudo -u garmin-mcp npm run build
sudo -u garmin-mcp npm run smoke:metrics
sudo -u garmin-mcp npm run smoke:http
sudo -u garmin-mcp npm run smoke:auth0

五个命令都成功后再配置真实凭据。npm run smoke:metrics 使用合成健康数据,npm run smoke:auth0 使用测试配置;两者都不会登录你的 Garmin 或 Auth0 账号。

3. 在本地电脑导出 Garmin session

不要在 VPS 或聊天窗口里反复输入 Garmin 密码。推荐在自己的可信电脑上完成一次登录,再只把导出的 session 传到 VPS。

在本地电脑执行:

git clone https://github.com/getsomecat/garmin-connect-mcp.git
cd garmin-connect-mcp
npm ci
cp .env.example .env

编辑 .env,临时填写:

[email protected]
GARMIN_PASSWORD=your-garmin-password
GARMIN_REGION=cn

国际区账号把 cn 改为 global。然后运行:

umask 077
npm run --silent export-session > garmin-session.json

如果 Garmin 要求 MFA,脚本会提示输入一次性验证码。

  • 验证码输错:按 Ctrl+C 结束,再重新运行导出命令,使用最新验证码;
  • 网页能登录但脚本失败:先检查中国区账号是否设置了 GARMIN_REGION=cn
  • 连续失败多次:停止尝试一段时间,避免触发 Garmin 风控或限流;
  • 成功后,删除 .env 中的 GARMIN_PASSWORD

为避免 JSON 中的引号或换行影响 systemd 环境文件,把 session 转为单行 Base64:

base64 < garmin-session.json | tr -d '\r\n' > garmin-session.b64

通过 SFTP、scp 或你信任的密码管理器把 garmin-session.b64 传到 VPS。传输完成并写入服务配置后,删除本地和 VPS 上的临时文件。session 和密码都不要发给模型。

4. 配置 Auth0

Auth0 在这里是授权服务器。ChatGPT 或 Codex 先在 Auth0 登录并取得有限权限的 access token,MCP 再验证这个 token 的签名、签发方、资源、有效期、scope 和用户 ID。

4.1 启用 tenant 设置

打开 Auth0 Dashboard 的 Settings → Advanced → Settings,启用:

  • Resource Parameter Compatibility Profile
  • Include Issuer in Authorization Responses
  • Client ID Metadata Document Registration

其中前两项是 Auth0 官方 MCP 指南要求的发现与资源绑定设置,第三项用于导入 OpenAI 客户端发布的 CIMD 元数据。控制台文案可能随版本稍有变化。

4.2 创建 API

进入 Applications → APIs → Create API,填写:

  • Name:Garmin Connect MCP
  • Identifier:https://your-domain.example/mcp
  • Signing algorithm:RS256

创建后打开该 API 的 Permissions/Scopes 页面,添加:

garmin:read

Identifier 必须与后面的 MCP_PUBLIC_URLMCP_AUTH0_AUDIENCE 逐字符一致。末尾多一个 / 也会被 Auth0 视为不同的 audience。

4.3 创建唯一允许的 Auth0 用户

对于私人单用户部署,建议:

  1. 使用一个 Database Connection;
  2. 创建一个仅供自己使用的 Auth0 用户;
  3. 禁止该 Connection 的公开注册;
  4. 将这个 Connection 提升为 Domain-Level Connection,因为 CIMD 属于第三方客户端;
  5. 在用户详情页复制 User ID,它通常类似 auth0|xxxxxxxx

稍后把这个 User ID 放入 MCP_AUTH0_ALLOWED_SUBJECTS。如果不设置用户白名单,tenant 中其他能登录的用户也可能访问同一个 Garmin 账号的数据。

4.4 区分 ChatGPT 与 Codex 的 CIMD 客户端

同一个 MCP URL 在 ChatGPT 个人插件和 Codex 直连中会出现为不同的 OAuth 客户端。不要只创建其中一个后假设另一个也能登录。

4.4.1 ChatGPT 个人插件

当 Auth0 已启用 issuer identification 时,进入 Applications → Applications → Create Application → Import from URL,输入:

https://chatgpt.com/oauth/client.json

先 Preview,确认名称、客户端认证方式以及回调地址;符合稳定回调条件时会使用:

https://chatgpt.com/connector_platform_oauth_redirect

再创建应用。若实际插件管理页展示的是带 callback ID 的 CIMD 或回调 URL,以管理页展示的精确值为准,不要自行拼接。

4.4.2 Codex 远程 MCP

Codex 直连使用按 MCP 地址派生的客户端,格式类似:

https://chatgpt.com/oauth/codex/<callback_id>/client.json

对应的本机回调类似:

http://127.0.0.1:<临时端口>/callback/<callback_id>

<callback_id> 由 Codex 根据 MCP URL 派生,不应照抄别人的值。最稳妥的做法是在第 9 节先发起一次 Codex 登录,再从终端输出、浏览器授权 URL 或 Auth0 Unknown client 详情中复制完整 CIMD URL,然后在 Auth0 选择 Import from URL → Preview → Create。导入后通常显示为 Native、Third-party、CIMD 应用,不需要给 VPS 保存 Auth0 client secret。

ChatGPT 稳定 CIMD、ChatGPT callback-specific CIMD 和 Codex CIMD 是不同客户端标识。删除并重建连接时,应重新核对该连接实际发送的 client_id,不要凭旧截图猜测。

4.5 为每个客户端授予用户委托权限

回到刚创建的 Garmin API:

  1. Settings → Application Access Policy 中,把 User-Delegated Access 设置为 Per-app authorization
  2. Client Access 保持禁止或不授权;
  3. Application Access 中找到要使用的 ChatGPT 和/或 Codex CIMD 应用;
  4. 对每个应用打开 User-Delegated Access → Grant Access
  5. 选择 garmin:read 并保存;
  6. 对普通交互式登录不要授予 Client Credentials/Machine-to-Machine 权限。

Auth0 把 User-Delegated Access 与 Client Access 分开管理。ChatGPT 和 Codex 的交互式 OAuth 都属于用户委托流程,只需要前者。若未来给 API 增加更多 scope,逐项授权比“自动授予未来全部权限”更符合最小权限原则。

这保证客户端只能在你登录并授权后,以你的身份申请 garmin:readMCP_AUTH0_ALLOWED_SUBJECTS 还会在 MCP 端再次限制允许访问 Garmin 数据的 Auth0 用户。

5. 创建 VPS 服务环境文件

如果计划保留非 OAuth 客户端,再在本地或密码管理器中生成一个与 Garmin session 完全无关的静态 Bearer:

openssl rand -hex 32

ChatGPT 和 Codex 都使用 Auth0 OAuth 时可以跳过这一步,并在环境文件中省略 MCP_BEARER_TOKEN

在 VPS 上创建受限文件:

sudo touch /etc/garmin-connect-mcp.env
sudo chown root:garmin-mcp /etc/garmin-connect-mcp.env
sudo chmod 0640 /etc/garmin-connect-mcp.env
sudoedit /etc/garmin-connect-mcp.env

写入以下内容,并替换所有占位符:

GARMIN_SESSION_TOKEN_B64=PASTE_THE_SINGLE_LINE_BASE64_SESSION_HERE
GARMIN_REGION=cn

GARMIN_CACHE_TTL=300
GARMIN_CACHE_MAX_ENTRIES=100
GARMIN_RETRY_ATTEMPTS=3
GARMIN_RETRY_BASE_DELAY_MS=1000
GARMIN_RETRY_MAX_DELAY_MS=30000
GARMIN_ACTIVITY_DETAIL=compact
GARMIN_LOG_LEVEL=info

MCP_TRANSPORT=http
MCP_HTTP_HOST=127.0.0.1
MCP_HTTP_PORT=3100
MCP_HTTP_PATH=/mcp

# 可选兼容回退:至少 32 字节,不是 Garmin session。
MCP_BEARER_TOKEN=PASTE_THE_INDEPENDENT_STATIC_BEARER_HERE

MCP_PUBLIC_URL=https://your-domain.example/mcp
MCP_AUTH0_DOMAIN=your-tenant.us.auth0.com
MCP_AUTH0_AUDIENCE=https://your-domain.example/mcp
MCP_AUTH0_ALLOWED_SUBJECTS=auth0|your-user-id

注意:

  • MCP_AUTH0_DOMAIN 不包含 https:// 和路径;
  • MCP_PUBLIC_URL、Auth0 API Identifier、MCP_AUTH0_AUDIENCE 必须完全相同;
  • Auth0 模式下不要设置任何 MCP_OAUTH_* 变量;
  • 如果不使用 Codex,可以省略 MCP_BEARER_TOKEN
  • 不要在这里保留 GARMIN_USERNAMEGARMIN_PASSWORD

6. 安装 systemd 服务

先再次检查 Node 的绝对路径:

command -v node
cd /opt/garmin-connect-mcp
sudo cp deploy/systemd/garmin-connect-mcp.service.example /etc/systemd/system/garmin-connect-mcp.service

项目模板的 ExecStart 使用 /usr/bin/node。如果 command -v node 的结果不同,现在运行 sudoedit /etc/systemd/system/garmin-connect-mcp.service,把 ExecStart 改为实际绝对路径。

启动服务:

sudo systemctl daemon-reload
sudo systemctl enable --now garmin-connect-mcp
sudo systemctl status garmin-connect-mcp --no-pager
sudo journalctl -u garmin-connect-mcp -n 100 --no-pager
curl -fsS http://127.0.0.1:3100/healthz

最后一个命令应返回健康状态。再确认 3100 只监听回环地址:

sudo ss -lntp | grep ':3100'

预期是 127.0.0.1:3100,而不是 0.0.0.0:3100。防火墙不需要开放 3100。

7. 在 Nginx 中增加 MCP 路由

把项目提供的 location 片段复制为 Nginx snippet:

sudo cp /opt/garmin-connect-mcp/deploy/nginx/garmin-connect-mcp.conf.example /etc/nginx/snippets/garmin-connect-mcp.conf

然后在第 1 节创建的子域名,或已有网站域名的 HTTPS server { ... } 块内部加入:

include /etc/nginx/snippets/garmin-connect-mcp.conf;

该 snippet 的核心内容是:

location ~ ^/(mcp|authorize|token|register|revoke|oauth/approve|\.well-known/oauth-(protected-resource(/mcp)?|authorization-server))$ {
    proxy_pass http://127.0.0.1:3100;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header Authorization $http_authorization;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_buffering off;
    proxy_read_timeout 130s;
    proxy_send_timeout 130s;
    client_max_body_size 1m;
}

测试并平滑重载:

sudo nginx -t
sudo systemctl reload nginx

平滑重载不会停止已有网站。如果同机有 Hexo 或其他站点,立即检查首页和静态资源仍然正常,然后测试:

curl -fsS https://your-domain.example/.well-known/oauth-protected-resource/mcp
curl -i https://your-domain.example/mcp

第一条应返回 JSON,里面的 resourcehttps://your-domain.example/mcpauthorization_servers 指向 Auth0 tenant。第二条在没有凭据时返回 401 Unauthorized 是正常的安全行为。

如果原站点存在 location ^~ /,它可能阻止正则 location 生效;应为 MCP 路径添加更明确的 location,或去掉会吞掉所有子路径的 ^~,然后再次执行 nginx -t

可选:Cloudflare

如果域名经过 Cloudflare:

  • SSL/TLS 建议使用 Full (strict)
  • 不要缓存 /mcp/.well-known/oauth-*
  • 不要对这些路径启用会返回 HTML 的浏览器质询、验证码或页面改写;
  • 仍只让源站公开 80/443,不开放 3100。

8. 在 ChatGPT 中连接

OpenAI 官方流程要求先有公网 HTTPS Streamable HTTP MCP endpoint,再在 ChatGPT 开启开发者模式并创建连接。界面名称可能显示为 Plugins、Apps 或 Connectors,但核心字段相同:

  1. 打开 Settings → Security and login,启用 Developer mode
  2. 打开 Plugins,使用页面上的添加按钮创建个人插件;若主页面没有直接显示 +,从右上角 Add/添加 → Create plugin/创建插件 进入;
  3. 名称填写 Garmin Connect,描述明确它只读访问私人 Garmin 数据;
  4. Connection 选择公网 MCP,URL 填写 https://your-domain.example/mcp
  5. 创建连接,让客户端自动执行 MCP 和 OAuth discovery;某些版本不会提供单独的“选择 OAuth”步骤,这是正常的;
  6. 在发现/授权页面确认授权服务器是 https://your-tenant.us.auth0.com/
  7. 确认资源是完整 MCP URL,业务权限包含 garmin:read;Auth0 还可能显示标准 OIDC 身份 scope;
  8. 使用第 4.3 节创建的 Auth0 用户登录并授权;
  9. 回到插件详情,确认扫描结果为 12 read,再安装或启用插件。

第一次测试建议使用不展示个人资料内容的请求:

调用 garmin_profile,只告诉我调用是否成功以及返回结构是否正常,不要显示姓名、邮箱或其他个人字段。

成功后再尝试:

  • “分析我最近 14 天的跑步训练量和恢复状态。”
  • “比较最近 7 天和之前 7 天的睡眠、静息心率和步数。”
  • “比较最近 7 天的 HRV、Body Battery、训练准备度和恢复时间。”
  • “读取急性/慢性训练负荷、负荷比、训练状态和最近 30 天 VO₂max 趋势。”
  • “列出本周训练,但不要输出个人资料字段。”

在新对话中,需要从工具菜单重新选择 Garmin Connect;无需重新部署。手机端登录同一 ChatGPT 账号后,如果该客户端显示这个开发者连接,也可以选择使用;如果移动端暂时不显示,请用 ChatGPT 网页版或桌面版。是否展示个人开发者连接可能受客户端版本和工作区策略影响。

避免旧 OAuth 配置缓存

如果同一个 URL 以前连接过项目内置 OAuth,ChatGPT 可能继续打开:

https://your-domain.example/authorize

在 Auth0 模式下,本机 /authorize 返回 404 是预期行为;正确的授权页应在 Auth0 tenant 域名。稳妥处理方法是:

  1. 暂时保留旧连接;
  2. 用一个新名称创建全新的个人连接;
  3. 确认新连接发现 Auth0 并成功调用工具;
  4. 再删除旧连接,把新连接改成原来的名称。

修改认证或工具元数据后,先在连接设置中执行 Refresh,再开启一个新对话测试。若服务器已经是十二个工具,而插件详情仍固定显示七个,删除旧的个人插件后用同一 URL 创建一个全新插件,让平台生成新的 App ID 并重新扫描;随后在 Auth0 核对新连接实际使用的 CIMD 客户端。反复在旧连接上重试通常不会清除已保存的工具和 OAuth 元数据快照。

9. 让 Codex 通过 OAuth 使用同一 VPS

推荐让 Codex 也使用 Auth0 OAuth,这样本机不必保存独立静态 Bearer。ChatGPT 桌面应用、Codex CLI 和 IDE 扩展会共享同一个 Codex host 的 MCP 配置;ChatGPT 网页版不会读取本机的 ~/.codex/config.toml,它使用第 8 节创建的托管插件。

9.1 添加 Streamable HTTP MCP

桌面界面操作:

  1. 打开 Settings → Plugins → MCP
  2. 选择 Add → Connect to custom MCP
  3. 名称填写 garmin_connect
  4. 类型选择 Streamable HTTP
  5. URL 填写 https://your-domain.example/mcp
  6. 不填写启动命令、参数或环境变量;
  7. 保存并按界面提示 Restart,然后选择 Authenticate。

也可以在本机的普通终端执行:

codex mcp add garmin_connect --url https://your-domain.example/mcp
codex mcp list
codex mcp login garmin_connect --oauth-client-registration cimd --scopes garmin:read

如果已经通过界面添加过 garmin_connect,不要重复运行 codex mcp add,直接运行后两条。--scopes garmin:read 明确请求本项目唯一的业务 scope;Auth0 可能同时请求它公开的标准 OIDC 身份 scope。

9.2 首次登录时注册 Codex CIMD

第一次 Authenticate/Login 可能在 Auth0 显示:

invalid_request: Unknown client: https://chatgpt.com/oauth/codex/<callback_id>/client.json

这不表示 Garmin 密码错误,也通常与本机代理无关。它表示 Auth0 尚未登记这个 Codex 客户端。按以下顺序处理:

  1. 在 Auth0 错误页展开 See details for this error,或查看终端打印的 authorize URL;
  2. 复制完整的 client_id,它必须是 https://chatgpt.com/oauth/codex/<callback_id>/client.json,不要只复制 <callback_id>
  3. 先直接打开该 JSON URL,确认 client_name 为 Codex、application_typenative,回调路径含相同的 <callback_id>
  4. 在 Auth0 进入 Applications → Applications → Create Application → Import from URL
  5. 粘贴完整 CIMD URL,依次选择 Preview → Create Application
  6. 在新应用的 Connections 中确认第 4.3 节的 Database/Domain-Level Connection 已启用;
  7. API Access → Garmin Connect MCP → User-Delegated Access 中 Grant Access,选择 garmin:read 并保存;
  8. 回到本机重新运行登录命令并在 Auth0 授权页接受授权:
codex mcp login garmin_connect --oauth-client-registration cimd --scopes garmin:read

授权完成后浏览器页面可能自动关闭,因为授权码已回调到 127.0.0.1 的临时端口。以终端出现下面的成功信息为准:

Successfully logged in to MCP server 'garmin_connect'.

再检查:

codex mcp list

预期 garmin_connectenabledAuthOAuth。完全退出并重新打开 Codex,在输入框执行 /mcp,然后确认工具清单为十二个。可用下面的隐私保护测试做端到端验收:

调用 garmin_profile,只告诉我成功或失败以及当前可用工具数量,不要显示个人资料字段。

如果终端显示 failed to write OAuth tokens to keyringfile-store.lockOperation not permitted,说明当前命令运行在无权写入 Codex 安全存储的受限环境。关闭该受限终端,在你正常登录的系统终端中重新运行 codex mcp login;不要用 sudo,并确认自己的账号拥有 ~/.codex

9.3 可选:静态 Bearer 回退

只有不方便完成 OAuth 的可信客户端才需要静态 Bearer。把以下配置加入可信项目的 .codex/config.toml,或个人的 ~/.codex/config.toml

[mcp_servers.garmin_connect]
url = "https://your-domain.example/mcp"
bearer_token_env_var = "GARMIN_MCP_BEARER_TOKEN"
startup_timeout_sec = 20
tool_timeout_sec = 120

环境变量的内容必须与 VPS 上的 MCP_BEARER_TOKEN 相同。macOS 桌面应用可以把令牌保存到权限为 0600 的本地文件,再加载到图形会话环境。以下命令在 macOS 默认 zsh 中不会把输入回显到屏幕:

mkdir -p ~/.codex/secrets
chmod 700 ~/.codex/secrets
umask 077
read -s "GARMIN_TOKEN?MCP Bearer token: "
print -r -- "$GARMIN_TOKEN" > ~/.codex/secrets/garmin-mcp-bearer-token
unset GARMIN_TOKEN
launchctl setenv GARMIN_MCP_BEARER_TOKEN "$(tr -d '\r\n' < ~/.codex/secrets/garmin-mcp-bearer-token)"

完全退出并重新打开 Codex,然后执行 /mcp 或打开 MCP 设置确认连接。这个本地文件也不要加入任何 Git 仓库。OAuth 与静态 Bearer 二选一即可,不要把 Garmin session token 当作这个 Bearer。

10. 验收清单

逐项确认:

  • 域名的 HTTPS 证书可信;
  • 如果同机有原网站,其首页和静态资源正常;
  • nginx -t 成功;
  • systemd 服务为 active (running)
  • 3100 只监听 127.0.0.1
  • /healthz 在 VPS 本机返回成功;
  • OAuth protected-resource metadata 的 resource 和 issuer 正确;
  • protected-resource metadata 的 scopes_supported 只有 garmin:read
  • 未登录访问 /mcp 返回 401;
  • Auth0 只创建了 garmin:read 用户委托授权;
  • 每个实际使用的 ChatGPT/Codex CIMD 客户端都已单独登记并获得用户委托权限;
  • MCP_AUTH0_ALLOWED_SUBJECTS 是自己的准确 User ID;
  • npm run smoke:http 输出 HTTP smoke test passed with 12 tools.
  • ChatGPT 或 Codex 实际看到十二个工具;
  • 若使用 Codex OAuth,codex mcp list 显示 enabledOAuth
  • garmin_profile 端到端调用成功;
  • Git 历史、日志和聊天里没有任何 session、密码或 Bearer。

11. 常见问题

现象 最可能原因 处理方法
Garmin 网页能登录,导出脚本却报密码错误 中国区/国际区不一致,或连续尝试触发风控 中国区设 GARMIN_REGION=cn;停止频繁重试后重新导出 session
MFA 验证码输错 验证码已失效或只能使用一次 Ctrl+C 后重新运行导出脚本,输入最新验证码
Garmin 返回 429 查询过于频繁 项目会指数退避;避免连续重复大范围查询,单次日期范围不超过 31 天
502 Bad Gateway Node 服务未运行或 Nginx 端口不一致 systemctl statusjournalctl127.0.0.1:3100/healthz
未登录访问 /mcp 返回 401 正常的认证挑战 继续检查 WWW-Authenticate 和 protected-resource metadata
Auth0 登录成功但 MCP 仍返回 401 audience、scope 或用户 sub 不匹配 对照三个完全相同的 MCP URL,确认 garmin:read 和 allowlist User ID
Codex 授权页显示 Unknown client: .../oauth/codex/.../client.json Auth0 只有 ChatGPT 插件客户端,没有登记 Codex 的 server-specific CIMD 复制错误中的完整 CIMD URL,在 Auth0 Import from URL 创建应用并授予用户委托 garmin:read,然后重试登录
Codex OAuth 浏览器授权后自动关闭 授权码已成功回调到本机临时端口 这是正常现象;检查终端是否显示 Successfully logged in,再运行 codex mcp list
failed to write OAuth tokens to keyringfile-store.lock 登录命令运行在受限沙箱中,无法写安全存储 在正常系统终端以自己的账号重试,不要使用 sudo,确认 ~/.codex 归自己所有
ChatGPT 打开本机 /authorize 并得到 404 旧连接缓存了内置 OAuth 元数据 新建一个全新的连接完成 Auth0 发现,验证后再删除旧连接
ChatGPT 一直转圈,Auth0 日志没有请求 客户端缓存、弹窗/代理/浏览器扩展拦截 先检查授权 URL 是否为 Auth0;用新连接测试,并在同一浏览器中临时排除拦截
界面没有单独的“选择 OAuth”步骤 新版客户端会从 MCP metadata 自动发现认证 继续创建连接并使用 Authenticate;核对跳转域名是 Auth0 即可
ChatGPT/Codex 仍只显示原来的七个工具 客户端保存了扩展前的工具快照,或服务器尚未更新到 v0.2.0 先在 VPS 更新、构建并确认 smoke:http 为 12;然后 Refresh/Restart。ChatGPT 个人插件仍不更新时创建全新插件,Codex 则重新启动 host 并检查 /mcp
HRV、训练准备度或 VO₂max 返回空值 设备不支持、历史不足或数据尚未同步 先确认 Garmin Connect App 中能看到该指标,再缩小到最近有记录的日期重试
Mac ChatGPT 应用意外退出 客户端问题,不足以证明服务端 OAuth 失败 用网页版完成配置;更新/重开客户端,并以 VPS/Auth0 日志判断请求是否到达
Garmin session 过期 Garmin 撤销或失效了长期 session 在可信电脑重新运行 export-session,更新 Base64 后重启服务
加入 MCP 后博客路由异常 snippet 放错 server 块,或现有 ^~ / 抢占路径 恢复备份、运行 nginx -t,只在正确 HTTPS server 中加入 MCP location
手机端看不到连接 客户端版本或工作区策略暂未开放个人开发者连接 使用同一账号的网页或桌面端;服务本身仍在 VPS 上运行

本地代理确实可能影响浏览器跳转、Cookie 或 Auth0 页面加载,但它不会把 MCP 的 Auth0 配置自动改成本机 /authorize。判断时以三类证据为准:浏览器地址栏、Auth0 tenant 日志、VPS Nginx/systemd 日志。若 Auth0 完全没有收到请求且地址栏指向自己的域名,优先排查旧连接缓存。

12. 更新项目

更新前先确认 Git 工作区没有手工改动:

cd /opt/garmin-connect-mcp
sudo -u garmin-mcp git status --short
sudo -u garmin-mcp git pull --ff-only origin main
sudo -u garmin-mcp npm ci
sudo -u garmin-mcp npm run build
sudo -u garmin-mcp npm run smoke:http
sudo -u garmin-mcp npm run smoke:auth0
sudo systemctl restart garmin-connect-mcp
sudo systemctl status garmin-connect-mcp --no-pager
curl -fsS http://127.0.0.1:3100/healthz

如果工具或认证元数据有变化,再到 ChatGPT 连接设置中执行 Refresh,并使用新对话验证。

13. 回退和备份

修改前备份:

  • /etc/garmin-connect-mcp.env
  • 现有 Nginx HTTPS 站点配置;
  • 当前可工作的项目 commit ID;
  • Auth0 API、CIMD 应用和用户授权设置的截图或导出。

如果 MCP 部署失败但博客必须立即恢复:

  1. 从 HTTPS server 块移除 MCP snippet 的 include
  2. 运行 sudo nginx -t
  3. 运行 sudo systemctl reload nginx
  4. 停止 MCP 服务;
  5. 验证博客首页。

项目还保留了内置单用户 OAuth 作为回退,但它不能与 Auth0 同时启用。具体变量和安全限制见根目录 README

14. 安全和隐私检查

  • 只读不等于不敏感:睡眠、体重、心率和活动仍是健康隐私数据;
  • 禁止 Auth0 Database Connection 的公开注册;
  • 始终设置 MCP_AUTH0_ALLOWED_SUBJECTS
  • 只授予 garmin:read 用户委托权限;
  • 分别核对 ChatGPT 与 Codex 实际使用的 CIMD URL,不授权来源不明的客户端;
  • 交互式 ChatGPT/Codex OAuth 不需要 Client Credentials/Machine-to-Machine 权限;
  • 不公开 3100,不把 Node 服务绑定到 0.0.0.0
  • 所有公网访问必须经过可信 HTTPS;
  • 环境文件为 0640,本地 secret 文件为 0600
  • 不在 Nginx、Git、聊天、截图或日志中出现凭据;
  • 怀疑泄露时,立即轮换 MCP Bearer、重新导出 Garmin session,并在 Auth0 撤销会话或授权;
  • 不要把单用户实例开放给其他人,因为所有通过验证的调用最终读取的是同一个 Garmin 账号。

本项目开源地址:https://github.com/getsomecat/garmin-connect-mcp

官方参考

© 版权声明
THE END
喜欢就支持一下叭
点赞9 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容