coach-mcp
by snoozelieb
README.md
# coach-mcp
<!-- mcp-name: io.github.snoozelieb/coach-mcp -->
[](https://github.com/snoozelieb/coach-mcp/actions/workflows/ci.yml)
[](LICENSE)
An opinionated AI training coach as an MCP server. It pulls your real data from
Garmin Connect and **prescribes with authority** — science-based load management
(ACWR), **code-enforced injury gates** (the server rejects plans that violate an
active injury restriction, no matter what the LLM says), and **persistent
coaching memory** so decisions, rationale, and your adaptation patterns survive
between conversations. It will tell you "no" when your enthusiasm exceeds your
capacity.
All health data and credentials stay on your machine — see
[Security & Privacy](#security--privacy).
## Quickstart
You need Python 3.12+, a free Garmin Connect account, and an MCP client
(Claude Code, Claude Desktop, or Cursor).
### Option A: uvx (recommended)
No install step — your MCP client runs the server on demand:
```bash
uvx garmin-coach-mcp
```
Jump to [Connect your MCP client](#connect-your-mcp-client) and use `uvx` as
the command.
### Option B: from source
```bash
git clone https://github.com/snoozelieb/coach-mcp.git
cd coach-mcp
python -m venv .venv
# Linux/macOS:
source .venv/bin/activate
# Windows:
.venv\Scripts\activate
pip install -r requirements.txt
cp .env.example .env # then edit: GARMIN_EMAIL, GARMIN_PASSWORD
python server.py
```
## Connect your MCP client
The server needs two environment variables: `GARMIN_EMAIL` and
`GARMIN_PASSWORD`. Optional: `COACH_DATA_DIR` (where your coaching data lives)
and `ANTHROPIC_API_KEY` (only for the standalone `daily_loop.py --llm` script).
From a source checkout, a `.env` file works too.
### Claude Code
```bash
claude mcp add coach-mcp \
--env GARMIN_EMAIL=you@example.com \
--env GARMIN_PASSWORD=your_garmin_password \
-- uvx garmin-coach-mcp
```
Or in `.mcp.json`:
```json
{
"mcpServers": {
"coach-mcp": {
"command": "uvx",
"args": ["garmin-coach-mcp"],
"env": {
"GARMIN_EMAIL": "you@example.com",
"GARMIN_PASSWORD": "your_garmin_password",
"COACH_DATA_DIR": "/path/to/your/coach-data"
}
}
}
}
```
Running from source instead: `claude mcp add coach-mcp -- python /full/path/to/coach-mcp/server.py`
### Claude Desktop
In `claude_desktop_config.json` (Settings → Developer → Edit Config):
```json
{
"mcpServers": {
"coach-mcp": {
"command": "uvx",
"args": ["garmin-coach-mcp"],
"env": {
"GARMIN_EMAIL": "you@example.com",
"GARMIN_PASSWORD": "your_garmin_password",
"COACH_DATA_DIR": "/path/to/your/coach-data"
}
}
}
}
```
### Cursor
In `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
```json
{
"mcpServers": {
"coach-mcp": {
"command": "uvx",
"args": ["garmin-coach-mcp"],
"env": {
"GARMIN_EMAIL": "you@example.com",
"GARMIN_PASSWORD": "your_garmin_password",
"COACH_DATA_DIR": "/path/to/your/coach-data"
}
}
}
}
```
If you installed with `pip install garmin-coach-mcp` instead of uvx, use
`"command": "garmin-coach-mcp"` with no args in any of the blocks above.
## First run
1. **Create your profile.** From a source checkout, run the interactive wizard:
```bash
python scripts/setup_wizard.py
```
It creates your athlete profile, training config, and empty plan/memory
files in the data directory. Alternatively, create the two required files
by hand and let the coach fill in the rest via conversation:
```bash
echo '{"personal":{"name":null},"injury_history":[],"life_constraints":{}}' > data/athlete.json
echo '{"events":[],"current_block":{"phase":"base"}}' > data/training_config.json
```
2. **Pull your Garmin baseline.** In your MCP client, say:
> "Run refresh_athlete_baseline and set up my training."
The coach pulls your name, weight, age, HR data, and training capacity from
Garmin, then starts the onboarding conversation — goals, constraints,
injury history, race calendar.
3. **Garmin MFA / expired session.** Garmin logins are token-cached. If tools
start returning `AUTH_REQUIRED`, recover with:
```bash
python scripts/garmin_login.py
```
It does a fresh credential login, prompts for the MFA code if Garmin asks,
and saves new tokens. Restart the MCP server afterwards.
## How it works
1. **Snapshot first** — every coaching conversation starts from
`get_coaching_snapshot()`: current time context, 7-day week grid (rest days
explicit), fitness metrics, plan adherence, open anomalies, injuries, sleep
gate.
2. **Load hierarchy before prescribing** — overall ACWR (injury gate, 0.8–1.3
sweet spot), then sport-specific ACWR (spike detection), then
sport-specific CTL (race readiness).
3. **Hard gates are code, not vibes** — `update_weekly_plan` and
`push_plan_to_garmin` reject sessions that violate an active injury's
restricted activities, and every non-rest session must carry a `purpose` or
the save is refused.
4. **Curiosity with memory** — planned-vs-actual anomalies (missed session,
type mismatch, activity on a rest day) register once with a lifecycle
(open → asked → resolved); the coach asks you what happened instead of
silently assuming.
5. **Everything persists** — decisions, approvals, adaptation patterns, and
season lifecycle (race debriefs, phase transitions) live in local JSON and
carry across sessions.
## MCP surface
49 tools — you don't call them directly; the coach uses them during
conversation:
| Category | Tools |
|----------|-------|
| **Coaching core** | `get_coaching_snapshot` (canonical, sectioned), `get_compliance_report`, `get_coaching_score` |
| **Planning** | `get_weekly_plan`, `update_weekly_plan`, `push_plan_to_garmin`, `get_week_constraints`, `get_weekly_prescription`, `get_periodization_status`, `update_phase` |
| **Garmin data** | `query_metrics` (kind=fitness/intensity/daily/readiness/personal_records), `get_activities_range` |
| **Athlete** | `get_athlete`, `update_athlete`, `set_ftp`, `set_threshold_pace`, `analyze_ftp_test`, `refresh_athlete_baseline`, `refresh_fitness_history`, `get_onboarding_guide` |
| **Methodology** | `get_methodology`, `update_methodology` |
| **Races** | `races` (action=list/add/update/research), `remove_race` |
| **Strength** | `sync_strength_session`, `get_strength_baseline`, `approve_progression`, `set_exercise_preference`, `generate_strength_workout`, `add_exercise` |
| **Injuries** | `diagnose_injury`, `research_injury`, `update_injury_status` |
| **Research** | `research_exercise`, `list_exercises`, `research_sport` |
| **Memory** | `log_coaching_decision`, `get_active_decisions`, `update_decision_status`, `record_athlete_response`, `get_response_patterns`, `resolve_anomaly` |
| **Approvals** | `propose_coaching_action`, `list_pending_approvals`, `approve_proposal`, `reject_proposal` |
| **Interactive** | `generate_smart_brief`, `interactive_check_in` |
Every tool carries MCP annotations (read-only / destructive / idempotent /
open-world), enforced by tests.
**5 prompts**: `weekly_planning`, `morning_brief`, `injury_assessment`,
`week_review`, `onboarding`.
**6 resources**: `coach://athlete/profile`, `coach://plan/current`,
`coach://config/training`, `coach://coaching/decisions`, `coach://context/now`,
`coach://coaching/doctrine` (the long-form coaching doctrine).
## Security & Privacy
Everything stays on your machine:
- **Credentials**: `GARMIN_EMAIL`/`GARMIN_PASSWORD` live in your MCP client
config or a local `.env`. Garmin OAuth tokens are cached in a local token
store (`.garth/` in a source checkout, a per-user `garmin-tokens` directory
for installed copies; override with `COACH_TOKEN_DIR`).
- **Health data**: all coaching data (profile, plans, fitness history, sleep,
coaching memory) is local JSON in your data directory. There is no backend,
no telemetry, no analytics.
- **What leaves your machine**: requests to Garmin's own API (your
credentials/tokens, sent only to Garmin); whatever your MCP client sends to
its LLM as part of the conversation; optional public web-page fetches when
the coach researches a race, injury, or exercise; and, only if you run
`daily_loop.py --llm`, one request to the Anthropic API.
- **Single athlete per data directory** by design. For multiple athletes, run
separate server instances with separate `COACH_DATA_DIR`s.
See [SECURITY.md](SECURITY.md) for details and how to report issues.
### Data directory
Resolution order: `COACH_DATA_DIR` env var → `data/` in a source checkout → a
per-user data directory (created on first run for installed packages). The
only file shipped with the package is `methodology.json` (safety rules, race
templates, personas); everything personal is created locally and never
committed.
## Advanced
```bash
# HTTP transport (streamable-http) instead of stdio
COACH_TRANSPORT=streamable-http FASTMCP_PORT=8000 garmin-coach-mcp
# Code Mode (search/execute meta-tools instead of 49 individual tools)
pip install fastmcp[code-mode]
COACH_CODE_MODE=1 garmin-coach-mcp
# Standalone morning audit
python scripts/daily_loop.py # template-based brief
python scripts/daily_loop.py --llm # LLM brief (needs ANTHROPIC_API_KEY)
# Tests (1,333 tests; clean checkouts use committed sanitized fixtures)
pip install -r requirements-dev.txt
python -m pytest -q
```
## Architecture
`server.py` registers tools from the `coach/` package (11 tool modules, pure
parsers, a typed pydantic storage layer, CTL/ATL/ACWR fitness math, a Garmin
client with token-first auth, and a workout builder that pushes structured
workouts to your watch). The project went through a five-phase modernization —
auth rebuild, schema layer, hard gates, sectioned snapshot, packaging — whose
full history and rationale live in
[docs/UPGRADE_ROADMAP.md](docs/UPGRADE_ROADMAP.md). Development conventions
are in [CLAUDE.md](CLAUDE.md).
## License
[MIT](LICENSE)
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues