Threshold
Health Warn
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Pass
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Garmin MCP server: connect Garmin Connect to Claude, ChatGPT and any AI. Readiness, HRV, training load, FIT analysis. Free on Cloudflare Workers.
Threshold: Garmin MCP Server
Your Garmin data, analyzed by Claude, ChatGPT and any AI assistant.
An open-source Garmin MCP server on the official Garmin Personal API. It answers questions about your training, sleep, HRV and recovery, with numbers computed by code, not guessed by the model.
Website · Quick start · Connect your AI · FAQ
"Am I recovered enough for intervals today?" · "Is my HRV trending down?" · "Am I ramping up too fast?" · "Break down this morning's run." · "What are my 5k and 10k bests this year?" · "Which months did I climb the most?"
Threshold is a self-hosted Model Context Protocol (MCP) server that connects Garmin Connect to AI assistants. It runs on your own free Cloudflare account and talks to Garmin's official Personal API with your own key. No password, no scraping, no third-party service in between. It works with Claude (web, desktop, iOS, Android), ChatGPT, Claude Code, Cursor, VS Code, Codex, Gemini CLI, Windsurf and any MCP client.
Why Threshold
Other Garmin MCP servers hand the model raw JSON and hope it does the math. Threshold does the math.
| Typical Garmin MCP | Threshold | |
|---|---|---|
| Readiness, training load (CTL/ATL/TSB, ACWR), HRV baseline | model guesses from raw JSON | computed in code, rules shown |
| Aerobic decoupling, cardiac drift, best efforts, power curve | no | from the original FIT file |
| VO2 max and performance condition hidden from the API | no | decoded from the FIT file |
| Full-history archive with SQL | no | D1 archive, read-only SQL tool |
| Works on your phone (Claude / ChatGPT mobile) | rarely | OAuth built in |
| Hosting | your laptop must stay on | Cloudflare free tier, one click |
| Context-window friendly | 100 fields per item | compact, unit-labelled output |
| Privacy | GPS and names in every response | GPS and identifiers removed by default |
What you can ask
| Tool | Answers |
|---|---|
daily_briefing |
"How am I today?" HRV, resting HR, sleep, Body Battery, form and load ramp against your 60-day baselines, with a green / amber / red call and the rule behind it. |
recovery_analysis |
HRV on a personal baseline (ln rMSSD, 7-day rolling mean, normal band = baseline ± 0.5 SD), resting-HR drift, sleep versus usual. |
training_load |
Fitness (CTL), fatigue (ATL), form (TSB), acute:chronic ratio, monotony, strain, weekly load, load by sport. |
weekly_report |
Week-by-week volume, load, intensity split (80/20?), sleep, HRV, resting HR, and change versus last week. |
metric_trend |
Any metric over time (HRV, resting HR, sleep, weight, VO2 max, running pace, efficiency...), slope, min/max, and comparison with the previous period. |
analyze_activity |
One workout from its FIT file: splits, laps, HR zones, best efforts, decoupling and drift, NP/IF/TSS, efficiency factor, hidden VO2 max. |
personal_records |
Fastest 400 m to marathon, 10–100 km rides, swim bests, power curve 5 s–60 min, longest and hilliest sessions. |
list_activities |
Activities by date, sport, type or name, one compact line each. |
query_archive |
Read-only SQL over your whole history, for everything else. |
get_health_data, get_day_detail, get_activity, get_profile |
Garmin's own records, compacted: sleep stages, HRV readings, heart rate, SpO2, stress, Body Battery, weight, hydration, nutrition, menstrual cycle, fitness age. |
sync_now, server_status, garmin_api_get |
Pull fresh data now, health checks and archive coverage, and raw access to any Personal API path. |
Clients that support MCP prompts also get morning check-in, weekly review, analyze last workout and race readiness.
Quick start (5 minutes)
You need a free Cloudflare account and a Garmin watch that syncs to Garmin Connect.
1. Get your Garmin key. Open Garmin Connect → Personal API, click Create, and copy the key. Garmin shows it only once.
2. Deploy. Click the button, sign in to Cloudflare, and finish the deploy.
3. Add your two keys. In Cloudflare open Workers & Pages → threshold → Settings → Variables and secrets → Add variable. Add each one with type Secret, then click Deploy:
| Name | Value |
|---|---|
GARMIN_API_KEY |
the key from step 1 |
ACCESS_KEY |
a password you make up, 20+ characters. You type it once per app you connect. |
4. Open your server. Cloudflare shows its address when the deploy finishes: https://threshold.<you>.workers.dev. The page says Ready and shows your MCP URL. If a key is missing, the page tells you exactly where to add it (and suggests a strong access key).
5. Connect Claude. Settings → Connectors → Add custom connector → paste the MCP URL → enter your access key. Other apps are below.
Your history fills in by itself in the background.
Add or change a key laterCloudflare dashboard → Workers & Pages → threshold → Settings → Variables and secrets → Add. Choose type Secret, enter the name (GARMIN_API_KEY or ACCESS_KEY) and the value, then Deploy.
git clone https://github.com/KareemmSayed/Threshold.git && cd Threshold
npm ci
npx wrangler deploy # creates the database and KV store on first deploy
npx wrangler secret put GARMIN_API_KEY # paste your Garmin key
npx wrangler secret put ACCESS_KEY # paste your access key
Connect your AI
Your MCP URL is https://threshold.<your-subdomain>.workers.dev/mcp. Apps with a connector screen sign in with OAuth: they open your server's page, you type your access key, done. Apps with a config file send the access key as a bearer token.
Settings → Connectors → Add custom connector → paste the MCP URL → Connect, then enter your access key on the page that opens. Add it once on claude.ai and it is on your phone too.
ChatGPTSettings → Apps & Connectors → Advanced → turn on Developer mode. Then Create a connector, paste the MCP URL, choose OAuth, and enter your access key when asked.
Claude Codeclaude mcp add --transport http threshold https://threshold.<you>.workers.dev/mcp \
--header "Authorization: Bearer <ACCESS_KEY>"
Cursor (~/.cursor/mcp.json), Windsurf (~/.codeium/windsurf/mcp_config.json)
{
"mcpServers": {
"threshold": {
"url": "https://threshold.<you>.workers.dev/mcp",
"headers": { "Authorization": "Bearer <ACCESS_KEY>" }
}
}
}
Windsurf names the field serverUrl instead of url.
.vscode/mcp.json)
{
"servers": {
"threshold": {
"type": "http",
"url": "https://threshold.<you>.workers.dev/mcp",
"headers": { "Authorization": "Bearer ${input:threshold-key}" }
}
},
"inputs": [{ "id": "threshold-key", "type": "promptString", "description": "Threshold access key", "password": true }]
}
Codex CLI (~/.codex/config.toml)
[mcp_servers.threshold]
url = "https://threshold.<you>.workers.dev/mcp"
bearer_token_env_var = "THRESHOLD_ACCESS_KEY"
Gemini CLI (~/.gemini/settings.json)
{
"mcpServers": {
"threshold": {
"httpUrl": "https://threshold.<you>.workers.dev/mcp",
"headers": { "Authorization": "Bearer <ACCESS_KEY>" }
}
}
}
Any app that only runs local (stdio) servers, e.g. Claude Desktop's config file
mcp-remote bridges stdio to your server:
{
"mcpServers": {
"threshold": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://threshold.<you>.workers.dev/mcp", "--header", "Authorization:${AUTH_HEADER}"],
"env": { "AUTH_HEADER": "Bearer <ACCESS_KEY>" }
}
}
}
Anything else: Threshold speaks the standard Streamable HTTP transport with OAuth 2.1 (dynamic client registration, Client ID Metadata Documents, PKCE) or a bearer header.
How it works
Claude, ChatGPT, Cursor, ... Cloudflare Workers (your account) Garmin
┌──────────────────────────┐ OAuth or ┌─────────────────────────────────────────┐ API key ┌──────────────┐
│ "How am I today?" │──bearer key──▶│ /mcp 16 tools, 4 prompts │──────────▶│ Personal API │
└──────────────────────────┘ │ ├─ analysis: load, HRV, readiness... │ 24 h │ (read-only) │
│ ├─ FIT decoder: hidden VO2 max, PRs │ token └──────────────┘
│ └─ D1 archive ◀── cron every 10 min │◀──────────────┘
└─────────────────────────────────────────┘
- Archive first. Tools read from a D1 (SQLite) archive and fetch only missing or recent days from Garmin, split to Garmin's range limits. Recent days are re-checked because watches sync late.
- Background sync. Every 10 minutes the cron job refreshes recent days, walks back through your history a little at a time, and decodes FIT files, newest first. Each run stays inside the Workers request and CPU limits, and backs off when Garmin rate-limits the key.
- Fast FIT decoder. A small decoder written for Workers reads a one-hour activity in about a millisecond, 20× faster than Garmin's JavaScript SDK, which is what makes FIT analysis possible on the free tier.
- Hidden metrics. Garmin writes VO2 max and performance condition into the FIT file's undocumented message 140 but leaves them out of the API. Threshold decodes them (VO2 max = field 7 × 3.5 / 65536, confirmed by the community) and labels anything less certain as experimental. If Garmin strips that data from API downloads, the tool says so.
Does it fit Cloudflare's free tier?
Yes. One person's data is small.
| Free-tier limit | Threshold's use |
|---|---|
| 100,000 requests / day | ~150 cron runs plus your chats |
| 10 ms CPU / request | Most tool calls use a few ms; one FIT file per cron run (3–10 ms) |
| 50 external requests / invocation | 30 per tool call, 12 per cron run |
| D1: 5 GB, 5 M reads, 100 k writes / day | Years of history fit in a few MB |
| KV: 1,000 writes / day | A few per day (Garmin token, OAuth) |
| 5 cron triggers | 1 |
The first backfill of a long history takes a few hours to a few days on Free, and FIT files import one per run. On the Workers Paid plan ($5/month) set PLAN to paid and it does everything 5–20× faster.
Configuration
| Name | Kind | Default | Meaning |
|---|---|---|---|
GARMIN_API_KEY |
secret | – | Your Garmin Personal API key. |
ACCESS_KEY |
secret | – | Unlocks your server (16+ characters). |
PLAN |
var | free |
paid does more work per run. |
TIMEZONE |
var | Garmin profile, else UTC | IANA zone used for "today". |
GARMIN_KEY_EXPIRES |
var | – | YYYY-MM-DD; tools warn 14 days before. |
BACKFILL_FROM |
var | as far as Garmin answers | Oldest date to archive. |
SYNC_FIT_FILES |
var | true |
false skips FIT downloads in the background. |
PUBLIC_URL |
var | request origin | Pin the OAuth origin when you use a custom domain. |
Set any of these in the Cloudflare dashboard: Workers & Pages → threshold → Settings → Variables and secrets → Add (type Secret for the two keys, Text for the rest). They survive redeploys.
Key expiry. Garmin keys last 30 days to a year and Garmin emails you about a week before. Create a new key, run npx wrangler secret put GARMIN_API_KEY, and update GARMIN_KEY_EXPIRES. No redeploy needed.
Privacy and security
- Single user, your account. Garmin's terms make the Personal API key personal. Threshold is built for one person on their own Cloudflare account; there is no hosted service and your data never passes through this project.
- The Garmin key never leaves the Worker. It is exchanged for a 24-hour token, cached in your KV namespace.
- Read-only. The Personal API cannot change your Garmin account, and the SQL tool refuses anything but
SELECT. - Only you can connect apps. The OAuth consent page asks for your access key, shows where tokens will be sent, and warns about local apps. Tokens are bound to your server's
/mcpURL. - Less exposure to cloud models. GPS coordinates, place names and personal identifiers are removed from responses unless you ask for them.
See SECURITY.md to report a vulnerability.
Troubleshooting
- Ask your assistant to run
server_status: it checks the keys, the Garmin connection, key expiry, archive coverage and the last background run. https://<your-worker>/healthreturns{"ok": true}when the server is configured.npx wrangler tailstreams live logs.- Missing older data? The archive fills in the background;
server_statusshows how far back it has reached.
Limits worth knowing
- Garmin's Personal API does not expose Training Readiness, Training Status or race predictions. Threshold computes its own readiness and load with transparent rules instead.
- Message-140 fields other than VO2 max are community-decoded and labelled experimental.
- Not medical advice. Consumer wearables estimate; trends matter more than single readings.
Development
npm ci
cp .dev.vars.example .dev.vars # add your keys
npm run dev # local server with a local D1 and KV
npm run check # typecheck, tests, and a dry-run build
Tests run inside the Workers runtime against a fake Garmin API: the FIT decoder is checked record for record against Garmin's SDK, and the integration tests drive the full OAuth flow, every tool, and the cron backfill.
src/
index.ts Worker entry: HTTP and cron
auth/ OAuth provider, consent and landing pages
mcp/ MCP server, tools, prompts
garmin/ API client (token cache, retries, rate limits), endpoints, compaction
archive/ D1 schema, archive-first sync, backfill, read-only SQL
fit/ FIT decoder and activity analysis
analysis/ training load, recovery, readiness, weekly report, trends
Contributions are welcome. Please open an issue first for larger changes.
FAQ
What is a Garmin MCP server?
An MCP server exposes data and tools to AI assistants through the Model Context Protocol. A Garmin MCP server lets an assistant such as Claude or ChatGPT read your Garmin Connect data (activities, sleep, HRV, heart rate, stress, Body Battery, weight) and answer questions about it. Threshold also analyzes that data in code: readiness, training load, HRV baselines, best efforts and more.
How do I connect my Garmin to Claude?
Deploy Threshold to Cloudflare (one click), then in Claude open Settings → Connectors → Add custom connector, paste your server's /mcp URL and sign in with your access key. It then works on claude.ai, Claude Desktop and the Claude iOS and Android apps. See Connect your AI.
How do I connect my Garmin to ChatGPT?
Turn on Developer mode in ChatGPT (Settings → Apps & Connectors → Advanced), create a connector with your server's /mcp URL, choose OAuth, and enter your access key.
Does it need my Garmin password?
No. It uses a Garmin Personal API key that you create in Garmin Connect and can revoke at any time. Older Garmin MCP servers log in with your password through the unofficial garminconnect library; Threshold does not.
Is it free?
Yes. It is MIT-licensed and fits Cloudflare's Workers Free plan. The optional $5/month Workers Paid plan only makes the first history import faster.
Is my data private?
Yes. You deploy it to your own Cloudflare account; your data never passes through this project. Only someone with your access key can connect an app, and GPS coordinates and personal identifiers are removed from responses by default.
What Garmin data can it read?
Activities and their original FIT files, daily summaries (steps, calories, intensity minutes, floors), sleep and sleep stages, HRV, resting and all-day heart rate, stress, Body Battery, SpO2, respiration, weight and body composition, hydration, nutrition logs, menstrual cycle, fitness age, heart-rate zones and profile settings.
Can it see Training Readiness, Training Status or VO2 max?
Garmin's Personal API does not return Training Readiness or Training Status, so Threshold computes its own readiness and training load with transparent rules. VO2 max comes from the activity list and is also decoded from each activity's FIT file, where Garmin keeps more precise values.
Can it change anything on my Garmin account?
No. The Personal API is read-only, and so is Threshold.
Which Garmin watches work?
Any device that syncs to Garmin Connect: Forerunner, Fenix, Epix, Enduro, Venu, Vivoactive, Instinct, Edge bike computers and more. Some metrics (HRV, Body Battery) need a watch that records them.
Credits
Endpoint paths follow Garmin's own Personal API client. FIT field names and scales come from Garmin's FIT SDK. Hidden-field decoding builds on work shared by the Garmin developer forums, intervals.icu and Runalyze communities.
Threshold is not affiliated with or endorsed by Garmin. Garmin and Garmin Connect are trademarks of Garmin Ltd. or its subsidiaries.
License
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found