Skip to main content
Glama
benniblau

zwift-mcp

by benniblau

zwift-mcp

Zwiftのトレーニング履歴をローカルSQLiteデータベースに保存し、MCPクライアント(ClaudeやMCPに対応するその他のクライアント)およびREST API経由で公開します。

2つのコンポーネント:

  • zwift_downloader.py — cronジョブで、ZwiftゲームAPIとZwiftPowerからデータを取得し、元のFITファイルを解析して、すべてをSQLiteに保存します。

  • mcp_server.py — ステートレスなstreamable-HTTP MCPサーバーとREST APIを提供し、両方が1つのベアラートークンの背後で同じデータベースを読み取ります。

2つのソースの理由

ZwiftとZwiftPowerはそれぞれ異なる情報を保有しており、どちらも完全ではありません:

Zwift API

ZwiftPower

ソロやワークアウトを含むすべてのライド

✅

❌ レースのみ

心拍数、ケイデンス、スピード、最大パワー

✅ 詳細エンドポイント

✅ レースごと

ラップと毎秒のストリームデータ

FITファイル内のみ

❌

レース順位、カテゴリー、フィールド

❌

✅

クリティカルパワーカーブ

❌

✅ (レースのみ)

トレーニング負荷、CTL/ATL/TSB

❌

❌

したがって、ダウンローダーはZwiftからアクティビティ一覧を取得し、一覧に含まれないサマリーフィールドを詳細エンドポイントから取得し、ラップとストリームのために各FITをダウンロードして解析し、ZwiftPowerからレース結果を取得し、トレーニング負荷をローカルで計算します。2つのサイトの結果は開始時刻でリンクされます。というのも、両者に共通の識別子がないためです。実際のデータでは、約3分以内に一致します。これは、スタートの旗が下りる前にペン(スタートエリア)で過ごす時間です。

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

サーバー用のベアラートークンを生成してください:

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

.env に ZWIFT_MCP_AUTH_TOKEN として設定してください。

最初の同期の前に、エンドポイントがこのコードが想定する形のままであることを確認してください。いずれもドキュメント化されておらず、安定していません:

.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

URLと Authorization: Bearer <token> を使用してMCPクライアントを接続します。

本番環境

deploy/zwift-mcp.service は、/opt/zwift-mcp 配下へのインストール向けの堅牢化されたsystemdユニットです。代わりにホームディレクトリから実行する場合、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 に設定します。ポートごとに1つの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

これにより、プル、新しい依存関係のインストール、ユニットの再起動、ヘルスエンドポイントの確認が行われ、追跡対象ファイルにローカルでの編集がある場合は実行を拒否します。

.env、データベース、fits/ はgitignore対象のため、プルでは決して変更されません。プルで 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

CTL / ATL / TSB 付きの日別TSS

get_training_trends

週次または月次のボリューム

get_training_zones

閾値と、そこから導出されるパワー/心拍ゾーン

get_activity_stats

全体、スポーツ別、ワールド別の合計

get_power_curve

期間ごとの最高平均パワー、ローカル vs ZwiftPower

get_race_results

カテゴリーと順位付きのZwiftPower結果

get_race_details

1つのレースと、同期済みの場合は完走者の一覧

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

保存される値はすべてSI単位です。メートル、秒、ワット、bpm、m/s。換算はビューにあります。

知っておくべきこと

  • トレーニング負荷はここで計算され、取得されません。 各ライドは、当時Zwiftが保持していたFTP(profileFtp)に基づいてスケーリングされ、現在のFTPにフォールバックします。ZWIFT_FTP_OVERRIDE は古い値を置き換えます。

  • tss_source はTSSがどのように算出されたかを示します — 解析されたFITからの np、またはサマリーから推定された avg_power。推定値は大きなサージがあるライドを過小評価するため、隠されずにラベル付けされます。

  • ランのスコアリングはバイクのFTPに基づいて行われます。ZWIFT_RUN_FTP が設定されている場合を除きます。Zwiftのランニングパワーは同じ量ではないため、これらのTSS値は参考値として扱ってください。

  • FITファイルが詳細情報です。 それがなければ、ラップ、ストリームデータ、正規化パワー、パワーカーブはありません。

  • パワーメーターなしで記録されたランには、パワーカーブもパワーゾーンもありません。 そのFITには全ゼロのパワーチャンネルが含まれますが、それはデータがなく、存在しないことを意味します。

  • ZwiftPowerのクリティカルパワーカーブはレースのみを対象としており、しかも最近のレースだけです。カーブが空でも正常な応答であり、失敗ではありません。

  • データベースにはマイグレーションがありません。 スキーマのカラムが変更された場合は、zwift_activities.db を削除して再同期してください。fits/ にキャッシュされたFITがあるため、再ダウンロードは発生しません。

  • ZwiftPowerは別のアカウントリンクです。 APIが何も返さない場合は、ブラウザでzwiftpower.comを一度開き、Zwiftでサインインしてください。何かをクエリできる前に、プロフィールがそこに存在する必要があります。

  • ZwiftPowerの障害で同期が失敗することはありません。 レースデータはゲームデータに比べて二次的なものなので、障害は sync_state に記録され、スキップされます。

  • どちらのAPIも公開されていません。 フィールド名やエンドポイントは予告なく変更されます。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