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 拉取比赛结果,并在本地计算训练负荷。两个网站的结果通过开始时间进行关联,因为它们没有共享的标识符——在实际数据上,这个时间差在约三分钟内,也就是你在起点等待区到发令旗落下之前所花的时间。
设置
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 installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
MCP server for Withings health data — sleep, activity, heart, and body metrics.
The hockey data API. Stats, odds, and everything between. REST API and MCP server.
MCP server wrapping the Tesla Fleet API and TeslaMate API
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/benniblau/zwift-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server