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 и вычисляет тренировочную нагрузку локально. Результаты с двух сайтов связываются по времени старта, поскольку общего идентификатора у них нет — на реальных данных расхождение составляет около трёх минут, то есть время, которое вы проводите в стартовом загоне до опускания флага.

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.

Перед первой синхронизацией убедитесь, что 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 показывает, как был получен TSS — np из разобранного 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 существует, чтобы сообщить вам, какой из них сломался.

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