Skip to main content
Glama
benniblau

zwift-mcp

by benniblau

zwift-mcp

Ваша история тренировок Zwift в локальной базе данных SQLite, доступная MCP-клиентам (Claude и всему остальному, что поддерживает MCP), а также через REST API.

Два компонента:

  • zwift_downloader.py — cron-задача, которая получает данные из игрового API Zwift и из ZwiftPower, разбирает исходные файлы FIT и сохраняет всё в SQLite

  • mcp_server.py — stateless MCP-сервер с транспортным протоколом streamable-HTTP и REST API, оба читают одну и ту же базу данных под одним bearer-токеном

Зачем два источника

Zwift и ZwiftPower знают разные вещи, и ни один из них не является полным:

Zwift API

ZwiftPower

Каждая поездка, включая соло и тренировки

❌ только гонки

Пульс, каденс, скорость, максимальная мощность

✅ endpoint деталей

✅ по каждой гонке

Круги и посекундные потоки

только внутри файла FIT

Позиция в гонке, категория, стартовая группа

Кривая критической мощности

✅ (только гонки)

Тренировочная нагрузка, CTL/ATL/TSB

Таким образом, загрузчик получает список активностей из Zwift, обращается к endpoint деталей для сводных полей, которых нет в списке, скачивает и разбирает каждый 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.

Перед первой синхронизацией убедитесь, что endpoints выглядят так, как ожидает этот код, — ни один из них не документирован и не стабилен:

.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

Подключите MCP-клиент, используя URL и Authorization: Bearer <token>.

Продакшн

deploy/zwift-mcp.service — это усиленный systemd-юнит для установки в /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

Эта команда выполняет pull, устанавливает новые зависимости, перезапускает юнит и проверяет health endpoint — и отказывается запускаться, если отслеживаемые файлы имеют локальные изменения.

.env, база данных и fits/ находятся в gitignore, поэтому pull их никогда не затрагивает. Если pull меняет 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

Все хранимые значения в СИ: метры, секунды, ватты, уд/мин, м/с. Преобразования выполняются в представлениях.

Что стоит знать

  • Тренировочная нагрузка вычисляется здесь, а не запрашивается. Каждая поездка масштабируется относительно FTP, который был у Zwift на тот момент (profileFtp), а при отсутствии — относительно текущего. ZWIFT_FTP_OVERRIDE заменяет устаревшее значение.

  • tss_source показывает, как был получен TSSnp из разобранного FIT или avg_power, оценённое по сводке. Оценка занижает нагрузку поездки с большими рывками, поэтому она помечается, а не скрывается.

  • Пробежки оцениваются относительно велосипедного FTP, если не задан ZWIFT_RUN_FTP. Мощность бега у Zwift — не та же величина, поэтому относитесь к этим значениям TSS как к ориентировочным.

  • Файл FIT — это источник деталей. Без него нет кругов, нет потоков, нет нормализованной мощности и нет кривой мощности.

  • Пробежки, записанные без измерителя мощности, не получают кривой мощности и зон мощности. В их FIT-файле канал мощности состоит из нулей, что означает отсутствие данных, а не сами данные.

  • Кривая критической мощности ZwiftPower покрывает только гонки, и только недавние — пустая кривая это нормальный ответ, а не сбой.

  • В базе данных нет миграций. Если изменился столбец схемы, удалите zwift_activities.db и выполните синхронизацию заново; кэшированные FIT-файлы в fits/ означают, что ничего не будет скачано повторно.

  • ZwiftPower — это отдельная привязка аккаунта. Если API ничего не возвращает, откройте zwiftpower.com в браузере один раз и войдите через Zwift; профиль должен существовать там, прежде чем можно будет что-либо запрашивать.

  • Сбой ZwiftPower никогда не приводит к сбою синхронизации. Данные гонок вторичны по отношению к игровым данным, поэтому сбой записывается в sync_state и пропускается.

  • Ни один из API не является публичным. Имена полей и endpoints меняются без предупреждения. 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