Skip to main content
Glama
benniblau

zwift-mcp

by benniblau

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

读取工具

工具

说明

query_activities

按运动类型、世界、日期、距离、时长、功率、比赛筛选

get_activity_details

摘要、圈数、区间时间、该次骑行的功率曲线、比赛结果

get_training_load

每日 TSS 以及 CTL / ATL / TSB

get_training_trends

每周或每月训练量

get_training_zones

阈值以及由此得出的功率/心率区间

get_activity_stats

总计,按运动类型和世界分类

get_power_curve

每个时长段的最佳平均功率,本地与 ZwiftPower 对比

get_race_results

ZwiftPower 成绩,包含组别和名次

get_race_details

单场比赛,以及已同步的完赛阵容

get_athlete_profile

Zwift 个人资料、ZwiftPower 个人资料、当前状态

execute_sql

对整个数据库的只读 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-state
curl -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 用于告诉你哪个出了问题。

-
license - not tested
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

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

View all MCP Connectors

Latest Blog Posts

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