fitMCP

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.

Category
Visit Server

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_query enforces 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)

  1. Create an API application at https://www.strava.com/settings/api. Note the Client ID and Client Secret.

  2. Authorize your own account, requesting the activity:read_all scope. Visit (replace CLIENT_ID):

    https://www.strava.com/oauth/authorize?client_id=CLIENT_ID&response_type=code&redirect_uri=http://localhost&approval_prompt=force&scope=activity:read_all
    

    After approving, the browser redirects to http://localhost/?...&code=AUTH_CODE&.... Copy AUTH_CODE from the URL.

  3. 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_code
    

    Put the refresh_token from the response into STRAVA_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 token

After that you can leave GARMIN_PASSWORD out of .env; syncs reuse the cached session token. (Setting the password in .env still 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.py reads it via getpass and only persists the resulting session token. Keep GARMIN_PASSWORD blank.
  • Owner-only token cache — after every Garmin login the GARTH_HOME directory and its token files are chmod'd to 0700/0600 (POSIX).
  • Exposure warning — sync.py and server.py warn on startup if your .env is group/world-readable, with the exact chmod 600 fix.
  • Treat session tokens like a password — a cached garth token grants access to your Garmin account. If a machine is compromised, delete GARTH_HOME and re-run login.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

  1. Create providers/newplatform.py extending FitnessProvider.
  2. Add credentials to .env.
  3. 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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured