Skip to content
Conduit

mcp server

intervals-mcp

Remote MCP server for the Intervals.icu training API — a claude.ai custom connector with coaching-specific tools (load rules, readiness flags, week-row notes, Plan Builder integration, signal analysis

christhonie/intervals-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 23, 2026

Intervals.icu MCP

License: MIT Node TypeScript MCP

A remote Model Context Protocol server for the Intervals.icu training API, built as a claude.ai custom connector. It exposes coaching-focused tools (fitness/PMC metrics, training history, calendar workouts, wellness, week-row notes, Plan Builder targets, raw activity streams, and a signal-analysis toolkit) with the coaching business logic embedded server-side, so the assistant gets a complete result from a single tool call.

It's built as an alternative to existing Intervals.icu MCP servers, adding capabilities they typically don't expose: week-row calendar notes, writes to completed activities, and Plan Builder TARGET/PLAN/apply-plan integration.

Status — personal deployment. This is a single-athlete deployment built around one coaching workflow (the athlete ID and OAuth client are configured per-instance), not a turn-key multi-tenant service. The source is shared as a reference for others building Intervals.icu MCP servers — self-hosting is possible but assumes you're comfortable with the OAuth 2.1 + Kubernetes setup described below. There is no public hosted endpoint.

Credits

This project was built from scratch in TypeScript, but it stands on the shoulders of the Intervals.icu MCP community. Credit and thanks to:

  • hhopke/intervals-icu-mcp (MIT) — the most mature community server (Python/FastMCP). Used as the reference for endpoint coverage, tool naming, and parameter shapes.
  • VSidhArt/intervals-mcp — for the OpenAPI-spec exploration approach.

We built fresh rather than forking because claude.ai remote connectors require the server to be an OAuth 2.1 + PKCE authorization server (no bearer-token option in the connector UI), which the community servers do not implement — that layer is reused from our own fatsecret-mcp / hevy-mcp deployments. See the decision log for the full rationale.

Architecture

                  OAuth 2.1 + PKCE              HTTP Basic (API_KEY:key)
claude.ai ─────► /authorize, /token ──────► /mcp ─────────► intervals.icu API
                 (this server is the                │
                  authorization server)             │
                                          Deployment (1 replica, mcp namespace)
                                          envFrom: intervals-mcp-secrets
  • Transport: Streamable HTTP (src/http-server.ts). A stdio entry (src/index.ts) is kept for local debugging.
  • Auth to claude.ai: OAuth 2.1 + PKCE via the MCP SDK auth router and a minimal single-client provider (src/oauth-provider.ts).
  • Auth to Intervals.icu: HTTP Basic — username literal API_KEY, password is the personal API key.
  • API client: a small hand-written typed client (src/intervals-client.ts). The OpenAPI spec is committed as openapi-spec.json (the field-name source of truth); see the decision log for why we don't generate the client.

Tools

ToolPhaseNotes
get_fitness_metrics1CTL/ATL/TSB/ramp/eFTP + overreachRisk/fatigueSpike flags
get_training_history1rides vs gymSessions, Strava stubs filtered
get_activity_detail1non-null fields only
get_events1category filter; completed flag
get_wellness1+ hrvTrendDown flag
push_workout1applies Rule 1 (WeightTraining load)
update_event1applies Rule 1; infers type from existing event
push_note1week-row notes via for_week + exclusive end_date_local
update_note1in-place PUT update of an existing NOTE event
get_power_curves1best power efforts over standard durations; ⚠ endpoint params need live validation
get_training_summary1volume/load aggregates
update_activity2write load/description to a completed activity
create_activity2manual activity, source=MANUAL
get_weekly_targets3Plan Builder TARGET + compliance calc
push_weekly_target3write/update a weekly TARGET
push_sport_targets3per-sport weekly TARGETs (delete-then-recreate); empty array clears the week
list_plan_folders3discover folder_id for apply_plan
apply_plan3apply a plan folder to the calendar (Monday start)
update_sport_settings3FTP/LTHR/max-HR/zones — ⚠ preview-by-default (needs confirm:true)
update_activity_type3CTL/ATL contribution multipliers — ⚠ preview-by-default
push_plan_block9PLAN phase bar spanning the Plan page (bare-hex colour)
get_activity_streams10raw 1 Hz time-series (watts/hr/smo2/…), positionally aligned
detect_threshold_crossings11times a stream crosses a threshold value/direction
detect_peaks_nadirs11prominence-filtered local maxima/minima
compute_epoch_stats11per-epoch summary stats; exclude_windows
compute_correlation_window11Pearson r over a window, with optional lag scan
detect_plateaus11sustained stable/elevated periods
smooth_stream11trailing rolling-mean derived stream (returns a handle)
compute_derivative111st/2nd derivative derived stream
extract_segment11materialise raw/smoothed values over a window
align_events_to_stream11event-locked windows + mean response shape

Phases 4–8 had no net new tools (covered by RELEASE.md and the decision log). Phase 11 tools accept either a raw stream name or a derived handle (e.g. smo2~mean:10), keeping the high-fidelity series server-side between calls.

Business rules (server-side)

  • Rule 1 — WeightTraining load: when rpe and duration_minutes are supplied without an explicit icu_training_load, load = round(rpe × minutes × 0.15). Never auto-computed for power/HR sports (it would corrupt TSS).
  • Tags pass through unchanged.
  • Preview-by-default on the riskiest writes (update_sport_settings, update_activity_type): without confirm:true they return a diff and write nothing.
  • Event colours: PLAN events require bare hex (the leading # is stripped); NOTE events tolerate #.

Development

npm install
cp .env.sample .env   # fill in INTERVALS_API_KEY, INTERVALS_ATHLETE_ID, OAUTH_*
npm run type-check
npm run build
npm run start:stdio   # local stdio server
npm run inspect       # MCP Inspector

Deployment

Container + Kubernetes (mcp namespace, ArgoCD/cert-manager/nginx). See k8s/, deploy/argocd/, and the deployment runbook in the docs. Target endpoint: https://icu-mcp.christhonie.co.za/mcp.

Documentation

Requirements, design, and the decision log live under docs/ as an Antora ROOT module.

OAuth state persistence (optional, Redis)

By default the OAuth provider keeps codes/tokens in memory, so a pod rollout forces claude.ai to re-authenticate. Set REDIS_URL to persist them in Redis instead — tokens then survive rollouts (no re-auth after a deploy). Unset = in-memory. If Redis is unreachable the store degrades gracefully (behaves as in-memory) rather than failing auth. See src/oauth-store.ts.

Reusing this in other MCP servers (template)

src/oauth-store.ts is generic and the MinimalOAuthProvider changes carry no Intervals-specific logic, so the pattern ports directly to the sibling MCP servers (fatsecret-mcp, hevy-mcp):

  1. Copy src/oauth-store.ts verbatim.
  2. npm install ioredis.
  3. In the provider, replace the in-memory Maps with the injected OAuthStore (codes/access/refresh) and make the methods await the store (see this repo's src/oauth-provider.ts).
  4. In the HTTP entry point: const store = createOAuthStore("<app>:oauth") and pass store into the provider. Use an app-unique prefix so multiple servers can share one Redis without key collisions.
  5. Add REDIS_URL to the app's K8s Secret (a distinct Redis DB number per app is recommended). envFrom: secretRef injects it automatically.

The MCP transport/session remains in-memory by design — this template persists only the OAuth layer (the part that otherwise forces re-authentication).

License

MIT — see LICENSE.

↑↓ navigate openesc close