Skip to main content
Glama
benniblau

zwift-mcp

by benniblau

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_PASS

Generieren 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.py

Es 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 nothing

Ein nächtlicher Cron-Eintrag:

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

Der 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 Desktop

Im HTTP-Modus:

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

Verbinden 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 -f

Wä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>&1

Um eine laufende Installation zu aktualisieren:

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

Das 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

Ressourcenzwift://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

query_activities

Nach Sportart, Welt, Datum, Distanz, Dauer, Leistung und Rennen filtern

get_activity_details

Zusammenfassung, Runden, Zeit in der Zone, die Kurve dieser Fahrt, Rennergebnis

get_training_load

Täglicher TSS mit CTL / ATL / TSB

get_training_trends

Wöchentliches oder monatliches Volumen

get_training_zones

Schwellenwerte und die daraus abgeleiteten Leistungs-/HF-Zonen

get_activity_stats

Gesamtsummen insgesamt, nach Sportart und nach Welt

get_power_curve

Beste Durchschnittsleistung pro Dauer, lokal vs. ZwiftPower

get_race_results

ZwiftPower-Ergebnisse mit Kategorie und Position

get_race_details

Ein Rennen, plus das Zielfeld, wenn synchronisiert

get_athlete_profile

Zwift-Profil, ZwiftPower-Profil, aktuelle Form

execute_sql

Schreibgeschütztes SELECT gegen die gesamte Datenbank

Schreib-Werkzeugerename_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-state
curl -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 activity

Alle 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_OVERRIDE ersetzt einen veralteten Wert.

  • tss_source zeigt, wie ein TSS zustande kamnp aus einer geparsten FIT-Datei oder avg_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_FTP gesetzt 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.db und synchronisieren Sie neu; zwischengespeicherte FITs in fits/ 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_state protokolliert und übersprungen.

  • Keine der APIs ist öffentlich. Feldnamen und Endpunkte ändern sich ohne Vorankündigung. probe_zwift_api.py existiert, um Ihnen zu sagen, welche kaputtgegangen ist.

-
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