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 拉取比赛结果,并在本地计算训练负荷。两个网站的结果通过开始时间进行关联,因为它们没有共享的标识符——在实际数据上,这个时间差在约三分钟内,也就是你在起点等待区到发令旗落下之前所花的时间。

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

读取工具

工具

说明

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 用于告诉你哪个出了问题。

Related MCP Connectors

Related MCP Servers

  • A
    license
    B
    quality
    A
    maintenance
    MCP 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.
    35
    338 npm
    AGPL 3.0
  • A
    license
    Not graded
    quality
    A
    maintenance
    MCP 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.
    1
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    A 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