Threshold MCP server
2026년 10월 24일 커밋Garmin Connect 데이터를 AI 어시스턴트에 연결하여 훈련 부하, HRV, 회복 및 체력 지표를 모델의 추측이 아닌 계산된 코드를 통해 분석합니다.
다른 이름 KareemmSayed/Threshold
- 스타
- 8
- Tools
- —
- 다운로드, 30일
- —
- 최신 버전
- —
- 마지막 커밋
- 2026년 10월 24일
- 라이선스
- MIT
Threshold MCP란?
Threshold은(는) MCP server입니다. Garmin Connect 데이터를 AI 어시스턴트에 연결하여 훈련 부하, HRV, 회복 및 체력 지표를 모델의 추측이 아닌 계산된 코드를 통해 분석합니다.
프로젝트 README에서 생성된 요약
Threshold MCP server 정보
Garmin 기기를 사용하는 운동선수는 훈련 데이터를 Claude, ChatGPT 또는 Cursor와 같은 AI 어시스턴트에 연결하여 정밀한 생리학적 분석을 받을 수 있습니다. 모델이 원시 데이터를 해석하도록 전달하는 다른 서버와 달리, Threshold는 모든 수학적 계산을 코드에서 수행합니다. 이를 통해 훈련 부하, HRV 기준선, 회복 상태와 같은 지표가 언어 모델의 추측이 아닌 정확한 값으로 산출되도록 보장합니다.
이 서버는 공식 Garmin Personal API를 사용하여 사용자의 Cloudflare Workers 계정에 직접 호스팅됩니다. 보안을 위해 Garmin API 키와 커스텀 액세스 키가 필요합니다. 클라우드에서 실행되므로 모바일 AI 앱을 통해 언제든 접근할 수 있으며, 로컬 컴퓨터를 계속 켜둘 필요가 없습니다. FIT 파일 분석, 읽기 전용 SQL 아카이브를 통한 기록 쿼리, 수면 단계 및 Body Battery와 같은 건강 데이터의 요약 정보 확인을 위한 특화된 도구를 제공합니다.
활용 사례
- 1일일 준비도 평가현재 지표를 바탕으로 고강도 운동을 수행할 만큼 충분히 회복되었는지 AI 어시스턴트에게 물어보세요.
프로젝트의 README를 바탕으로 작성되었습니다. 모든 활용 사례는 해당 내용의 인용문과 대조하여 확인합니다.
구성
AUTH_HEADER
| 이름 | 설명 | 유형 |
|---|---|---|
| AUTH_HEADER | — |
자동 검사
2026년 11월 14일에 실행됨전체 보고서 →아직 인증되지 않았습니다. 서버는 80점 이상의 점수, 심각한 결함 없음, 그리고 검사에 충분한 데이터를 갖추어야 합니다. 검사 방식 · 점수를 높이기 위해 수정할 항목
- 소스 저장소 연결 및 접근 가능kareemmsayed/threshold is public and reachablegithub.com/kareemmsayed/threshold ↗통과
- 패키지가 이 저장소를 소스로 명시검사되지 않음
- 빌드 출처(Sigstore 증명)검사되지 않음
- 오픈 소스 라이선스 (OSI)MITspdx.org/licenses/MIT.html ↗통과
- 최근 1년 내 커밋 또는 릴리스Last commit or release 2026-10-24github.com/kareemmsayed/threshold ↗통과
- OpenSSF Scorecard검사되지 않음
- 패키지 이름이 인기 패키지와 유사하지 않음검사되지 않음
- 패키지 및 해당 종속성에 중요한 보안 권고 없음검사되지 않음
- 모델에 대한 숨겨진 지침 없음0 tool descriptions and the README read; no override phrases, credential paths or hidden characters통과
- 파괴적인 도구에 라벨 표시됨검사되지 않음
- 원격 엔드포인트가 MCP 핸드셰이크에 응답검사되지 않음
README
GitHub에서 보기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 of Garmin's overnight HRV average, 7-day rolling mean, normal band = baseline ± 0.5 SD), resting-HR drift, sleep versus usual. Shows how many of the last 7 nights have HRV. |
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. |
show_view |
Shows the answer as a card in this project's design, inside the chat in apps that support MCP Apps (Claude, ChatGPT, VS Code, Goose) and as a private link everywhere else. Presets build the card from the analysis tools, so the server supplies every number; custom blocks cover any other question. |
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.
Cloudflare 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.
Settings → 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 mcp add --transport http threshold https://threshold.<you>.workers.dev/mcp \
--header "Authorization: Bearer <ACCESS_KEY>"
{
"mcpServers": {
"threshold": {
"url": "https://threshold.<you>.workers.dev/mcp",
"headers": { "Authorization": "Bearer <ACCESS_KEY>" }
}
}
}
Windsurf names the field serverUrl instead of url.
{
"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 }]
}
[mcp_servers.threshold]
url = "https://threshold.<you>.workers.dev/mcp"
bearer_token_env_var = "THRESHOLD_ACCESS_KEY"
{
"mcpServers": {
"threshold": {
"httpUrl": "https://threshold.<you>.workers.dev/mcp",
"headers": { "Authorization": "Bearer <ACCESS_KEY>" }
}
}
}
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 17 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.
- Cards, not walls of text.
show_viewships its own HTML (an MCP Appsui://resource), so every assistant shows the same design without installing anything. The model only picks a preset and the sections the user asked about, or fills fixed block types; it never writes HTML. Apps without MCP Apps support get a private link to the same card. - 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. |
HRV_BASELINE_FROM |
var | – | YYYY-MM-DD; HRV baselines ignore earlier nights. Set it when you change watch. |
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. |
VIEW_LINK_HOURS |
var | 24 |
How long show_view private links work (max 720). 0 turns links off. |
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.
- Private links expire. A
show_viewlink is a random 192-bit token (only its hash is stored), opens one card, expires afterVIEW_LINK_HOURS, and stops working when you change the access key. Anyone you send it to can see that card, so share it like any private link. Cards built from presets never include GPS data or activity names.
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.
- HRV comes from Garmin's nightly HRV summary in the Personal API. Some accounts and devices return none. HRV signals then show no data and the briefing skips them.
- Threshold uses Garmin's overnight HRV average. It does not compute rMSSD from beat-to-beat intervals and cannot check Garmin's method.
- HRV from different devices is not comparable, and Threshold cannot see a device change. Set
HRV_BASELINE_FROMto the first night on the new watch; verdicts return after about three weeks (14 baseline nights plus the current week). - With fewer than 5 of the last 7 nights, an HRV verdict is capped at "slightly low" or "slightly high". Every HRV result shows its night count.
- The readiness and recovery rules are transparent heuristics. They are not validated against performance or recovery outcomes. The tests prove the code applies the rules, and the FIT decoder test proves parsing matches Garmin's SDK. Neither shows that a call predicts anything.
- 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
view/ show_view: card spec, presets, private links; client/ holds the card's CSS and script
After editing src/view/client/, run npm run gen:view to rebuild src/view/assets.ts; CI checks that it is up to date.
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
Threshold MCP 질문
Threshold는 다른 서버와 비교했을 때 훈련 지표를 어떻게 처리하나요?
모델이 해석하도록 원시 JSON을 전달하는 일반적인 서버와 달리, Threshold는 준비도, 훈련 부하, HRV 기준선과 같은 지표를 코드 내에서 직접 계산합니다.
서버는 어디에 호스팅되나요?
사용자의 무료 Cloudflare Workers 계정에서 서버가 실행됩니다.
Garmin 전체 기록에 접근할 수 있나요?
네, D1 아카이브를 통해 전체 기록을 쿼리할 수 있는 읽기 전용 SQL 도구를 제공합니다.