zwift-mcp
zwift-mcp
Ihre Zwift-Trainingshistorie in einer lokalen SQLite-Datenbank, bereitgestellt für MCP-Clients (Claude und alles andere, was MCP spricht) und über eine REST-API.
Zwei Komponenten:
zwift_downloader.py— ein Cron-Job, der von der Zwift-Game-API und von ZwiftPower abruft, die ursprünglichen FIT-Dateien parst und alles in SQLite speichert.mcp_server.py— ein zustandsloser Streamable-HTTP-MCP-Server plus eine REST-API, die beide dieselbe Datenbank hinter einem Bearer-Token lesen.
Warum zwei Quellen
Zwift und ZwiftPower wissen unterschiedliche Dinge, und keine der beiden ist vollständig:
Zwift API | ZwiftPower | |
Jede Fahrt, einschließlich Solofahrten und Workouts | ✅ | ❌ nur Rennen |
Herzfrequenz, Trittfrequenz, Geschwindigkeit, Maximalleistung | ✅ Detail-Endpunkt | ✅ pro Rennen |
Runden und Datenströme pro Sekunde | nur in der FIT-Datei | ❌ |
Rennposition, Kategorie, Feld | ❌ | ✅ |
Critical-Power-Kurve | ❌ | ✅ (nur Rennen) |
Trainingsbelastung, CTL/ATL/TSB | ❌ | ❌ |
Der Downloader listet also Aktivitäten von Zwift auf, ruft den Detail-Endpunkt für die Zusammenfassungsfelder ab, die die Liste auslässt, lädt jede FIT-Datei herunter und parst sie für Runden und Datenströme, holt Rennergebnisse von ZwiftPower und berechnet die Trainingsbelastung lokal. Die Ergebnisse der beiden Websites werden über die Startzeit verknüpft, da sie keine gemeinsame Kennung haben – bei echten Daten stimmt diese innerhalb von etwa drei Minuten überein, die Zeit, die Sie im Startbereich verbringen, bevor die Flagge fällt.
Einrichtung
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp .env.example .env # fill in ZWIFT_USER / ZWIFT_PASSGenerieren Sie ein Bearer-Token für den Server:
python3 -c "import secrets; print(secrets.token_urlsafe(32))"Legen Sie es in .env als ZWIFT_MCP_AUTH_TOKEN ab.
Überprüfen Sie vor der ersten Synchronisierung, dass die Endpunkte noch so aussehen, wie dieser Code es erwartet – keiner davon ist dokumentiert oder stabil:
.venv/bin/python probe_zwift_api.pyEs schreibt probe_zwift.json (gitignoriert) mit den rohen Nutzdaten und gibt eine Zusammenfassung aus. Wenn ein Abschnitt einen Fehler meldet, korrigieren Sie das Mapping vor der Synchronisierung, anstatt die Datenbank mit NULLs zu füllen.
Synchronisierung
.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 nothingEin nächtlicher Cron-Eintrag:
30 4 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py >> sync.log 2>&1Der erste Lauf ist der langsame: Er lädt für jede Aktivität eine FIT-Datei herunter. Spätere Läufe holen nur Neues, und zwischengespeicherte FITs werden nie erneut heruntergeladen.
Server ausführen
.venv/bin/python mcp_server.py # HTTP (default), port 8081
.venv/bin/python mcp_server.py --transport stdio # local Claude DesktopIm HTTP-Modus:
/mcp and /mcp/ MCP streamable HTTP endpoint (both spellings work)
/api/v1/... REST API, same bearer token
/api/v1/health liveness probe, unauthenticatedVerbinden Sie einen MCP-Client mit der URL und Authorization: Bearer <token>.
Produktion
deploy/zwift-mcp.service ist eine gehärtete systemd-Unit für eine Installation unter /opt/zwift-mcp. Wenn Sie die Unit stattdessen aus einem Home-Verzeichnis ausführen, bedeutet das, ProtectHome zu entfernen – es würde das eigene Arbeitsverzeichnis des Dienstes verbergen – und ReadWritePaths auf den Installationspfad zu richten.
sudo cp deploy/zwift-mcp.service /etc/systemd/system/
sudo systemctl enable --now zwift-mcp
journalctl -u zwift-mcp -fWählen Sie einen Port, den nichts anderes verwendet (ss -tlnp), und setzen Sie ihn in .env; ein MCP-Server pro Port.
Ein nächtlicher Sync, zeitlich versetzt zu allem anderen, was auf dem Rechner läuft:
15 9 * * * cd /opt/zwift-mcp && .venv/bin/python zwift_downloader.py --days 10 > download.log 2>&1Um eine laufende Installation zu aktualisieren:
cd /opt/zwift-mcp && ./deploy/update.shDas zieht Änderungen, installiert neue Abhängigkeiten, startet die Unit neu und überprüft den Health-Endpoint – und weigert sich zu laufen, wenn versionierte Dateien lokale Änderungen aufweisen.
.env, die Datenbank und fits/ sind gitignoriert, daher fasst ein Pull sie nie an. Wenn der Pull schema/schema_zwift.sql ändert, beachten Sie, dass es keine Migrationen gibt – löschen Sie die Datenbank und synchronisieren Sie neu (zwischengespeicherte FITs machen das günstig).
Die Datenbank befindet sich im WAL-Modus, sodass der nächtliche Sync und der laufende Server sich nicht gegenseitig blockieren. Behalten Sie das bei: Unter dem Standard-Rollback-Journal liefert eine lange Neuberechnung aktiven Abfragen „database is locked“.
MCP-Schnittstelle
Ressourcen — zwift://athlete, zwift://activities, zwift://activities/recent, zwift://stats/summary, zwift://stats/monthly, zwift://training/daily, zwift://power/curve, zwift://racing/results
Lese-Werkzeuge
Werkzeug | Funktion |
| Nach Sportart, Welt, Datum, Distanz, Dauer, Leistung und Rennen filtern |
| Zusammenfassung, Runden, Zeit in der Zone, die Kurve dieser Fahrt, Rennergebnis |
| Täglicher TSS mit CTL / ATL / TSB |
| Wöchentliches oder monatliches Volumen |
| Schwellenwerte und die daraus abgeleiteten Leistungs-/HF-Zonen |
| Gesamtsummen insgesamt, nach Sportart und nach Welt |
| Beste Durchschnittsleistung pro Dauer, lokal vs. ZwiftPower |
| ZwiftPower-Ergebnisse mit Kategorie und Position |
| Ein Rennen, plus das Zielfeld, wenn synchronisiert |
| Zwift-Profil, ZwiftPower-Profil, aktuelle Form |
| Schreibgeschütztes SELECT gegen die gesamte Datenbank |
Schreib-Werkzeuge — rename_activity (schiebt zu Zwift und aktualisiert dann lokal), set_local_annotation (lokale Tags und Notizen, werden nie irgendwohin gesendet) und get_activity_fit_file (wo die ursprüngliche FIT-Datei liegt).
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-statecurl -H "Authorization: Bearer $ZWIFT_MCP_AUTH_TOKEN" \
"http://localhost:8081/api/v1/activities?races_only=true&limit=5"Datenbank
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 activityAlle gespeicherten Werte sind SI-Einheiten: Meter, Sekunden, Watt, bpm, m/s. Die Umrechnungen leben in den Views.
Wissenswertes
Die Trainingsbelastung wird hier berechnet, nicht abgerufen. Jede Fahrt wird anhand des zu diesem Zeitpunkt bei Zwift hinterlegten FTP skaliert (
profileFtp), mit Rückfall auf den aktuellen Wert.ZWIFT_FTP_OVERRIDEersetzt einen veralteten Wert.tss_sourcezeigt, wie ein TSS zustande kam –npaus einer geparsten FIT-Datei oderavg_power, geschätzt aus der Zusammenfassung. Die Schätzung unterschätzt eine Fahrt mit großen Antritten, daher wird sie gekennzeichnet statt versteckt.Läufe werden gegen den Rad-FTP bewertet, sofern nicht
ZWIFT_RUN_FTPgesetzt ist. Zwifts Laufleistung ist nicht dieselbe Größe, behandeln Sie diese TSS-Werte daher als indikativ.Die FIT-Datei ist das Detail. Ohne sie gibt es keine Runden, keine Datenströme, keine normalisierte Leistung und keine Leistungskurve.
Läufe, die ohne Leistungsmesser aufgezeichnet wurden, erhalten keine Leistungskurve und keine Leistungszonen. Ihre FIT-Datei enthält einen durchgehend Null-Leistungskanal, was Abwesenheit und keine Daten bedeutet.
Die Critical-Power-Kurve von ZwiftPower umfasst nur Rennen, und nur aktuelle – eine leere Kurve ist eine normale Antwort, kein Fehler.
Die Datenbank hat keine Migrationen. Wenn sich eine Schema-Spalte ändert, löschen Sie
zwift_activities.dbund synchronisieren Sie neu; zwischengespeicherte FITs infits/bedeuten, dass nichts erneut heruntergeladen wird.ZwiftPower ist eine separate Kontoverknüpfung. Wenn die API nichts zurückgibt, öffnen Sie einmal zwiftpower.com in einem Browser und melden Sie sich mit Zwift an; das Profil muss dort existieren, bevor etwas abgefragt werden kann.
Ein ZwiftPower-Fehler lässt den Sync nie fehlschlagen. Renndaten sind sekundär zu den Spieldaten, daher wird ein Ausfall in
sync_stateprotokolliert und übersprungen.Keine der APIs ist öffentlich. Feldnamen und Endpunkte ändern sich ohne Vorankündigung.
probe_zwift_api.pyexistiert, um Ihnen zu sagen, welche kaputtgegangen ist.
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