fitMCP
Pulls activities, sleep, HRV, body battery, and stress data from Garmin Connect.
Pulls activities, sleep, and weight data from Google Fit.
Pulls activity data from Strava.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@fitMCPShow my last 5 runs"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 } }Related MCP server: Garmin MCP Server
Setup
Automated (macOS / Linux)
./setup.sh # venv + deps + .env scaffold, then prints next stepsThen 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 --claudeA 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 aboveIf PowerShell blocks the script, run it once as:
powershell -ExecutionPolicy Bypass -File .\setup.ps1Then 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 configOn 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 credentialsCredentials (.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 allWindows (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 itThe 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 allRunning the MCP server
python server.py # serves over stdioClaude 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 |
| Sync garmin/strava/all into DuckDB |
| Last sync time + record counts per platform |
| Row counts, date ranges, file size |
| Paginated activity list |
| Full detail incl. raw payload |
| Longest, fastest, max HR, most elevation |
| Sleep records |
| HRV + body battery |
| Body battery trend |
| One-day recovery snapshot |
| Weekly load trend |
| VO2max estimates over time |
| Week totals + sport split |
| Time/distance/count per sport |
| Same metric per platform |
| Pearson correlation + scatter |
| Time series by day/week/month |
| 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=. pytestThe 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.
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.
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/gzarog/fitMCP'
If you have feedback or need assistance with the MCP directory API, please join our Discord server