Skip to main content
Glama

strava-mcp

ci docs release npm image license

Ask an AI assistant anything about your Strava history, answered in milliseconds from a complete local mirror of your account.

strava-mcp is a self-hosted MCP server for one athlete's own Strava data. A background sync copies everything Strava's API returns for the account into a single DuckDB file, within Strava's rate limits; the tools answer from that file, and a read-only snapshot of it feeds Grafana, notebooks and backups. One container, one docker compose up -d.

  • A complete mirror. Activities with laps, splits, best efforts and every segment effort; full-resolution streams; zones; kudos, comments, photos; gear, segments, routes, clubs, the profile and a history of its totals. Stored verbatim, versioned when it changes, projected into typed tables with a description on every column.

  • Rate-limit aware sync. Usage comes from Strava's headers; background work takes at most 75% of each window and spreads it over the day, so your questions always have budget. A webhook delivers new activities in seconds.

  • Cache-first tools, plus SQL. 33 tools include local-only SQL/schema/status, cache-first reads and explicit Strava writes (upload status always reads Strava). strava_query uses engine-enforced read-only transactions over stored history; check coverage before treating it as complete. Mirrored answers preserve provenance.

  • Discoverable context without hidden sync. strava://reference/usage lists URI templates and the five local-only analysis prompts, with bounded examples and costs, even in clients that hide template discovery. Local schema, coverage/status and budget resources cost zero API calls; missing rate headers mean unknown accounting, not free requests.

  • A snapshot anything can read. A consistent read-only copy, published next to the mirror and optionally over HTTP. Grafana, a marimo notebook and scheduled backups ship as Compose profiles.

  • Bearer token or your identity provider. A bearer protects the endpoint by default; the oauth profile adds an OAuth 2.1 gateway for claude.ai, Claude Code, Cursor and any client that signs in with your OIDC provider.

flowchart LR
  strava[Strava API v3]
  subgraph server [strava-mcp container]
    direction TB
    limiter[rate limiter] --> queue[durable job queue] --> engine[sync engine]
    engine --> mirror[(DuckDB mirror)]
    mirror --> tools[MCP tools, resources, prompts]
    mirror -- periodic copy --> snapshot[(read-only snapshot)]
  end
  strava <-->|REST, webhook| limiter
  tools -->|Streamable HTTP, bearer or OAuth| client[Claude, Cursor, Codex, pi, ...]
  snapshot --> grafana[Grafana]
  snapshot --> marimo[marimo notebook]
  snapshot --> backup[backups]

Quickstart

With Docker Compose (v2.24 or later), after creating a Strava API application with localhost as its Authorization Callback Domain:

git clone https://github.com/justmytwospence/strava-mcp.git && cd strava-mcp
cp .env.example .env                                   # set STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, MCP_BEARER_TOKEN
docker compose run --rm -p 8765:8765 strava-mcp auth   # authorize once in the browser
docker compose up -d                                   # http://<host>:8000/mcp

Then connect a client, pasting the MCP_BEARER_TOKEN from .env (or source .env first):

claude mcp add --transport http strava http://localhost:8000/mcp --header "Authorization: Bearer $MCP_BEARER_TOKEN"

Without Docker, the npm package runs the same server over stdio for a client that spawns it (Claude Desktop, Codex, pi, ...), with the mirror in ./state/:

STRAVA_CLIENT_ID=12345 STRAVA_CLIENT_SECRET=... npx -y --package @justmytwospence/strava-mcp strava-mcp-auth
npx -y @justmytwospence/strava-mcp

The quickstart walks through the Strava application, the first sync and what it costs; clients covers Claude Code, Claude Desktop, claude.ai, Cursor, VS Code, Codex, ChatGPT, Gemini CLI and pi.

Related MCP server: fitness-mcp-server

Things you can ask

  • "How did my weekly running volume change over the last three months?"

  • "Compare all my efforts on segment 1234: is the improvement pace or heart rate?"

  • "What was my heart-rate drift in yesterday's long run?"

  • "List every ride over 100 km with more than 2,000 m of climbing."

  • "Which bike has the most kilometres this year?"

  • "Rename this morning's ride to 'Coffee ride' and mark it as a commute."

Most of these are one SQL query over the typed tables rather than dozens of API calls.

Documentation

justmytwospence.github.io/strava-mcp: quickstart, clients, FAQ, configuration, authentication, how sync works, the mirror and the snapshot, Grafana, notebooks, backups, operations, troubleshooting, tools reference, schema, export API.

Works well with garmin-mcp

garmin-mcp is the sibling project for Garmin Connect: the same design over sleep, HRV, Body Battery, training status and the original FIT files. Its optional Strava sync reads this server's export endpoint to rename Garmin activities after Strava and to remove courses for routes you have unstarred, without spending Strava API quota.

Strava's terms

Strava's API Agreement and API Policy restrict caching Strava data beyond seven days, storing it in persistent databases, and operating MCP servers or similar agent interfaces over the API; Strava's own MCP connector is the one it authorizes. This project mirrors a single athlete's own data onto that athlete's own hardware. Read the terms and the brand guidelines and decide for yourself. Not affiliated with or endorsed by Strava.

Contributing

See CONTRIBUTING.md for the layout, the checks (pnpm run lint, pnpm run typecheck, pnpm test), how to add a column or a table, and how releases are made.

License

PolyForm Noncommercial 1.0.0: free for personal use, research, education and noncommercial organizations. Commercial use requires a separate license; contact Spencer Boucher. Versions released before 2026-10-27 under MIT remain available under MIT.

Outside contributions require agreeing to a contributor license agreement; open an issue before sending a pull request.

Available Tools

33 tools
strava_create_activityCreate ActivityA

Create a manual activity entry (one API request). Requires activity:write scope and intent. Do not blindly retry a lost response; duplicate creation is possible.

Inputs: name (required), sport_type (required), elapsed_time (required), start_date_local (required), +4 more.

strava_create_uploadUpload Activity FileA

Upload an activity file (FIT, TCX, GPX) to create a new activity (one API request plus later status polls). Requires activity:write. Maximum decoded file size 25 MiB; canonical base64 only. Do not replay a lost response; reconcile first. Returns an upload ID for strava_get_upload.

Inputs: data_type (required), file_content (required), name, commute, +3 more.

strava_export_route_gpxExport Route as GPXA
Read-onlyIdempotent

Export a route as a GPX file. Returns raw GPX XML content.

Inputs: route_id (required).

strava_export_route_tcxExport Route as TCXA
Read-onlyIdempotent

Export a route as a TCX file. Returns raw TCX XML content.

Inputs: route_id (required).

strava_export_segment_gpxExport Segment as GPXA
Read-onlyIdempotent

Export a Strava segment as a GPX file for navigation. Returns GPX XML that can be imported into Garmin Connect, Coros, or other GPS devices. Note: The Strava API does not support uploading routes, so the GPX must be imported manually into your device/app.

Inputs: segment_id (required).

strava_get_activityGet ActivityA
Read-onlyIdempotent

Get detailed information about a specific activity including description, calories, splits, laps, segment efforts, best efforts, and gear. Served from the local mirror; recent activities are refreshed from Strava.

Inputs: activity_id (required), include_all_efforts.

strava_get_activity_streamsGet Activity StreamsA
Read-onlyIdempotent

Get high-resolution time-series data for an activity (heart rate, power, cadence, altitude, GPS coordinates, etc.). Data is downsampled if too large. Cache miss normally costs one API request. Missing stream rate headers mean unknown accounting, not free calls; check strava_sync_status or strava://mirror/budget first.

Inputs: keys (required), activity_id (required), max_points.

strava_get_athlete_statsGet Athlete StatsA
Read-onlyIdempotent

Get activity statistics for the authenticated athlete: recent, year-to-date, and all-time totals for runs, rides, and swims. Includes biggest ride distance and biggest climb.

Inputs: athlete_id.

strava_get_athlete_zonesGet Athlete ZonesB
Read-onlyIdempotent

Get the authenticated athlete's heart rate and power zones.

Inputs: no named parameters.

strava_get_authenticated_athleteGet Authenticated AthleteA
Read-onlyIdempotent

Get the profile of the currently authenticated athlete, including name, location, stats summary, bikes, and shoes.

Inputs: no named parameters.

strava_get_clubGet ClubA
Read-onlyIdempotent

Get detailed information about a club including description, member count, and sport type.

Inputs: club_id (required).

strava_get_gearGet GearA
Read-onlyIdempotent

Get equipment details by ID (name, brand, model, total distance). Gear IDs look like "b12345" for bikes or "g12345" for shoes.

Inputs: gear_id (required).

strava_get_routeGet RouteB
Read-onlyIdempotent

Get detailed route information including distance, elevation gain, and estimated moving time.

Inputs: route_id (required).

strava_get_route_streamsGet Route StreamsA
Read-onlyIdempotent

Get bounded/downsampled GPS, distance and altitude data for a route. A cache refresh normally costs one API request; check strava_sync_status or strava://mirror/budget first.

Inputs: route_id (required), max_points.

strava_get_segmentGet SegmentB
Read-onlyIdempotent

Get detailed information about a segment including distance, elevation, grade, effort count, and athlete count.

Inputs: segment_id (required).

strava_get_segment_effortGet Segment EffortB
Read-onlyIdempotent

Get a specific segment effort by ID, including elapsed time, heart rate, watts, and achievements.

Inputs: effort_id (required).

strava_get_segment_effort_streamsGet Segment Effort StreamsA
Read-onlyIdempotent

Get bounded/downsampled time-series data for a segment effort. Cache miss normally costs one API request; check strava_sync_status or strava://mirror/budget. Missing headers do not exempt requests from known quotas.

Inputs: keys (required), effort_id (required), max_points.

strava_get_segment_streamsGet Segment StreamsA
Read-onlyIdempotent

Get bounded/downsampled GPS, distance and altitude data for a segment. Cache miss normally costs one API request; check strava_sync_status or strava://mirror/budget first.

Inputs: keys (required), segment_id (required), max_points.

strava_get_uploadGet Upload StatusA
Read-onlyIdempotent

Read upload processing status directly from Strava (one API request per call, never mirrored). Returns status, error and the resulting activity ID. Poll sparingly; check strava_sync_status or strava://mirror/budget and respect cooldown.

Inputs: upload_id (required).

strava_list_activitiesList Athlete ActivitiesA
Read-onlyIdempotent

List the authenticated athlete's activities, newest first. Supports filtering by date range and pagination. Served from stored history, which can be incomplete during backfill. A stale listing refresh traverses upstream pages; local page size does not cap API requests. Check strava_sync_status or strava://mirror/budget before broad refreshes; narrow dates/use local SQL first.

Inputs: page, after, before, per_page.

strava_list_activity_commentsList Activity CommentsA
Read-onlyIdempotent

Page through stored comments. A cache refresh fetches the entire upstream list, potentially many API pages regardless of per_page.

Inputs: activity_id (required), page, per_page.

strava_list_activity_kudoersList Activity KudoersA
Read-onlyIdempotent

Page through stored kudoers. A cache refresh fetches the entire upstream list, potentially many API pages regardless of per_page.

Inputs: activity_id (required), page, per_page.

strava_list_activity_lapsList Activity LapsA
Read-onlyIdempotent

List laps of an activity. Each lap includes distance, time, speed, heart rate, watts, and elevation.

Inputs: activity_id (required).

strava_list_activity_zonesList Activity ZonesA
Read-onlyIdempotent

Get heart rate and power zone distribution for an activity. Shows time spent in each zone.

Inputs: activity_id (required).

strava_list_athlete_clubsList Athlete ClubsA
Read-onlyIdempotent

Page through stored clubs the athlete belongs to. Refresh fetches all upstream pages; local per_page does not cap API request cost.

Inputs: page, per_page.

strava_list_athlete_routesList Athlete RoutesB
Read-onlyIdempotent

Page through stored routes. Refresh fetches all upstream pages; local per_page does not cap API request cost.

Inputs: page, per_page, athlete_id.

strava_list_segment_effortsList Segment EffortsA
Read-onlyIdempotent

List the authenticated athlete's efforts on a segment, newest first, optionally filtered by date range. Limited to per_page results (no next-page cursor). Mirror history can be incomplete; fallback fetches all upstream pages before slicing, so per_page does not cap API calls. Check strava_sync_status or strava://mirror/budget; prefer stored SQL when budget is tight.

Inputs: segment_id (required), per_page, end_date_local, start_date_local.

strava_list_starred_segmentsList Starred SegmentsA
Read-onlyIdempotent

Page through stored starred segments. A stale refresh fetches the complete upstream list (multiple API pages); local per_page does not cap request cost.

Inputs: page, per_page.

strava_queryQuery the Strava Mirror (SQL)A
Read-onlyIdempotent

Run a read-only DuckDB SQL query against the athlete's stored Strava data (possibly incomplete; check sync status). Enforced read-only transaction, 30 seconds, 5000 rows maximum; no file/network access. Use strava_schema first to see the tables. Main tables: activity, activity_stream_point (per-point streams: time, distance, lat, lng, altitude, heartrate, cadence, watts, temp, velocity_smooth, grade_smooth), activity_lap, activity_split, activity_best_effort, activity_segment_effort, segment, activity_zone_bucket, gear, route, athlete_stats. Units: meters, seconds, m/s. raw_current holds every payload as JSON (column body) for fields not in a typed table. Costs no Strava API calls.

Inputs: sql (required), max_rows. Returns: note, rows, columns, row_count, +1 more

strava_schemaDescribe the Strava MirrorA
Read-onlyIdempotent

List the mirror's tables and views with their descriptions and row counts, or, given a table name, its columns with types, units and descriptions. Use before strava_query.

Inputs: table. Returns: table, tables, columns, comment

strava_star_segmentStar/Unstar SegmentA
DestructiveIdempotent

Star or unstar a segment for the authenticated athlete (one write API request). Unstarring removes an existing preference. Reconcile a lost response before retrying.

Inputs: starred (required), segment_id (required).

strava_sync_statusStrava Mirror Sync StatusA
Read-onlyIdempotent

How complete and fresh the local mirror is: activity coverage (details, streams, zones), the sync queue by kind, last run of each periodic task, recent errors, and unmapped fields. Zero API calls. Rate status distinguishes header observations from local estimates and unknown pools; missing headers never mean free calls. Check this or strava://mirror/budget before broad remote reads; 429 cooldown/reset times apply to all priorities.

Inputs: no named parameters.

strava_update_activityUpdate ActivityA
DestructiveIdempotent

Update an activity's mutable properties. Requires activity:write scope. Only provided fields are updated (one API request); replacing values can destroy prior content. Reconcile lost responses before retrying.

Inputs: activity_id (required), name, commute, gear_id, +4 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.6/5.0

    Scored across 33 tools

    Disambiguation4/5

    Most tools map cleanly to a distinct Strava resource plus action, and the stream/list variants are scoped by entity. The main conceptual overlap is between local mirror listing tools and the generic SQL query/schema tools, but the descriptions make the distinction fairly clear.

    Naming Consistency4/5

    Nearly all tools use a consistent strava_ snake_case prefix and verb_noun pattern, such as strava_list_activities and strava_create_upload. Minor deviations like strava_schema, strava_query, and strava_sync_status are still readable and predictable.

    Tool Count2/5

    With 33 tools, the surface is large enough to feel heavy and increases selection burden. The broad Strava domain justifies many endpoints, but several stream, export, and local-mirror operations could potentially be consolidated.

    Completeness4/5

    The server covers core Strava workflows well: activities, segments, efforts, routes, streams, clubs, gear, uploads, athlete stats, and SQL mirror access. Some gaps remain, such as club activities/members and a few mutation endpoints, though some may reflect Strava API limitations.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      Provides MCP servers for Claude to access fitness data from Strava and intervals.icu, enabling natural language queries for activity analysis, advanced training metrics, and wellness tracking.
      1
      -
    • F
      license
      Not graded
      quality
      D
      maintenance
      MCP server to fetch Strava activities using OAuth authentication with automatic token refresh. Allows retrieving recent activities, activities by date range, activity details, and athlete stats.
      -