zwift-mcp
zwift-mcp
您的 Zwift 训练历史保存在本地 SQLite 数据库中,通过 MCP 客户端(Claude 以及任何支持 MCP 的客户端)和 REST API 进行访问。
两个组件:
zwift_downloader.py— 一个 cron 作业,从 Zwift 游戏 API 和 ZwiftPower 拉取数据,解析原始 FIT 文件,并将所有内容存储在 SQLite 中mcp_server.py— 一个无状态的 streamable-HTTP MCP 服务器,外加一个 REST API,两者都通过同一个 bearer 令牌读取同一个数据库
为什么有两个数据源
Zwift 和 ZwiftPower 掌握的信息不同,而且两者都不完整:
Zwift API | ZwiftPower | |
所有骑行,包括 solo 和训练课程 | ✅ | ❌ 仅限比赛 |
心率、踏频、速度、最大功率 | ✅ 详情端点 | ✅ 每场比赛 |
圈数和每秒数据流 | 仅存在于 FIT 文件中 | ❌ |
比赛名次、组别、参赛阵容 | ❌ | ✅ |
临界功率曲线 | ❌ | ✅(仅限比赛) |
训练负荷,CTL/ATL/TSB | ❌ | ❌ |
因此,下载器会从 Zwift 列出活动,调用详情端点获取列表中缺少的汇总字段,下载并解析每个 FIT 文件以获取圈数和数据流,从 ZwiftPower 拉取比赛结果,并在本地计算训练负荷。两个网站的结果通过开始时间进行关联,因为它们没有共享的标识符——在实际数据上,这个时间差在约三分钟内,也就是你在起点等待区到发令旗落下之前所花的时间。
Related MCP server: catence
设置
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env # fill in ZWIFT_USER / ZWIFT_PASS为服务器生成一个 bearer 令牌:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"将其放入 .env,键名为 ZWIFT_MCP_AUTH_TOKEN。
在首次同步之前,请确认这些端点仍然符合此代码的预期——它们既没有文档也不稳定:
.venv/bin/python probe_zwift_api.py它会将原始载荷写入 probe_zwift.json(已 gitignore),并打印摘要。如果某个部分报告错误,请在同步前修复映射,而不是用 NULL 填充数据库。
同步
.venv/bin/python zwift_downloader.py # incremental
.venv/bin/python zwift_downloader.py --days 30
.venv/bin/python zwift_downloader.py --since 2024-01-01
.venv/bin/python zwift_downloader.py --full # re-fetch everything
.venv/bin/python zwift_downloader.py --with-samples # + per-second streams
.venv/bin/python zwift_downloader.py --backfill-detail
.venv/bin/python zwift_downloader.py --redo-detail # re-parse cached FIT files
.venv/bin/python zwift_downloader.py --skip-fit # no FIT pass (fast)
.venv/bin/python zwift_downloader.py --zp-only # ZwiftPower only
.venv/bin/python zwift_downloader.py --skip-zp # game API only
.venv/bin/python zwift_downloader.py --with-zp-fields # + full race fields
.venv/bin/python zwift_downloader.py --summary # print stats, sync nothing一个每夜运行的 cron 条目:
30 4 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py >> sync.log 2>&1第一次运行是最慢的:它会为每项活动下载一个 FIT 文件。后续运行只获取新增内容,已缓存的 FIT 文件不会重新下载。
运行服务器
.venv/bin/python mcp_server.py # HTTP (default), port 8081
.venv/bin/python mcp_server.py --transport stdio # local Claude Desktop在 HTTP 模式下:
/mcp and /mcp/ MCP streamable HTTP endpoint (both spellings work)
/api/v1/... REST API, same bearer token
/api/v1/health liveness probe, unauthenticated使用 URL 和 Authorization: Bearer <token> 连接 MCP 客户端。
生产环境
deploy/zwift-mcp.service 是一个加固的 systemd unit,用于安装在 /opt/zwift-mcp 下。如果改从主目录运行,则需要去掉 ProtectHome——否则它会隐藏服务自己的工作目录——并将 ReadWritePaths 指向安装路径。
sudo cp deploy/zwift-mcp.service /etc/systemd/system/
sudo systemctl enable --now zwift-mcp
journalctl -u zwift-mcp -f选择一个未被使用的端口(ss -tlnp),并在 .env 中设置;每个端口运行一个 MCP 服务器。
每夜同步,与机器上运行的其他任务错开时间:
15 9 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py --days 10 > download.log 2>&1要更新正在运行的部署:
cd /opt/zwift-mcp && ./deploy/update.sh该命令会拉取更新、安装任何新的依赖、重启 unit 并检查健康端点——如果被跟踪的文件有本地修改,它会拒绝运行。
.env、数据库和 fits/ 都在 gitignore 中,因此拉取永远不会触碰它们。如果拉取更改了 schema/schema_zwift.sql,请注意没有迁移机制——删除数据库并重新同步(缓存的 FIT 文件使这一过程代价很低)。
数据库使用 WAL 模式,因此每夜同步和运行中的服务器不会互相阻塞。请保持这种状态:在默认的回滚日志模式下,长时间的重算会让实时查询遇到“database is locked”错误。
MCP 接口
资源 — zwift://athlete, zwift://activities, zwift://activities/recent, zwift://stats/summary, zwift://stats/monthly, zwift://training/daily, zwift://power/curve, zwift://racing/results
读取工具
工具 | 说明 |
| 按运动类型、世界、日期、距离、时长、功率、比赛筛选 |
| 摘要、圈数、区间时间、该次骑行的功率曲线、比赛结果 |
| 每日 TSS 以及 CTL / ATL / TSB |
| 每周或每月训练量 |
| 阈值以及由此得出的功率/心率区间 |
| 总计,按运动类型和世界分类 |
| 每个时长段的最佳平均功率,本地与 ZwiftPower 对比 |
| ZwiftPower 成绩,包含组别和名次 |
| 单场比赛,以及已同步的完赛阵容 |
| Zwift 个人资料、ZwiftPower 个人资料、当前状态 |
| 对整个数据库的只读 SELECT |
写入工具 — rename_activity(推送到 Zwift,然后更新本地)、set_local_annotation(本地标签和备注,绝不会发送到任何地方)和 get_activity_fit_file(原始 FIT 文件的存放位置)。
REST API
GET /api/v1/health unauthenticated
GET /api/v1/athlete
GET /api/v1/activities?sport=&world_id=&start_date=&races_only=&limit=
GET /api/v1/activities/{id}?include_samples=
PATCH /api/v1/activities/{id} {"name": …, "local_notes": …}
GET /api/v1/activities/{id}/laps
GET /api/v1/activities/{id}/samples?limit=&offset=
GET /api/v1/stats/summary
GET /api/v1/stats/monthly
GET /api/v1/daily-metrics?start_date=&end_date=&limit=
GET /api/v1/power-curve?source=local|zwiftpower|both
GET /api/v1/races?start_date=&title_contains=&limit=
GET /api/v1/races/{event_id}
GET /api/v1/zwiftpower/profile
GET /api/v1/sync-statecurl -H "Authorization: Bearer $ZWIFT_MCP_AUTH_TOKEN" \
"http://localhost:8081/api/v1/activities?races_only=true&limit=5"数据库
athletes — profile, FTP, weight, lifetime totals
worlds — world id lookup (seeded)
activities — one row per ride or run
activity_laps — from the FIT lap messages
activity_samples — per-second stream (only with --with-samples)
activity_zone_distribution — time in zone, computed from samples + FTP
power_curve — best mean power per duration, per activity
segment_results — segment efforts from the Zwift API
zp_profile — ZwiftPower category, zFTP, racing score
zp_results — one row per race
zp_event_results — full finishing fields (--with-zp-fields)
zp_critical_power — ZwiftPower's own CP curve
daily_metrics — derived TSS, CTL, ATL, TSB per day
sync_state — per-dataset watermarks
Views:
activity_summary — km, km/h, w/kg, TSS
monthly_stats — by month and sport
weekly_load — weekly volume and TSS
power_curve_best — all-time best per duration, with the ride that set it
race_results — ZwiftPower results joined to the local activity所有存储值均使用国际单位制:米、秒、瓦、次/分、米/秒。换算在视图中进行。
值得了解的事项
训练负荷是在这里计算的,不是获取的。 每次骑行都根据 Zwift 当时的 FTP(
profileFtp)进行缩放,若缺失则回退到当前 FTP。ZWIFT_FTP_OVERRIDE可替换过期值。tss_source告诉你 TSS 是如何得出的 — 来自解析 FIT 的np,或根据摘要估算的avg_power。这种估算会低估有大幅度功率波动的骑行,因此会被标记而不是隐藏。跑步活动按自行车 FTP 评分,除非设置了
ZWIFT_RUN_FTP。Zwift 的跑步功率并不是同一量值,因此请将这些 TSS 值视为参考。FIT 文件是细节所在。 没有它就没有圈数、没有数据流、没有标准化功率,也没有功率曲线。
没有功率计记录的跑步活动不会获得功率曲线或功率区间。 它们的 FIT 包含一条全零的功率通道,这表示缺失,而不是数据。
ZwiftPower 的临界功率曲线仅覆盖比赛,且仅限近期比赛——空曲线是正常结果,不是失败。
数据库没有迁移机制。 如果模式列发生变化,请删除
zwift_activities.db并重新同步;fits/中的缓存 FIT 意味着不会重新下载任何内容。ZwiftPower 是一个独立的账户关联。 如果 API 不返回任何内容,请用浏览器打开 zwiftpower.com 并使用 Zwift 登录一次;必须先在该网站上存在个人资料,之后才能查询任何内容。
ZwiftPower 失效永远不会导致同步失败。 比赛数据次于游戏数据,因此故障会被记录在
sync_state中并跳过。这两个 API 都不是公开的。 字段名和端点会随时更改,恕不另行通知。
probe_zwift_api.py用于告诉你哪个出了问题。
This server cannot be deployed
Maintenance
Related MCP Connectors
Remote MCP server for training, nutrition, wellness, and performance data with OAuth 2.0.
MCP server for Withings health data — sleep, activity, heart, and body metrics.
Strava MCP tools for AI: athletes, activities, segments, clubs, routes. Powered by HAPI MCP server.
Manage your endurance training data and race preparation
Related MCP Servers
- AlicenseBqualityAmaintenancePrivacy-first MCP server for Strava activities, streams, routes and training data.29189 npm3MIT
- AlicenseBqualityAmaintenanceMCP server for local fitness-data extraction and analysis from Garmin Connect, Intervals.icu, and Strava. Provides read-only analytical tools over DuckDB and targeted Strava enrichment.35338 npmAGPL 3.0
- AlicenseNot gradedqualityAmaintenanceMCP server that mirrors your Garmin data into a personal database and exposes tools for health summaries, training load, muscle readiness, and race analysis, with optional chat-driven insights via stdio or HTTP.1MIT
- AlicenseNot gradedqualityCmaintenanceA read-only MCP server that synchronizes Garmin Connect summaries into a local SQLite cache and provides tools to query daily activity, recent activities, and recovery data.MIT