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
访问服务器

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.

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选