zwift-mcp
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分以内に一致します。これは、スタートの旗が下りる前にペン(スタートエリア)で過ごす時間です。
セットアップ
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.pyprobe_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 DesktopHTTPモードの場合:
/mcp and /mcp/ MCP streamable HTTP endpoint (both spellings work)
/api/v1/... REST API, same bearer token
/api/v1/health liveness probe, unauthenticatedURLと 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
読み取りツール
ツール | 説明 |
| スポーツ、ワールド、日付、距離、時間、パワー、レースでフィルタリング |
| サマリー、ラップ、ゾーン内時間、そのライドのカーブ、レース結果 |
| CTL / ATL / TSB 付きの日別TSS |
| 週次または月次のボリューム |
| 閾値と、そこから導出されるパワー/心拍ゾーン |
| 全体、スポーツ別、ワールド別の合計 |
| 期間ごとの最高平均パワー、ローカル vs ZwiftPower |
| カテゴリーと順位付きのZwiftPower結果 |
| 1つのレースと、同期済みの場合は完走者の一覧 |
| Zwiftプロフィール、ZwiftPowerプロフィール、現在のフォーム状態 |
| データベース全体に対する読み取り専用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-statecurl -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は、どちらが壊れたかを知らせるためにあります。
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