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분 이내로 일치하는데, 이는 출발 신호 전에 그리드에서 보내는 시간입니다.

설정

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))"

.envZWIFT_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가 깨졌는지 알려주기 위해 존재합니다.

-
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