zwift-mcp
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_PASSGenera 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.pyEscribe 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 nothingUna entrada cron nocturna:
30 4 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py >> sync.log 2>&1La 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 DesktopEn modo HTTP:
/mcp and /mcp/ MCP streamable HTTP endpoint (both spellings work)
/api/v1/... REST API, same bearer token
/api/v1/health liveness probe, unauthenticatedConecta 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 -fElige 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>&1Para actualizar un despliegue en ejecución:
cd /opt/zwift-mcp && ./deploy/update.shEso 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
Recursos — zwift://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 |
| Filtrar por deporte, mundo, fecha, distancia, duración, potencia, carreras |
| Resumen, vueltas, tiempo en zona, curva de esa salida, resultado de carrera |
| TSS diario con CTL / ATL / TSB |
| Volumen semanal o mensual |
| Umbrales y las zonas de potencia/FC derivadas de ellos |
| Totales generales, por deporte y por mundo |
| Mejor potencia media por duración, local vs ZwiftPower |
| Resultados de ZwiftPower con categoría y posición |
| Una carrera, más la clasificación final si está sincronizada |
| Perfil de Zwift, perfil de ZwiftPower, forma actual |
| SELECT de solo lectura contra toda la base de datos |
Herramientas de escritura — rename_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-statecurl -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 activityTodos 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_OVERRIDEreemplaza un valor desactualizado.tss_sourcete indica cómo se alcanzó un TSS —npdesde un FIT analizado, oavg_powerestimado 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.dby vuelve a sincronizar; los FIT en caché enfits/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_statey se omite.Ninguna API es pública. Los nombres de los campos y los endpoints cambian sin previo aviso.
probe_zwift_api.pyexiste para decirte cuál se ha roto.
This server cannot be installed
Maintenance
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
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
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