Skip to main content
Glama
benniblau

zwift-mcp

by benniblau

zwift-mcp

로컬 SQLite 데이터베이스에 저장된 Zwift 트레이닝 기록을 MCP 클라이언트(Claude 및 MCP를 지원하는 모든 것)와 REST API로 노출합니다.

두 가지 구성 요소:

  • zwift_downloader.py — Zwift 게임 API와 ZwiftPower에서 데이터를 가져와 원본 FIT 파일을 파싱하고 모든 것을 SQLite에 저장하는 cron 작업

  • mcp_server.py — 단일 베어러 토큰 뒤에서 동일한 데이터베이스를 읽는 무상태 streamable-HTTP MCP 서버와 REST API

두 가지 소스가 필요한 이유

Zwift와 ZwiftPower는 서로 다른 정보를 알고 있으며, 어느 쪽도 완전하지 않습니다:

Zwift API

ZwiftPower

모든 라이딩(솔로 및 워크아웃 포함)

✅

❌ 레이스만

심박수, 케이던스, 속도, 최대 파워

✅ 상세 엔드포인트

✅ 레이스별

랩 및 초 단위 스트림

FIT 파일 안에만 있음

❌

레이스 순위, 카테고리, 필드

❌

✅

크리티컬 파워 곡선

❌

✅ (레이스만)

트레이닝 부하, CTL/ATL/TSB

❌

❌

따라서 다운로더는 Zwift에서 활동 목록을 가져오고, 목록에 없는 요약 필드를 위해 상세 엔드포인트를 호출하며, 각 FIT를 다운로드하여 랩과 스트림을 파싱하고, ZwiftPower에서 레이스 결과를 가져와 트레이닝 부하를 로컬에서 계산합니다. 두 사이트의 결과는 공유 식별자가 없으므로 시작 시간으로 연결됩니다. 실제 데이터에서 그 시간은 약 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에 설정하세요. 포트당 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 처리되어 있으므로 pull이 이들을 건드리지 않습니다. pull이 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

일일 TSS 및 CTL / ATL / TSB

get_training_trends

주간 또는 월간 볼륨

get_training_zones

임계값과 그로부터 파생된 파워/심박수 구간

get_activity_stats

전체, 스포츠별, 월드별 합계

get_power_curve

기간별 최고 평균 파워, 로컬 대비 ZwiftPower

get_race_results

카테고리와 순위가 포함된 ZwiftPower 결과

get_race_details

단일 레이스와 동기화된 경우 완주 필드

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. 추정값은 급격한 파워 상승이 있는 라이딩을 과소평가하므로 숨기지 않고 라벨로 표시합니다.

  • 달리기는 ZWIFT_RUN_FTP가 설정되지 않은 경우 자전거 FTP 기준으로 점수가 매겨집니다. Zwift의 러닝 파워는 같은 양이 아니므로 해당 TSS 값은 참고용으로만 간주하세요.

  • FIT 파일이 세부 데이터입니다. FIT가 없으면 랩, 스트림, 정규화 파워, 파워 곡선이 없습니다.

  • 파워 미터 없이 기록된 달리기에는 파워 곡선이나 파워 구간이 없습니다. 해당 FIT에는 전체가 0인 파워 채널이 포함되어 있는데, 이는 데이터가 아니라 부재를 의미합니다.

  • ZwiftPower의 크리티컬 파워 곡선은 레이스만, 그리고 최근 레이스만 다룹니다. 빈 곡선은 실패가 아니라 정상적인 응답입니다.

  • 데이터베이스에는 마이그레이션이 없습니다. 스키마 열이 변경되면 zwift_activities.db를 삭제하고 다시 동기화하세요. fits/에 캐시된 FIT가 있으면 아무것도 다시 다운로드되지 않습니다.

  • ZwiftPower는 별도의 계정 연결입니다. API가 아무것도 반환하지 않으면 브라우저에서 zwiftpower.com을 한 번 열고 Zwift로 로그인하세요. 프로필이 거기에 있어야만 어떤 것도 조회할 수 있습니다.

  • ZwiftPower 장애로 인해 동기화가 실패하지 않습니다. 레이스 데이터는 게임 데이터에 부차적이므로 중단은 sync_state에 기록되고 건너뜁니다.

  • 두 API 모두 공개되지 않았습니다. 필드 이름과 엔드포인트는 예고 없이 변경될 수 있습니다. probe_zwift_api.py는 어떤 API가 깨졌는지 알려주기 위해 존재합니다.

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