Garmin Cloud MCP

Garmin Cloud MCP

A stateless MCP server for Cloudflare Workers that exposes read-only Garmin Connect data (activities, health, sleep, training status, HRV, VO2 max) for a single securely linked personal account.

Category
Visit Server

README

Garmin Cloud MCP

A personal, stateless Model Context Protocol server for Cloudflare Workers. The protected MCP endpoint exposes nine bounded, read-only tools for one securely linked personal Garmin Connect account.

The MCP endpoint uses Cloudflare's createMcpHandler() with a fresh MCP server for every request. There are no MCP protocol sessions, SSE endpoints, or retained MCP transport state. Inbound OAuth grants/codes/tokens and encrypted Garmin linking state are retained in separate Cloudflare KV namespaces. A narrowly scoped, single-owner Durable Object coordinates one-time MFA consumption; it is application security state, not MCP transport state.

Tools

All returned values are normalized into stable JSON shapes and also rendered as MCP text content. Unavailable scalar metrics are null; fields Garmin adds later are omitted. GPS polylines, charts, raw health/sleep samples, and other bulky arrays are never passed through.

Tool Purpose Input and bounds
get_profile Concise owner profile No input
list_activities Newest-first activity summaries start defaults to 0 (maximum 10000); limit defaults to 20 (maximum 50)
get_activity One activity summary with at most 20 basic splits Positive Garmin activity ID as a decimal string
get_daily_health Steps, heart rate, stress, body battery, intensity, and respiration One real YYYY-MM-DD local Garmin calendar date
get_sleep Sleep total, stages, scores, SpO2, stress, and respiration One real YYYY-MM-DD local Garmin calendar date
get_training_readiness Recovery score and bounded sleep, load, HRV, and stress factors One real YYYY-MM-DD local Garmin calendar date; at most 8 snapshots
get_training_status Training status, acute/chronic load, workload ratio, and load focus One real YYYY-MM-DD local Garmin calendar date
get_hrv Overnight HRV summary, status, and personal baseline One real YYYY-MM-DD local Garmin calendar date; no raw readings
get_vo2_max Generic and cycling VO2 max and fitness-age estimates One real YYYY-MM-DD local Garmin calendar date

The training/recovery category deliberately has four non-overlapping, single-date tools: readiness explains immediate recovery factors, status describes training load, HRV describes overnight variability against a personal baseline, and VO2 max reports aerobic-capacity estimates. The single-date contracts keep calls bounded and match the current endpoint division verified against python-garminconnect commit 414b540.

Examples:

{ "name": "list_activities", "arguments": { "start": 0, "limit": 10 } }
{ "name": "get_activity", "arguments": { "activityId": "1234567890" } }
{ "name": "get_daily_health", "arguments": { "date": "2026-08-12" } }
{ "name": "get_sleep", "arguments": { "date": "2026-08-12" } }
{ "name": "get_training_readiness", "arguments": { "date": "2026-08-12" } }
{ "name": "get_training_status", "arguments": { "date": "2026-08-12" } }
{ "name": "get_hrv", "arguments": { "date": "2026-08-12" } }
{ "name": "get_vo2_max", "arguments": { "date": "2026-08-12" } }

Each normalized result is capped at 64 KiB. Garmin's upstream JSON is capped at 2 MiB before normalization; responses above either limit fail safely. Activity list results are capped to the requested page, activity splits to 20, and training-readiness snapshots to 8. This service sends the requested dates to Garmin Connect and stores encrypted Garmin authorization tokens in Cloudflare KV. It does not retain Garmin response data or MCP transport/session state. Training readiness, training status, HRV, and VO2 max are sensitive health and fitness data returned to the authorized MCP client. Raw HRV readings, device identifiers/details, user/profile identifiers, long-form feedback prose, and other unlisted upstream fields are omitted; concise Garmin feedback keys are retained where they explain a normalized metric.

Authorization

/mcp is protected by the current @cloudflare/workers-oauth-provider OAuth 2.1 implementation. The Worker provides:

  • authorization code flow with S256 PKCE; implicit flow and plain PKCE are disabled;
  • rotating refresh tokens when offline_access is granted;
  • exact resource/audience binding to the MCP URL;
  • RFC 9728 protected-resource metadata and RFC 8414 authorization-server discovery;
  • garmin:read and offline_access scopes, with unknown or excess scopes rejected;
  • Client ID Metadata Documents (CIMD) as the preferred registration mechanism; and
  • constrained dynamic client registration (DCR) at /register only as a fallback for older MCP clients.

CIMD clients are public clients: their metadata must select token_endpoint_auth_method: none, and they must use S256 PKCE. The Worker sets clientIdMetadataDocumentEnabled: true; the matching global_fetch_strictly_public compatibility flag is required to keep metadata fetches SSRF-safe.

The /authorize endpoint uses a reusable personal-owner sign-in and explicit consent screen. The owner password is a Worker secret, never a client secret. Login and consent use CSRF protection, signed Secure/HttpOnly/SameSite cookies, bounded request bodies, and generic authentication errors.

Link Garmin Connect

Open /garmin on the HTTPS Worker host and sign in with the same OWNER_PASSWORD used by the authorization screen. The page shows whether the personal account is linked and supports:

  • entering the Garmin email and password to start or retry linking;
  • entering a Garmin multi-factor code in a second, one-time browser step;
  • verifying the resulting Garmin token with the Garmin profile endpoint before saving it; and
  • unlinking all locally stored Garmin tokens, MFA continuation, and rate-limit state.

Garmin credentials and MFA codes are submitted only over HTTPS and are never logged or written to KV. The password exists only for the duration of the first Worker request. Garmin's widget flow allows MFA to continue without the password: the Worker retains only Garmin cookies, an MFA CSRF token, the sign-in context, a random state, and timestamps. That continuation is AES-GCM encrypted in GARMIN_KV, expires after ten minutes, and is deleted before its single use.

KV does not offer atomic get-and-delete. To prevent two concurrent MFA submissions from both reaching Garmin, GARMIN_MFA_COORDINATOR transactionally grants one claim for the SHA-256 hash of the random continuation state. It never stores Garmin cookies, credentials, MFA codes, tokens, or MCP session state. The encrypted continuation itself remains in GARMIN_KV.

Garmin DI access and refresh tokens are stored as one authenticated AES-GCM record in GARMIN_KV. Non-secret owner and created/updated/expiry metadata is present on the versioned envelope; token values remain encrypted. Refresh uses Garmin's DI OAuth token endpoint, and the complete replacement pair is committed with one KV write only after Garmin accepts and verifies it. Records created by the previous OAuth1-based flow remain readable and refreshable during migration.

Unlinking deletes local state. This project does not claim to revoke tokens at Garmin because no supported Garmin token-revocation endpoint was verified.

Requirements

  • Node.js 24 or newer
  • Corepack with pnpm 11.3.0

Local setup

corepack enable
pnpm install
pnpm typegen
GARMIN_KEY="$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
printf 'OWNER_PASSWORD=replace-with-a-long-random-password\nGARMIN_TOKEN_ENCRYPTION_KEY=v1:%s\n' "$GARMIN_KEY" > .dev.vars
pnpm dev

.dev.vars is ignored by Git. Use a long, unique password; never put it in wrangler.jsonc or commit it. Local Wrangler supplies local KV namespaces for the configured OAUTH_KV and GARMIN_KV bindings and local Durable Object storage for GARMIN_MFA_COORDINATOR. Vite starts the Worker locally and prints its URL. Credential linking is deliberately rejected over plain HTTP; use an HTTPS tunnel or deployed preview when exercising /garmin with a real account. Check the public health endpoint with:

curl http://localhost:5173/health

An unauthenticated request to /mcp returns a Bearer challenge pointing to:

/.well-known/oauth-protected-resource/mcp

The authorization-server metadata is at /.well-known/oauth-authorization-server.

Cloudflare configuration and deployment

The committed OAUTH_KV and GARMIN_KV IDs identify this Worker's production namespaces. For a new Cloudflare account, create replacement namespaces, update those IDs, put the independent secrets, generate fresh types, and deploy with Wrangler:

pnpm wrangler kv namespace create OAUTH_KV --binding OAUTH_KV
pnpm wrangler kv namespace create GARMIN_KV --binding GARMIN_KV
pnpm wrangler secret put OWNER_PASSWORD
pnpm wrangler secret put GARMIN_TOKEN_ENCRYPTION_KEY
pnpm typegen
pnpm wrangler deploy

Set GARMIN_TOKEN_ENCRYPTION_KEY to v1: followed by a base64url-encoded 32-byte random key, as in the local setup example. It must be independent of OWNER_PASSWORD. Alternatively, add --update-config to each KV command and review its wrangler.jsonc edit. Never commit a real password, encryption key, or .dev.vars. Namespace IDs are non-secret configuration and are intentionally committed for this private application.

Rotating the encryption key makes existing Garmin records unreadable. For recovery, restore the old key or use the authenticated /garmin recovery button to discard unreadable state, then link again. The current envelope/key version is v1; unsupported versions fail closed.

Unofficial Garmin integration and recovery

Garmin Connect does not provide a supported public API for this personal workflow. The endpoint sequence is adapted from the MIT-licensed garth, garmin-connect, and python-garminconnect projects (see NOTICE.md), uses an explicit cookie jar and Worker-native Web Crypto/fetch, and may break when Garmin changes SSO HTML or private endpoints. There is no runtime dependency on those projects.

Garmin and Cloudflare aggressively limit automated sign-in (commonly HTTP 429 with Cloudflare 1015 pages). The Worker never automatically retries a credential or MFA submission. It records generic failure counts and increasing backoff in GARMIN_KV; an upstream limit enforces at least a one-hour cooldown. If limited, stop retrying, wait for the displayed/reported cooldown, confirm Garmin's service status, and sign in through Garmin's official site if account recovery is required.

If linking reports an account lock or required phone-number update, complete that recovery on Garmin's official site and retry later. If saved tokens stop refreshing, unlink and link again.

Connect from ChatGPT

ChatGPT needs the publicly reachable HTTPS Worker deployment; it cannot connect directly to the local Vite URL. Garmin account linking and ChatGPT app creation remain interactive owner steps.

  1. In ChatGPT, enable developer mode for the workspace/account under Settings → Apps → Advanced settings (the exact controls depend on plan and workspace role).
  2. Create a custom app/MCP connection and enter https://YOUR_WORKER_HOST/mcp as the server URL.
  3. Choose OAuth and select Client ID Metadata Document (CIMD) for client registration when that choice is shown. CIMD is the preferred path for this server.
  4. Request garmin:read offline_access. offline_access is required for ChatGPT to retain access by refreshing expired access tokens.
  5. Complete the owner-password sign-in, review the explicit consent page, authorize, and let ChatGPT scan the nine read-only tools.

The /register DCR endpoint remains advertised for older MCP clients that cannot use CIMD. It is a compatibility fallback, not the registration option to select in ChatGPT. DCR accepts only supported HTTPS/loopback redirects, authorization-code plus refresh-token grants, the code response type, and supported token authentication methods.

See OpenAI's developer mode and custom MCP app guidance and the MCP authorization specification for the client-side discovery flow.

Checks

pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm build
pnpm wrangler:dry-run

pnpm test runs the endpoint tests inside the Cloudflare Workers runtime. The MCP tests issue independent authenticated initialize, list, and call requests and assert that no Mcp-Session-Id is returned. The suite also covers discovery, CIMD advertisement, DCR constraints, PKCE/scope/ resource rejection, login and consent CSRF, bearer challenges, refresh rotation, secret leakage, public health, streamed body bounds, encrypted storage, concurrent one-time MFA, Garmin failure mapping, endpoint contracts, response normalization and bounds, and atomic Garmin token refresh persistence. Training/recovery coverage also fixes the upstream URL contracts, rejects malformed roots and invalid dates, verifies strict normalized allowlists and explicit nulls, caps readiness snapshots, and excludes raw HRV samples and device/profile identifiers.

After deployment, open https://YOUR_WORKER_HOST/garmin and link the Garmin account before using the tools. If Garmin authorization expires, use the same page to unlink/relink. For code updates, run all checks, push main, wait for GitHub Actions, then run pnpm wrangler deploy. For encryption key rotation, retain the previous key until the existing record is deliberately discarded and the account is linked again; changing the key alone makes the current record unreadable.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured