Skip to main content
Glama
benniblau

zwift-mcp

by benniblau

zwift-mcp

Tu historial de entrenamiento de Zwift en una base de datos SQLite local, expuesto a clientes MCP (Claude y cualquier otra cosa que hable MCP) y a través de una API REST.

Dos componentes:

  • zwift_downloader.py — un trabajo cron que obtiene datos de la API del juego Zwift y de ZwiftPower, analiza los archivos FIT originales y lo guarda todo en SQLite.

  • mcp_server.py — un servidor MCP sin estado con HTTP transmisible más una API REST, ambos leyendo la misma base de datos detrás de un token de portador.

Por qué dos fuentes

Zwift y ZwiftPower saben cosas distintas, y ninguna de las dos es completa:

Zwift API

ZwiftPower

Cada salida, incluidas las salidas en solitario y los entrenamientos

❌ solo carreras

Frecuencia cardíaca, cadencia, velocidad, potencia máxima

✅ endpoint de detalle

✅ por carrera

Vueltas y flujos por segundo

solo dentro del archivo FIT

Posición en la carrera, categoría, grupo

Curva de potencia crítica

✅ (solo carreras)

Carga de entrenamiento, CTL/ATL/TSB

Así que el descargador enumera las actividades de Zwift, llama al endpoint de detalle para los campos de resumen que la lista omite, descarga y analiza cada FIT para obtener vueltas y flujos, obtiene los resultados de las carreras de ZwiftPower y calcula la carga de entrenamiento localmente. Los resultados de los dos sitios se vinculan por la hora de inicio, ya que no comparten ningún identificador — con datos reales la coincidencia es de unos tres minutos, el tiempo que pasas en el cajón de salida antes de que caiga la bandera.

Configuración

python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env       # fill in ZWIFT_USER / ZWIFT_PASS

Genera un token de portador para el servidor:

python3 -c "import secrets; print(secrets.token_urlsafe(32))"

Ponlo en .env como ZWIFT_MCP_AUTH_TOKEN.

Antes de la primera sincronización, confirma que los endpoints siguen teniendo el aspecto que este código espera — ninguno de ellos está documentado ni es estable:

.venv/bin/python probe_zwift_api.py

Escribe probe_zwift.json (ignorado por git) con las cargas útiles sin procesar e imprime un resumen. Si una sección informa de un error, corrige el mapeo antes de sincronizar en lugar de llenar la base de datos con NULL.

Sincronización

.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

Una entrada cron nocturna:

30 4 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py >> sync.log 2>&1

La primera ejecución es la lenta: descarga un archivo FIT por actividad. Las ejecuciones posteriores solo obtienen lo nuevo, y los FIT en caché nunca se vuelven a descargar.

Ejecutar el servidor

.venv/bin/python mcp_server.py                    # HTTP (default), port 8081
.venv/bin/python mcp_server.py --transport stdio  # local Claude Desktop

En modo HTTP:

/mcp  and  /mcp/     MCP streamable HTTP endpoint (both spellings work)
/api/v1/...          REST API, same bearer token
/api/v1/health       liveness probe, unauthenticated

Conecta un cliente MCP con la URL y Authorization: Bearer <token>.

Producción

deploy/zwift-mcp.service es una unidad systemd endurecida para una instalación en /opt/zwift-mcp. Ejecutarla desde un directorio personal en su lugar implica eliminar ProtectHome — ocultaría el directorio de trabajo del propio servicio — y apuntar ReadWritePaths a la ruta de instalación.

sudo cp deploy/zwift-mcp.service /etc/systemd/system/
sudo systemctl enable --now zwift-mcp
journalctl -u zwift-mcp -f

Elige un puerto que no use nada más (ss -tlnp) y configúralo en .env; un servidor MCP por puerto.

Una sincronización nocturna, escalonada con respecto a cualquier otra cosa que se ejecute en la máquina:

15 9 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py --days 10 > download.log 2>&1

Para actualizar un despliegue en ejecución:

cd /opt/zwift-mcp && ./deploy/update.sh

Eso hace pull, instala las nuevas dependencias, reinicia la unidad y comprueba el endpoint de salud — y se niega a ejecutarse si los archivos rastreados tienen ediciones locales.

.env, la base de datos y fits/ están ignorados por git, así que un pull nunca los toca. Si el pull cambia schema/schema_zwift.sql, ten en cuenta que no hay migraciones — borra la base de datos y vuelve a sincronizar (los FIT en caché lo hacen barato).

La base de datos está en modo WAL, de modo que la sincronización nocturna y el servidor en ejecución no se bloquean mutuamente. Mantenlo así: con el diario de reversión predeterminado, un recálculo largo provoca que las consultas en vivo reciban 'database is locked'.

Superficie MCP

Recursoszwift://athlete, zwift://activities, zwift://activities/recent, zwift://stats/summary, zwift://stats/monthly, zwift://training/daily, zwift://power/curve, zwift://racing/results

Herramientas de lectura

Herramienta

Qué hace

query_activities

Filtrar por deporte, mundo, fecha, distancia, duración, potencia, carreras

get_activity_details

Resumen, vueltas, tiempo en zona, curva de esa salida, resultado de carrera

get_training_load

TSS diario con CTL / ATL / TSB

get_training_trends

Volumen semanal o mensual

get_training_zones

Umbrales y las zonas de potencia/FC derivadas de ellos

get_activity_stats

Totales generales, por deporte y por mundo

get_power_curve

Mejor potencia media por duración, local vs ZwiftPower

get_race_results

Resultados de ZwiftPower con categoría y posición

get_race_details

Una carrera, más la clasificación final si está sincronizada

get_athlete_profile

Perfil de Zwift, perfil de ZwiftPower, forma actual

execute_sql

SELECT de solo lectura contra toda la base de datos

Herramientas de escriturarename_activity (envía a Zwift y luego actualiza localmente), set_local_annotation (etiquetas y notas locales, nunca se envían a ningún sitio) y get_activity_fit_file (dónde vive el FIT original).

API REST

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"

Base de datos

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

Todos los valores almacenados son del SI: metros, segundos, vatios, ppm, m/s. Las conversiones viven en las vistas.

Cosas que merece la pena saber

  • La carga de entrenamiento se calcula aquí, no se obtiene. Cada salida se escala contra el FTP que Zwift tenía en ese momento (profileFtp), con respaldo al actual. ZWIFT_FTP_OVERRIDE reemplaza un valor desactualizado.

  • tss_source te indica cómo se alcanzó un TSSnp desde un FIT analizado, o avg_power estimado a partir del resumen. La estimación subestima una salida con grandes picos de potencia, así que se etiqueta en lugar de ocultarse.

  • Las actividades de running se puntúan contra el FTP de bicicleta a menos que se establezca ZWIFT_RUN_FTP. La potencia de running de Zwift no es la misma magnitud, así que trata esos valores de TSS como indicativos.

  • El archivo FIT es el detalle. Sin él no hay vueltas, ni flujos, ni potencia normalizada, ni curva de potencia.

  • Las actividades de running registradas sin medidor de potencia no obtienen curva de potencia ni zonas de potencia. Su FIT contiene un canal de potencia todo a cero, lo cual es ausencia, no datos.

  • La curva de potencia crítica de ZwiftPower cubre solo carreras, y solo las recientes — una curva vacía es una respuesta normal, no un fallo.

  • La base de datos no tiene migraciones. Si una columna del esquema cambia, borra zwift_activities.db y vuelve a sincronizar; los FIT en caché en fits/ significan que no se vuelve a descargar nada.

  • ZwiftPower es una vinculación de cuenta separada. Si la API no devuelve nada, abre zwiftpower.com en un navegador una vez e inicia sesión con Zwift; el perfil tiene que existir allí antes de que se pueda consultar nada.

  • Un fallo de ZwiftPower nunca hace fallar la sincronización. Los datos de carrera son secundarios respecto a los datos del juego, así que una interrupción se registra en sync_state y se omite.

  • Ninguna API es pública. Los nombres de los campos y los endpoints cambian sin previo aviso. probe_zwift_api.py existe para decirte cuál se ha roto.

-
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