Paceforge Garmin
БесплатноНе проверенEnables MCP hosts to read Garmin training data and write structured workouts to a calendar, with tools for listing activities, summarizing training, computing a
Описание
Enables MCP hosts to read Garmin training data and write structured workouts to a calendar, with tools for listing activities, summarizing training, computing athlete zones, building and scheduling workout plans, syncing data, and checking cache status.
README
PaceForge
An offline-first AI running coach. A local LLM agent that reasons over your Garmin and Strava training data through the Model Context Protocol — and a deterministic sports-science core that does the actual math.
No API keys. No cloud inference. Your training data never leaves your machine.
Why this exists
Most "AI coach" projects are a prompt wrapped around a chat API. They send your health data to a third party, they hallucinate paces, and they can't tell you why they told you to run 6 x 800m.
PaceForge is built the other way around:
| Typical LLM coach | PaceForge | |
|---|---|---|
| Inference | Cloud API, per-token billing | Local Ollama, $0, works on a plane |
| Your health data | Uploaded to a vendor | Never leaves the machine |
| Paces & load | Generated by the model | Computed by tested code; the model may not do arithmetic |
| Tool access | Bespoke function calls | Standard MCP servers any host can reuse |
| Advice provenance | "Trust me" | Every finding maps to a named threshold + a cited corpus passage |
| Failure mode | Confident nonsense | Confidence scores, explicit "not enough data" |
The interesting engineering claim: the language model is the interface, not the reasoning engine. VDOT, training load, injury risk, and periodisation are deterministic, unit-tested functions. The LLM's job is to pick the right tool, read the result, and explain it like a coach. That split is what makes the output auditable and what makes a 7B model good enough to run it.
Architecture
flowchart TB
subgraph Interface
CLI["CLI · paceforge chat / ask / analyze"]
HOST["Any MCP host<br/>Claude Desktop, IDEs"]
end
subgraph Agent["Agent runtime"]
LOOP["Bounded ReAct loop<br/>tool dedup · forced-answer fallback"]
MEM["Rolling memory<br/>compaction for 8K windows"]
LLM["Ollama<br/>native tools + prompt fallback"]
end
subgraph MCP["MCP layer (stdio JSON-RPC)"]
GS["paceforge-garmin-mcp<br/>7 tools · read + write"]
SS["paceforge-strava-mcp<br/>6 tools · read + analysis"]
end
subgraph Core["Deterministic core"]
VDOT["vdot.py<br/>Daniels/Gilbert · Riegel"]
MET["metrics.py<br/>TRIMP · EWMA ACWR"]
AN["analysis.py<br/>rule-based findings"]
PL["planner.py<br/>periodisation + invariants"]
RAG["rag/<br/>hybrid dense + BM25"]
end
subgraph Data
CACHE[("SQLite cache<br/>offline source of truth")]
GAR["Garmin Connect"]
STR["Strava v3"]
end
CLI --> LOOP
HOST --> GS
HOST --> SS
LOOP <--> LLM
LOOP <--> MEM
LOOP --> GS
LOOP --> SS
GS --> Core
SS --> Core
Core --> CACHE
GAR -. optional sync .-> CACHE
STR -. optional sync .-> CACHE
GS -. structured workouts .-> GAR
Data flows one way in, one way out. Providers normalise vendor JSON into canonical Pydantic
models exactly once, at the boundary. Everything downstream — metrics, analysis, planner, MCP
tools — sees metres, seconds, and seconds-per-kilometre, never averageRunningCadenceInStepsPerMinute.
Quickstart
# 1. Local model runtime
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:7b-instruct
# 2. Install (python -m pip, so it lands in the Python you actually run)
git clone https://github.com/yashhooda1/paceforge.git
cd paceforge
python -m pip install -e ".[dev]"
# 3. Load a year of realistic synthetic training data (no credentials needed)
paceforge seed
paceforge index
# 4. Check everything is wired up
paceforge doctor
Now use it:
paceforge analyze # deterministic coaching report — no LLM required
paceforge zones # VDOT, pace bands, HR zones
paceforge plan 2026-11-15 # periodised 5K block to race day
paceforge ask "am I overtraining?" --trace
paceforge chat # interactive session
paceforge analyze runs with no model at all — the sports-science core is independent of the
LLM. That is deliberate: the numbers must be right whether or not a model is running.
'paceforge' is not recognized? The console script lives in your Python'sScripts/(Windows) orbin/(POSIX) directory, which is often not onPATH. Every command also works aspython -m paceforge <command>—python -m paceforge analyze,python -m paceforge chat, and so on. That form is PATH-independent and always runs the same interpreter you invoked. See Troubleshooting.
Connect your real data
cp .env.example .env # fill in Garmin / Strava credentials
paceforge sync garmin --days 365
paceforge sync strava --days 365
Credentials are optional throughout. Without them the system runs cache-only, which is the normal offline path.
Use the MCP servers from any host
The Garmin and Strava tools are real MCP servers, not internal functions. Point Claude Desktop — or any MCP client — at them:
// claude_desktop_config.json
{
"mcpServers": {
"garmin": { "command": "paceforge-garmin-mcp" },
"strava": { "command": "paceforge-strava-mcp" }
}
}
| Server | Tools |
|---|---|
| garmin | garmin_list_activities, garmin_training_summary, garmin_athlete_zones, garmin_build_workout_plan, garmin_schedule_workouts, garmin_sync, garmin_cache_status |
| strava | strava_analyze_training, strava_predict_race, strava_list_activities, strava_weekly_volume, strava_sync, strava_cache_status |
Two servers rather than one on purpose: shorter tool lists measurably improve tool-selection accuracy on small local models, and it keeps the read path (Strava) separable from the device-write path (Garmin).
garmin_schedule_workouts writes to a real calendar, so it defaults to dry_run=true and the
system prompt requires explicit athlete approval before a live write.
What the coaching core actually computes
| Module | What it does | Why it's written this way |
|---|---|---|
coaching/vdot.py |
Daniels & Gilbert oxygen-cost model, VDOT from a race, inverse solver for predicted times, Riegel blending | Forward/inverse solvers are property-tested as true inverses with Hypothesis |
coaching/metrics.py |
Banister TRIMP, pace-based fallback load, EWMA acute/chronic load, ACWR, time-weighted intensity distribution | EWMA, not rolling sums — a 7-day window produces a step discontinuity a body never feels. Load falls back to pace so strap-less runs don't bias ACWR downward |
coaching/analysis.py |
Rule engine producing severity-ranked Finding objects with evidence |
Every threshold is a named constant. The LLM explains findings; it never invents them |
coaching/planner.py |
5K periodisation (base → build → peak → taper) with 3-up/1-down volume | Structural invariants: ≤10% weekly ramp, no back-to-back quality days, taper cuts volume and holds intensity. Asserted, not hoped for |
rag/ |
Hybrid retrieval — cosine + BM25 — over a bundled running-science corpus | Small jargon-dense corpus: "ACWR" and "acute chronic workload ratio" must match, and so must an exact term like "Riegel". Linear scan is genuinely faster than an ANN index at a few hundred chunks |
Embeddings degrade gracefully: local Ollama embedding model → sentence-transformers if cached →
a dependency-free hashing embedder that needs no download at all. The index records which backend
produced it and rebuilds automatically if that changes, so incompatible vectors are never compared.
Engineering notes
Things in here that exist because the naive version broke:
Tool-calling fallback. Not every local model emits native tool_calls. When Ollama reports no
tool support, llm/ollama.py switches to a prompt-encoded protocol and parses a fenced JSON block
out of the reply — transparently, so the agent above never learns which path was taken.
Loop breaking. Small models re-call the same tool forever. The agent hashes
(name, arguments); a repeat returns the cached result plus an explicit instruction to answer.
Iteration exhaustion doesn't return "max iterations reached" — it makes one final tool-free call so
the athlete always gets a real answer.
Context compaction. A 7B at 8K tokens overflows in a handful of turns of verbose JSON, and the first thing it forgets is the system prompt — which is exactly when it starts inventing paces. Tool results are truncated on ingest and older turns are summarised into a pinned brief.
Cadence units. Strava reports one leg, Garmin reports both. Merging the feeds without doubling Strava's value produces a confident, wrong "low cadence" finding on every Strava run. Normalised at the provider boundary.
MCP SDK 1.x and 2.x. The SDK's 2.0 release renamed FastMCP to MCPServer and
Tool.inputSchema to Tool.input_schema. An open upper bound turned that into a red CI run, which
is exactly what CI is for. Rather than pinning to 1.x and rotting, mcpx/_compat.py normalises the
differences and CI runs the MCP suite against both majors, so the claim stays true.
Structured logging. Every step emits a keyed event — llm.request, tool.call,
agent.iteration — so an agent run is a queryable trace rather than prose. --log-format json
pipes straight into any log store.
Testing
make test # pytest + coverage
make lint # ruff
make typecheck # mypy --strict
make check # all three, as CI runs them
The suite covers the parts where being wrong matters: VDOT round-trips under Hypothesis, ACWR behaviour on constructed load histories, planner invariants (ramp cap, hard/easy spacing, taper shape) asserted across many generated plans, retrieval quality against a fixed eval set, and the agent loop driven by a scripted fake model so tool-selection and loop-breaking are tested without a GPU.
Project layout
src/paceforge/
├── agent/ ReAct loop, rolling memory, prompts
├── llm/ ChatModel protocol + Ollama backend
├── mcpx/
│ ├── servers/ Garmin & Strava MCP servers (stdio)
│ ├── client.py MCP host + in-process fallback ToolSource
│ ├── toolkit.py Tool implementations, shared by both paths
│ └── _compat.py Shim across MCP SDK 1.x / 2.x
├── coaching/ VDOT, metrics, analysis rules, planner
├── rag/ Embeddings + hybrid SQLite vector store
├── providers/ Garmin / Strava adapters + normalisation
├── storage/ SQLite cache, migrations, synthetic seed
└── corpus/ Running-science knowledge base (markdown)
The Ollama model (persona only)
ollama run hoodarunner/paceforge-coach
ollama/Modelfile packages the coaching persona — the training philosophy, the
session-purpose reasoning, the tone — over qwen2.5:7b-instruct. It is not
PaceForge: it has no tools, so it cannot read your Garmin, compute your VDOT, or
calculate your ACWR. The system prompt makes that self-enforcing — asked for a
number it cannot know, it asks you for the input or points at paceforge analyze
rather than inventing one, and the push script refuses to publish a build that
fails that check.
The package is the coach. The model is the voice. See ollama/README.md.
Roadmap
- Sleep and HRV ingestion for readiness-adjusted daily prescription
- Per-stream analysis (splits, GAP on elevation) rather than session summaries
- Fine-grained plan adaptation from completed-vs-prescribed deltas
- Coros and Polar providers behind the existing
ActivityProviderprotocol - Optional local web UI over the same MCP tools
Troubleshooting
'paceforge' is not recognized as an internal or external command
The package installs a console script, but its directory may not be on PATH — common on Windows,
and common anywhere with several Pythons installed (conda, a venv, the system one). Two fixes:
Use the module form. Always works, no PATH changes, and guarantees you are running the same interpreter you typed:
python -m paceforge seed
python -m paceforge analyze
python -m paceforge chat
Or put the scripts directory on PATH. First confirm the package is actually installed:
python -c "import paceforge; print(paceforge.__version__)"
If that fails, the install did not land in this interpreter — re-run python -m pip install -e ".[dev]"
(python -m pip, not bare pip, so pip and python cannot disagree). If it prints a version, find the
scripts directory and add it to PATH:
# Windows
python -c "import sysconfig; print(sysconfig.get_path('scripts'))"
# macOS / Linux
python -c "import sysconfig; print(sysconfig.get_path('scripts'))"
ollama pull succeeded but paceforge doctor says the model is unreachable
Ollama must be running, not merely installed — start the Ollama app (or ollama serve). Confirm
with curl http://localhost:11434/api/tags. If Ollama listens on a non-default host or port, set
PACEFORGE_OLLAMA_HOST in .env.
Load and heart-rate zones look wrong
Set PACEFORGE_ATHLETE_RESTING_HR and PACEFORGE_ATHLETE_MAX_HR in .env to your real values.
TRIMP and every HR zone are computed from your heart-rate reserve, so the 50/190 defaults will skew
every load number if they are not close to yours.
paceforge analyze says "Not enough recent running"
The cache is empty or thin. Run paceforge seed for synthetic data, or
paceforge sync garmin --days 365 once credentials are in .env.
Disclaimer
PaceForge is a training-analysis tool, not medical advice. Focal bone pain, pain that alters your gait, or anything persisting beyond two weeks belongs with a sports-medicine professional.
License
MIT — see LICENSE.
Built by Yash Hooda · Data/AI Engineer, long-distance runner.
Установка Paceforge Garmin
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/yashhooda1/paceforgeFAQ
Paceforge Garmin MCP бесплатный?
Да, Paceforge Garmin MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Paceforge Garmin?
Нет, Paceforge Garmin работает без API-ключей и переменных окружения.
Paceforge Garmin — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Paceforge Garmin в Claude Desktop, Claude Code или Cursor?
Открой Paceforge Garmin на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
Notion
Read and write pages in your workspace
автор: NotionLinear
Issues, cycles, triage — from Claude
автор: LinearGoogle Drive
Search and read your Drive files
автор: Googlemindsdb/mindsdb
Connect and unify data across various platforms and databases with [MindsDB as a single MCP server](https://docs.mindsdb.com/mcp/overview).
автор: mindsdbfulcradynamics/fulcra-context-mcp
MCP server for accessing personal health and biometric data including sleep stages, heart rate, HRV, glucose, workouts, calendar, and location via the Fulcra Li
автор: fulcradynamicsaymericzip/intlayer
A MCP Server that enhance your IDE with AI-powered assistance for Intlayer i18n / CMS tool: smart CLI access, access to the docs.
автор: aymericziprinadelph/Agent-MCP
A framework for creating multi-agent systems using MCP for coordinated AI collaboration, featuring task management, shared context, and RAG capabilities.
автор: rinadelphWhenLabs-org/when
Developer toolkit: auto-detect stack for AI context files, catch port conflicts, validate .env schemas, spot docs drift, audit dependency licenses, and time cod
автор: WhenLabs-orgBeltran12138/wecom-docs-mcp-server
WeCom (Enterprise WeChat) document operations via MCP: create, read, and edit Docs and Smartsheets (9 tools). Fills the doc-CRUD gap — existing WeCom MCP server
автор: Beltran12138madbonez/caldav-mcp
Universal MCP server for CalDAV protocol integration. Works with any CalDAV-compatible calendar server including Yandex Calendar, Google Calendar (via CalDAV),
автор: madbonezCompare Paceforge Garmin with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории productivity
