Skip to main content
Glama
sthoelscher

intervals-icu-chatgpt-mcp

by sthoelscher

intervals-icu-chatgpt-mcp

Let ChatGPT read your training data, without screenshots.

A small, read-only MCP server that connects ChatGPT to your intervals.icu account, and through it to everything your Garmin watch records: activities with laps and streams, heart rate and pace zones, fitness and fatigue, sleep, HRV, resting heart rate and your training calendar. It brings its own OAuth login, so you can host it on the public internet and use it from ChatGPT on the web, on macOS and on your phone.

Garmin watch ─► Garmin Connect ─► intervals.icu ─► this server (fly.io) ─► ChatGPT
                 (official sync)    (API key)        (OAuth, read-only)

Ask things like "How was my long run on Saturday?", "Compare this week's easy runs" or "Am I recovered enough for intervals tomorrow?" and ChatGPT fetches the data itself.

Not affiliated with Garmin, intervals.icu or OpenAI. A hobby project, maintained when time allows.

Why this one

There are several good MCP servers for intervals.icu already. This one is built for a narrower case:

  • Read-only by construction. No tool writes, and the API client can only send GET. Your calendar, activities and settings cannot be changed through ChatGPT.

  • Safe to expose. It ships a complete OAuth 2.1 flow (dynamic client registration, PKCE, short-lived tokens) with a password login page. Without it, anyone who learns the URL could read your health data.

  • Stateless. No database. It runs on the smallest fly.io machine that stops when idle, which typically costs nothing or close to it.

  • Answers sized for a chat model. Empty fields are dropped, streams are downsampled, and aerobic decoupling is computed from the raw data, so it also works for runs without a power meter.

Garmin is only reached through intervals.icu's official Garmin sync. There is no scraping and no Garmin login involved.

Related MCP server: intervals-icu-mcp

What you need

  • An intervals.icu account (free) connected to Garmin Connect

  • A ChatGPT plan that allows custom connectors (developer mode)

  • A fly.io account and flyctl, or any other host that runs a Docker image behind HTTPS

  • Node.js 22 or newer, only for local development

Setup

1. intervals.icu

  1. Create an account and connect Garmin Connect in the settings. Enable activities and wellness. Older data may need a one-off import.

  2. Near the bottom of the settings page (Developer Settings), note your athlete id (starts with i) and create an API key. Treat the key like a password: it allows full access to your intervals.icu account, even though this server only reads.

2. Deploy to fly.io

git clone https://github.com/sthoelscher/intervals-icu-chatgpt-mcp.git
cd intervals-icu-chatgpt-mcp

cp fly.example.toml fly.toml           # then replace YOUR-APP-NAME (twice)
fly apps create YOUR-APP-NAME

fly secrets set --stage -a YOUR-APP-NAME \
  INTERVALS_API_KEY='your-api-key' \
  INTERVALS_ATHLETE_ID='i123456' \
  JWT_SECRET="$(openssl rand -hex 32)" \
  LOGIN_PASSWORD="$(openssl rand -base64 18)"    # or your own long password

fly deploy --ha=false

curl https://YOUR-APP-NAME.fly.dev/health should answer ok. If you let fly generate the password, read it once with fly ssh console -C 'printenv LOGIN_PASSWORD' and store it in your password manager.

--ha=false keeps it to one machine. Two machines would double the login rate limit, which is counted per machine.

Set the secrets from your own terminal and keep them out of chats, issues and shell history you share.

3. Connect ChatGPT

On chatgpt.com, open Settings → Apps, enable developer mode under Advanced, and create a new app or connector (the labels change from time to time):

  • URL: https://YOUR-APP-NAME.fly.dev/mcp

  • Authentication: OAuth

ChatGPT opens a login page. Enter your LOGIN_PASSWORD and allow access. A connector created on chatgpt.com is available in the ChatGPT apps as well. The macOS app can also add it directly under Settings → Plugins → MCPs → Add → Streamable HTTP.

Start with: "Get my training overview."

After a deploy that renames or removes tools, start a new chat. Existing chats keep the old tool list.

Tools

27 tools, all read-only.

Area

Tools

Overview

get_overview: today's date in your timezone, last 7 days of activities and wellness, next 7 days of the calendar

Athlete

get_athlete (thresholds and zones per sport), get_period_summary

Activities

list_activities, search_activities, get_activity, get_activity_intervals, get_activity_streams, get_activity_histogram, get_activity_curve, get_activity_best_efforts, get_activity_decoupling, get_activity_weather, get_activity_segments, get_activity_notes, search_intervals

Wellness

get_wellness: fitness (CTL), fatigue (ATL), resting HR, HRV, sleep, readiness, weight, steps and more

Performance

get_best_curves (power, pace, HR), get_power_model, get_power_hr_curve

Calendar

list_events, get_event, get_training_plan, list_workouts, get_workout

Other

list_gear, get_weather_forecast

Answers drop null and empty fields and are capped at 120,000 characters.

Security model

  • One user. The server serves exactly one intervals.icu account, configured through secrets. The login page asks for LOGIN_PASSWORD. Failed attempts are limited to 10 per 15 minutes per IP and 30 per hour in total. Use a generated password (openssl rand -base64 18), not a memorable one.

  • Known clients only. Dynamic client registration accepts only redirect targets on ChatGPT's origins (https://chatgpt.com, https://chat.openai.com) and loopback addresses. This stops anyone from registering their own client and sending you a link to your real login page (consent phishing). Change the list with ALLOWED_REDIRECT_ORIGINS. The login page shows where access will go.

  • OAuth 2.1. Public clients with PKCE (S256). Authorization codes are valid for 5 minutes and single use. Access tokens last 1 hour and are bound to this server's /mcp URL. Refresh tokens last 30 days, and no chain of refreshes outlives 180 days after you entered the password.

  • Stateless tokens. Client ids, codes and tokens are JWTs signed with JWT_SECRET (HS256). Nothing is stored, so an auto-stopped machine keeps working. Because anyone can obtain a signed client id, the server refuses to start unless JWT_SECRET is random and at least 32 bytes (openssl rand -hex 32). Rotating JWT_SECRET revokes every token and client at once. That is the emergency switch, and the only way to revoke.

  • Read-only. The intervals.icu client has no code path for anything but GET.

  • Headers. The login page cannot be framed, is not cached and sends no referrer. HSTS is on for HTTPS.

  • Logs contain paths, status codes, durations, tool names, registration origins and login outcomes. Never query strings, tokens, passwords or training data.

  • Proxy. TRUST_PROXY=1 (default) is right on fly.io. Without a proxy in front, set it to 0, otherwise clients can fake their IP and get around the per-IP login limit.

If you find a security problem, please open a private security advisory on GitHub instead of an issue.

Local development

npm install
cp .env.example .env    # INTERVALS_API_KEY and INTERVALS_ATHLETE_ID
set -a; . ./.env; set +a

npm run smoke           # calls every tool once against your account (stdio)
npm run inspect         # MCP Inspector
npm run dev:stdio       # stdio server, e.g. for the ChatGPT macOS app's STDIO option

To run the HTTP server with OAuth locally:

PUBLIC_URL=http://localhost:8787 LOGIN_PASSWORD=local-test-password-123 JWT_SECRET=$(openssl rand -hex 32) \
  npm run dev:http
# in a second shell
BASE=http://localhost:8787 LOGIN_PASSWORD=local-test-password-123 npm run oauth-e2e

oauth-e2e walks the whole flow the way ChatGPT does: discovery, registration (including refused foreign redirect targets), login, wrong password, foreign resource, PKCE, single-use codes, tampered tokens and refresh.

Variable

Needed for

Meaning

INTERVALS_API_KEY

both

intervals.icu API key

INTERVALS_ATHLETE_ID

both

intervals.icu athlete id, e.g. i123456

PUBLIC_URL

HTTP

public base URL, e.g. https://YOUR-APP-NAME.fly.dev

LOGIN_PASSWORD

HTTP

password on the login page, at least 12 characters

JWT_SECRET

HTTP

signing key, random, at least 32 bytes (openssl rand -hex 32)

ALLOWED_REDIRECT_ORIGINS

HTTP

optional, comma separated, default ChatGPT's origins; loopback is always allowed

TRUST_PROXY

HTTP

optional, proxies in front, default 1 (fly.io)

PORT

HTTP

default 8787

Limitations

  • Only reads. Planning workouts into your calendar is deliberately not supported.

  • One athlete per deployment.

  • Tested with ChatGPT (web and macOS). Other MCP clients with OAuth support should work but are untested.

  • Pace based decoupling is distorted by hills, wind and stops. Compare it between similar routes.

License

MIT

Related MCP Connectors

Related MCP Servers