fitMCP
A multi-platform fitness MCP server that syncs data from Garmin, Strava, Google Fit, and Suunto into a local DuckDB database and provides analytics tools via MCP.
README
fitness_mcp
A generic, multi-platform fitness MCP server. It pulls data from Garmin Connect, Strava, Google Fit, and Suunto, stores everything locally in a single DuckDB file, and exposes analytics as MCP tools over stdio — usable from Claude Desktop, Cursor, Windsurf, VS Code, or any MCP client.
- Zero running infrastructure — sync is manual, the server runs on demand.
- Fully extensible — adding a platform = implement one abstract class and register it. The tools layer never changes.
- Read-only by design — MCP tools only read;
fitness_queryenforces SELECT-only.
Architecture
server.py MCP entry point (stdio) providers/base.py abstract interface + dataclasses
sync.py CLI + shared sync engine providers/garmin.py Garmin Connect (garth)
db/database.py DuckDB connection + upserts providers/strava.py Strava (OAuth2 + httpx)
db/schema.sql table definitions tools/*.py MCP tools (activities, health, …)
Data flow: providers fetch normalized records → db upserts them into DuckDB
→ tools query DuckDB and return a uniform JSON envelope.
Every tool returns:
{ "success": true, "data": [...], "error": null, "meta": { "count": 42 } }
Setup
Automated (macOS / Linux)
./setup.sh # venv + deps + .env scaffold, then prints next steps
Then edit .env, and optionally let the script do the rest:
./setup.sh --login # interactive Garmin login (password never stored)
./setup.sh --sync # initial sync of all platforms
./setup.sh --claude # install the Claude Desktop MCP config
./setup.sh --dev # install dev deps and run the test suite
# flags combine: ./setup.sh --dev --login --sync --claude
A Makefile wraps the common actions — make help, make setup, make login,
make sync, make serve, make test, make claude-install.
Automated (Windows / PowerShell)
.\setup.ps1 # venv + deps + .env scaffold (locked to your user), then next steps
.\setup.ps1 -Dev -Login -Sync -Claude # switches combine, same meaning as above
If PowerShell blocks the script, run it once as:
powershell -ExecutionPolicy Bypass -File .\setup.ps1
Then use the venv directly for the recurring commands:
.\.venv\Scripts\python.exe login.py
.\.venv\Scripts\python.exe sync.py --platform garmin --full-history
.\.venv\Scripts\python.exe scripts\claude_config.py --write # Claude Desktop config
On Windows the setup script restricts .env to your user account via icacls
(the POSIX chmod warning is a no-op there); the garth token cache lives under
%USERPROFILE%\.garth, which is already user-scoped by default.
The Claude Desktop entry can be generated or installed on its own:
python scripts/claude_config.py # print the JSON snippet
python scripts/claude_config.py --write # merge it into your Claude config (with backup)
Manual
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then fill in credentials
Credentials (.env)
DUCKDB_PATH=./fitness.duckdb
# Garmin Connect (via garth)
GARMIN_EMAIL=your@email.com
GARMIN_PASSWORD=yourpassword
GARTH_HOME=~/.garth # cached session token lives here
# Strava
STRAVA_CLIENT_ID=...
STRAVA_CLIENT_SECRET=...
STRAVA_REFRESH_TOKEN=...
.env and the DuckDB file are gitignored.
Strava: getting a refresh token (one time)
-
Create an API application at https://www.strava.com/settings/api. Note the Client ID and Client Secret.
-
Authorize your own account, requesting the
activity:read_allscope. Visit (replaceCLIENT_ID):https://www.strava.com/oauth/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http://localhost&approval_prompt=force&scope=activity:read_allAfter approving, the browser redirects to
http://localhost/?...&code=AUTH_CODE&.... CopyAUTH_CODEfrom the URL. -
Exchange the code for tokens:
curl -X POST https://www.strava.com/oauth/token \ -d client_id=CLIENT_ID -d client_secret=CLIENT_SECRET \ -d code=AUTH_CODE -d grant_type=authorization_codePut the
refresh_tokenfrom the response intoSTRAVA_REFRESH_TOKEN. The server refreshes the short-lived access token automatically on every sync.
Garmin uses
garth. Preferred login — run it once interactively so your password is never written to disk:python login.py # prompts for email/password (+ MFA); caches a tokenAfter that you can leave
GARMIN_PASSWORDout of.env; syncs reuse the cached session token. (Setting the password in.envstill works as a fallback for non-interactive/headless use.)
Security of credentials & sessions
- Nothing secret is committed —
.env, the DuckDB file, and the garth token cache (.garth/,*.token) are all gitignored. - Password stays off disk —
python login.pyreads it viagetpassand only persists the resulting session token. KeepGARMIN_PASSWORDblank. - Owner-only token cache — after every Garmin login the
GARTH_HOMEdirectory and its token files arechmod'd to0700/0600(POSIX). - Exposure warning —
sync.pyandserver.pywarn on startup if your.envis group/world-readable, with the exactchmod 600fix. - Treat session tokens like a password — a cached garth token grants access
to your Garmin account. If a machine is compromised, delete
GARTH_HOMEand re-runlogin.py. OAuth refresh tokens (Strava/Google/Suunto) are likewise sensitive; revoke them in each platform's app settings if leaked.
Syncing data
python sync.py --platform all # last 30 days, all platforms
python sync.py --platform garmin # garmin | strava | google_fit | suunto
python sync.py --platform garmin --from 2025-01-01 --to 2025-06-30
python sync.py --platform garmin --full-history # everything from 2010 (first run)
Supported platforms: garmin, strava, google_fit, suunto. Garmin is the
richest source (activities, sleep, HRV, body battery, stress); Strava and Suunto
provide activities; Google Fit provides activities, sleep, and weight.
After all platforms sync, duplicate workouts (same day + sport, duration and
distance within 5%) are de-duplicated: the Garmin record is kept (richer
metrics) and the Strava id is merged into its raw_json.
Recurring (automated) sync
After a one-time python login.py, the Garmin token auto-refreshes, so
scheduled syncs run unattended for months (re-run login.py only when the
long-lived token finally expires). The runner logs each run to logs/sync.log
and exits non-zero if any platform errored:
python scripts/scheduled_sync.py --platform all
Windows (Task Scheduler):
.\scripts\register_sync_task.ps1 # daily 07:00, all platforms
.\scripts\register_sync_task.ps1 -Time 06:30 -Platform garmin
.\scripts\register_sync_task.ps1 -Daily2x # 07:00 and 19:00
.\scripts\register_sync_task.ps1 -Unregister # remove it
The task runs as your user via S4U (no stored password, runs whether or not
you're logged in) and catches up missed runs. Trigger a test run with
Get-ScheduledTask fitnessmcp-sync | Start-ScheduledTask.
macOS/Linux (cron): point cron at the same runner —
0 7 * * * cd /path/to/fitMCP && .venv/bin/python scripts/scheduled_sync.py --platform all
Running the MCP server
python server.py # serves over stdio
Claude Desktop config
~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or
%APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"fitness": {
"command": "/absolute/path/to/fitness-mcp/.venv/bin/python",
"args": ["/absolute/path/to/fitness-mcp/server.py"],
"env": { "PYTHONPATH": "/absolute/path/to/fitness-mcp" }
}
}
}
MCP tools
| Tool | Purpose |
|---|---|
fitness_sync(platform) |
Sync garmin/strava/all into DuckDB |
fitness_sync_status() |
Last sync time + record counts per platform |
fitness_get_database_stats() |
Row counts, date ranges, file size |
fitness_get_activities(date_from, date_to, platform, sport_type, limit, offset) |
Paginated activity list |
fitness_get_activity_detail(activity_id) |
Full detail incl. raw payload |
fitness_get_personal_bests(sport_type) |
Longest, fastest, max HR, most elevation |
fitness_get_sleep(date_from, date_to, platform) |
Sleep records |
fitness_get_hrv(date_from, date_to, platform) |
HRV + body battery |
fitness_get_body_battery(date_from, date_to) |
Body battery trend |
fitness_get_recovery_status(date) |
One-day recovery snapshot |
fitness_get_training_load(weeks, platform) |
Weekly load trend |
fitness_get_vo2max_trend(months) |
VO2max estimates over time |
fitness_get_weekly_summary(week_offset) |
Week totals + sport split |
fitness_get_sport_breakdown(date_from, date_to) |
Time/distance/count per sport |
fitness_compare_platforms(metric, date_from, date_to) |
Same metric per platform |
fitness_correlate(metric_a, metric_b, date_from, date_to) |
Pearson correlation + scatter |
fitness_get_trends(metric, date_from, date_to, granularity) |
Time series by day/week/month |
fitness_query(sql) |
Read-only SELECT against the database |
Metrics for fitness_correlate / fitness_get_trends: sleep_score,
sleep_duration, hrv, hrv_score, body_battery, training_load,
distance, duration, avg_hr, stress, weight.
Tests
pip install -r requirements-dev.txt
PYTHONPATH=. pytest
The suite covers the DuckDB layer (upsert idempotency, dedup, sync log),
provider payload parsing for all four platforms, the read-only SQL guard, and
every MCP tool end-to-end against a seeded temp database — no network or
credentials required. CI runs it on every push and pull request
(.github/workflows/tests.yml).
Adding a new platform
- Create
providers/newplatform.pyextendingFitnessProvider. - Add credentials to
.env. - Register it in
sync.py:PROVIDERS["newplatform"] = NewPlatformProvider.
Done — every tool includes it automatically when platform="all".
Project status
Implemented: foundation, all four providers (Garmin, Strava, Google Fit,
Suunto), cross-platform dedup, the full tools layer (activities, health,
training, analysis, sync), and a pytest suite with CI. Google Fit has no HRV and
Suunto exposes only workouts via its public API; both still satisfy the common
FitnessProvider interface, so the tools layer treats them uniformly.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。