io.github.matisdsp/fartlek
Provides a coach's morning report from Garmin Connect data, including readiness verdicts, training load (CTL/ATL/TSB), acute:chronic workload, activity analysis, and more.
Click on "Deploy 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., "@io.github.matisdsp/fartlek Can I go hard today?"
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.
Fartlek
A coach's morning report from your Garmin data, for any LLM via MCP.
Every other Garmin MCP server hands the LLM a filing cabinet of raw JSON — one night of sleep is ~52K tokens, one activity stream ~155K. The model can't read it, so it skims and improvises. Fartlek does the synthesis server-side: computed sports-science metrics (CTL/ATL/TSB, ACWR, monotony, calibrated training load), personal baselines with significance floors, safety alerts — delivered as compact, verdict-first reports the model can actually reason about.
The token contract (v0.1): calling every tool in the catalog once, at default arguments, costs under 9K tokens — a sixth of one raw Garmin sleep payload. Excluding the garmin_raw escape hatch, the whole synthesis surface sums to under 4K. Hard caps are enforced per response by the renderer, with disclosed truncation.
Status: v0.1 (Phase 1). 8 synthesis tools; the trend suite (weekly review, multi-week load, fitness/race outlook, recovery audit) ships with v0.2. Design:
docs/DESIGN.md· plan:ROADMAP.md· contributors:docs/HANDOFF.md.
The tools
Tool | What it answers | Cap |
| "How am I today — can I train hard?" Fused GREEN/AMBER/RED verdict vs your own baselines | 600 |
| Browse the log, get activity IDs | 1,300 |
| One session in depth: reps, fade, comparison to your most similar past session | 1,000–4,000 |
| Reference card: zones, PRs, goal, data coverage | 600 |
| Shoe/bike mileage vs your Garmin-set retirement limits, and what you're actually rotating | 500 |
| Tell it your goal race / phase / availability (local only) | 200 |
| Log RPE, wellness, illness/injury — the athlete outranks the sensors | 120 |
| Force refresh / deepen history backfill | 150 |
| Bounded, compacted escape hatch to named raw sources | 5,000 |
First call on a fresh install runs the cold start automatically (~30 API calls, ≈1 minute): 180 days of history, warm CTL/ATL from day 0, then background sleep/HRV backfill.
Related MCP server: claude-garmin
Quickstart
Install with either:
# uv (recommended) — runs without cloning
uvx fartlek-mcp
# or pipx
pipx install fartlek-mcpOr clone and run from source (requires Python ≥ 3.12 and uv):
git clone https://github.com/matisdsp/fartlek && cd fartlek
uv sync
# One-time Garmin login (email/password + MFA if enabled).
# Credentials are never stored; OAuth tokens go to ~/.fartlek/tokens/.
uv run fartlek auth
# Optional but recommended: warm the local store now instead of on first use
uv run fartlek sync --nights 60
uv run fartlek doctor # check everything is healthyThen point your MCP client at the server.
Any MCP-compatible client works — the server speaks standard JSON-RPC over stdio, so it is client-agnostic. The snippets below are just the per-client config formats; Claude Desktop, Claude Code, Cursor, Continue, Cline, Windsurf, Zed, VS Code (Copilot Chat), and Gemini CLI all work. The universal invocation is
uvx fartlek-mcp.
Claude Code — from this directory, .mcp.json is picked up automatically. From anywhere else:
claude mcp add fartlek -- uvx fartlek-mcpClaude Desktop — claude_desktop_config.json:
{
"mcpServers": {
"fartlek": {
"command": "uvx",
"args": ["fartlek-mcp"]
}
}
}Cursor — .cursor/mcp.json, same command/args block as above.
Continue / Cline / Windsurf / Zed — same pattern: wherever the client keeps its MCP server list, add a fartlek entry with command: "uvx", args: ["fartlek-mcp"]. Most editors adopt the Claude Desktop format verbatim.
Any other stdio MCP client — invoke the server binary directly:
fartlek-mcp # speaks JSON-RPC over stdin/stdoutAsk things like "can I go hard today?", "analyze my last run", "how did I sleep this week?" — and tell it how sessions felt: your reported RPE and illness notes gate the readiness verdict.
Docker
Build and run locally:
docker build -t fartlek-mcp .
# Tokens and store are persisted in ./fartlek-data on the host
mkdir -p fartlek-data
docker run -i --rm -v "$PWD/fartlek-data:/data" fartlek-mcpFor fartlek auth, run it interactively once to populate the volume, then use the image as the MCP server:
docker run -it --rm -v "$PWD/fartlek-data:/data" --entrypoint fartlek fartlek-mcp authCLI
Command | What it does |
| one-time Garmin Connect login (MFA supported), tokens stored locally |
| manual sync (tier 0+1, optional N-night sleep/HRV backfill) |
| check tokens, Garmin connectivity, local store health |
| list local accounts |
| export the store (consistent SQLite snapshot + CSV per table) |
| wipe all local tokens and data (asks confirmation) |
Environment: GARMINTOKENS overrides the token location, FARTLEK_HOME the data directory (default ~/.fartlek).
Releasing to PyPI (maintainers)
Releases are published via trusted publishing (OIDC) — no API tokens anywhere.
On pypi.org → Account settings → Publishing → add a GitHub publisher:
PyPI project name:
fartlek-mcp· owner:matisdsp· repo:fartlekWorkflow:
release.yml· environment:pypi
Bump
versioninpyproject.toml, commit, then tag and push:
git tag v0.1.0 && git push origin v0.1.0The release workflow builds, runs tests, and uploads to PyPI. A published version can't be overwritten — to fix a mistake, bump to the next patch (0.1.1).
Privacy
Local-first: stdio transport, your credentials and health data never leave your machine. The server only talks to Garmin's API with your own tokens, sequentially and rate-limited. fartlek export gives you everything; fartlek reset removes everything.
How Fartlek reaches your data — read this before connecting an account
Garmin has an official developer programme, and Fartlek is not part of it. Like every other open-source Garmin client, it signs in with your own credentials and reads the same endpoints the Garmin Connect apps use. Those endpoints are not published for third-party use, and Garmin's Terms of Use list, among examples of prohibited conduct, "using any process, whether automated or manual, that accesses, copies, or scrapes content from the Site through any means not purposely made available through the Site."
What that means in practice:
Your account is yours to risk. Garmin can rate-limit, block, or suspend accounts for automated access. Fartlek is deliberately polite — sequential calls, backoff on 429, and Garmin is contacted only by the sync process, never per question — but politeness is not permission.
It can break without warning. Garmin changed its login in March 2026 and broke every third-party client for weeks. This will happen again.
Read-only, by decision. Fartlek never writes to Garmin: no workouts pushed to your watch, no training plans, no edits to your activities. The two tools that write (
garmin_log,garmin_set_profile) write to the local SQLite store and nothing else. Pushing structured workouts was specified and then dropped — seedocs/DESIGN.md§2.4. Garmin's Training API is the sanctioned route for that, and it requires a cloud-to-cloud integration, which would mean your data leaving your machine.Nothing is redistributed. Your data stays on your disk. Fartlek's own responses are derived from it and are shown only to you and the LLM client you chose.
If that trade-off is not one you want to make, do not connect an account. This is stated here rather than buried, because it is the kind of thing you should decide before installing, not discover afterwards.
License & trademark
Apache 2.0. Fartlek is an independent open-source project, not affiliated with, endorsed by, or sponsored by Garmin Ltd. "Garmin" is used only to describe compatibility with Garmin Connect data.
Available Tools
14 toolsgarmin_activitiesARead-only
Browse the log and get activity IDs. One row per session, each carrying the activity_id garmin_activity accepts. Filter by date range and sport; truncation is disclosed with narrowing advice.
Inputs: limit, sport, end_date, start_date. Returns: result
garmin_activityARead-only
ONE session in depth: execution vs structure, rep-by-rep fade, decoupling, comparison to the closest past session, planned-vs-executed. Select by activity_id, by date, or omit both for the latest — add sport for the latest of that sport. 'splits' adds the lap table; 'full' adds an HR/pace curve.
Inputs: date, sport, detail, activity_id. Returns: result
garmin_athleteARead-only
Reference card: zones, thresholds, PRs, goal and phase, baselines, injury notes, device data coverage. Call once when athlete context is unknown; it changes rarely. To change it, garmin_set_profile.
Inputs: no named parameters. Returns: result
garmin_briefARead-only
Call FIRST for anything about TODAY: readiness, whether to train, current state. Zero arguments. Returns a fused go/modify/rest verdict against personal baselines, active alerts, yesterday's session with its activity_id, and today's planned workout. One session → garmin_activity; browsing → garmin_activities.
Inputs: date. Returns: result
garmin_fitnessARead-only
Is training working: VO2max and efficiency trends, HR at a fixed pace, long-run durability, a race projection against the stored goal, and form projected to race day with taper guidance. Call for 'am I getting fitter', race planning, taper timing, goal feasibility. Set the goal with garmin_set_profile.
Inputs: weeks, anchor_date. Returns: result
garmin_loadARead-only
Multi-week dose: fitness/fatigue/form (CTL/ATL/TSB), ramp rate, ACWR, monotony/strain, and intensity drift vs this athlete's own norm. Call for 'am I training too much', ramp/taper dosing, periodization. Not single-day readiness (garmin_brief); overtraining physiology is garmin_recovery.
Inputs: weeks, anchor_date. Returns: result
garmin_logAIdempotent
Subjective data the watch cannot capture: session RPE (1-10), Hooper wellness (fatigue, soreness, stress, mood, sleep quality, each 1-7), and notes — especially illness or injury (set flag; resolve when healed). Feeds sRPE load and caps the readiness verdict. Ask for RPE after discussing a session if missing.
Inputs: rpe, date, flag, mood, +7 more. Returns: result
garmin_rawARead-only
Bounded escape hatch to one named Garmin source, compacted and hard-capped. Use ONLY when a synthesis tool cannot answer and the user explicitly asks for raw values. Never a starting point.
Inputs: source (required), date, series, max_points, +1 more. Returns: result
garmin_recoveryARead-only
Sleep, HRV, resting HR and load structure vs personal baselines, plus the multi-marker overtraining audit. Call for tiredness, sleep, 'am I overtraining or getting sick', or when another tool flags recovery. OWNS overtraining questions. Single-day go/no-go is garmin_brief.
Inputs: days, anchor_date. Returns: result
garmin_referenceARead-only
How a number was computed and whether to trust it: formula, inputs, whether each threshold is a population default or personally derived, and the caveats. No arguments for the index; metric='acwr' for one in depth.
Inputs: topic, metric. Returns: result
garmin_set_profileAIdempotent
Athlete context the watch cannot know: goal race (date; a distance, or a fixed-time event like 24h with a target distance), phase, weekly availability, intensity preference, LT1 override. Local only; only provided fields change. Injuries and illness go to garmin_log.
Inputs: phase, goal_time, phase_week, tid_target, +7 more. Returns: result
garmin_syncA
Force a refresh and report freshness, or start a resumable historical backfill (backfill_days > 0, deepens sleep/HRV history). Use only if data looks stale — every other tool auto-refreshes.
Inputs: backfill_days. Returns: result
garmin_weekARead-only
One week in session-level detail: load vs recent weeks, intensity distribution, a per-day session table with activity_ids, recovery summary, and plan compliance where a plan exists. Call for 'how was my week' or a specific week. Multi-week trajectory is garmin_load.
Inputs: anchor_date. Returns: result
garmin_whats_changedARead-only
Call for 'anything I should know?', 'what's new?', 'catch me up', or after days away. Scans every tracked metric and returns ONLY statistically significant changes, ranked safety-first; says 'nothing notable' when nothing tripped. Today's readiness is garmin_brief.
Inputs: since_days. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 14 tools
Each tool has a clearly distinct purpose with explicit cross-references (e.g., garmin_brief owns today's readiness, garmin_recovery owns overtraining, garmin_load owns multi-week training dose). No two tools appear to do the same thing; even similar tools like garmin_activities/garmin_activity are distinguished by singular vs plural and description.
All tools share the garmin_ prefix and lowercase snake_case, but the pattern mixes nouns (garmin_activity, garmin_week), verbs (garmin_sync), verb phrases (garmin_set_profile), and a question (garmin_whats_changed). Read vs write is somewhat predictable (nouns for queries, verbs for actions), but not uniform.
14 tools is within the ideal 3-15 range and each tool covers a distinct aspect of Garmin data analysis: from daily brief to weekly summary, fitness trends, recovery, load, and raw data. The size feels justified for the domain.
The server covers the full workflow of an athletic coaching assistant: profile management, subjective logging, daily readiness, activity browsing and deep-dive, fitness/recovery/load analysis, week summaries, change detection, and reference/raw data escape hatches. Minor gaps exist (e.g., no direct plan editing or log deletion), but they are outside the stated purpose.
Related MCP Connectors
Adaptive running coach MCP server — training data, plans, and recovery for AI assistants.
tri-train is a training platform for triathletes that syncs activities from Strava and Garmin and exposes them to Claude through a remote MCP server. Ask Claude how your last block went, why your run pace dropped, or whether you are fresh enough for Sunday's race, and it answers from your real data: activities with laps and grade-adjusted pace, weekly load and the performance management chart (CTL/ATL/TSB), sleep, HRV and resting heart rate, and your race calendar with predictions. Claude can also act: create and adjust planned workouts with structured intervals, push them to your Garmin watch, add races and write notes. Every change is journaled and can be undone. Read and write access are separate OAuth scopes, and the server enforces them on every call. Built and hosted in Switzerland by a triathlete; data stays on our own servers (Swiss revDSG / GDPR).
Apple Health training load, recovery, HRV and workout detail for Claude, ChatGPT and any MCP client.
Multi-tenant hosted MCP server for Oura Ring — 21 read-only tools, OAuth per user.
Related MCP Servers
- FlicenseBqualityDmaintenanceA Model Context Protocol server that integrates Garmin Connect data with LLMs to provide personalized running analysis and training plans. It enables users to monitor performance metrics, manage training loads, and receive data-driven workout suggestions based on health indicators like VO2 Max and recovery status.435-
- AlicenseNot gradedqualityCmaintenanceMCP server that connects Garmin Connect data to Claude, enabling training analysis, recovery checks, and personalized plans based on real metrics like HRV, training load, and activities.24MIT
- AlicenseAqualityCmaintenanceA Model Context Protocol (MCP) server for Garmin Connect integration. Access your activities, health data, training metrics, and more through Claude and other LLMs.2268MIT
- AlicenseBqualityBmaintenanceAn MCP server that gives LLMs access to Garmin Connect data, including training, recovery, sleep, stress, VO2 Max, and running summaries for personalized fitness advice.2442 PyPI1MIT