11 KiB
🚴 Strava MCP Server
A production-ready Model Context Protocol (MCP) server that exposes the Strava API v3 as MCP-compatible tools, resources, and prompts — enabling AI agents and LLM-based applications to query your Strava training data through a standardized interface.
Built with FastMCP and the MCP Python SDK, compatible with MCP Inspector, Claude Desktop, and any MCP-compliant client.
✨ Features
- 🛠️ 25+ MCP Tools covering all major Strava API read endpoints (including athlete profile & stats)
- 💬 2 MCP Prompts for structured AI-driven training analysis
- 🔄 Automatic OAuth token rotation — tokens are refreshed transparently
- 🌐 Streamable HTTP transport for broad client compatibility
- 🔒 Read-only — no write operations, safe to use with AI agents
📋 Table of Contents
Requirements
- Python 3.10+
- uv (recommended) or pip
- A Strava account with API access
- A Strava API Application
Installation
# Clone the repository
git clone <your-repo-url>
cd strava-mcp-server
# Install dependencies with uv (also installs the package itself)
uv sync
# Or install directly from PyPI (once published)
uvx strava-mcp-server
# or: pip install strava-mcp-server
Strava API Setup
1. Create a Strava API Application
- Go to https://www.strava.com/settings/api
- Create a new application
- Set Authorization Callback Domain to
localhost - Note your Client ID and Client Secret
2. Generate Your OAuth Tokens
The server uses the OAuth 2.0 refresh token flow. Run the token helper script to authorize and retrieve your initial tokens:
uv run strava-mcp-get-token
This will:
- Open your browser with the Strava authorization page
- Ask you to authorize the application
- Automatically capture the authorization code
- Exchange it for access and refresh tokens
- Save the
STRAVA_REFRESH_TOKENto your.envfile
Required OAuth Scopes:
activity:read_all,profile:read_all,read
activity:read_all— Access all activities including private onesprofile:read_all— Access full profile info including heart rate zonesread— Access public data
Configuration
Copy the example environment file and fill in your credentials:
cp .env.example .env
Edit .env:
STRAVA_CLIENT_ID=your_client_id_here
STRAVA_CLIENT_SECRET=your_client_secret_here
STRAVA_REFRESH_TOKEN=your_refresh_token_here
Note: The
STRAVA_REFRESH_TOKENis written automatically byget_token.py. You only need to fill inSTRAVA_CLIENT_IDandSTRAVA_CLIENT_SECRETmanually.
Running the Server
uv run strava-mcp
Expected output:
🚀 Starting Strava MCP Server on http://0.0.0.0:8000
MCP endpoint: http://0.0.0.0:8000/mcp (Streamable HTTP)
INFO: Started server process [12345]
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
The server listens on port 8000 by default and exposes a single endpoint:
http://localhost:8000/mcp
Stopping the Server
Press Ctrl+C for a clean shutdown. If the port is already in use from a previous run:
lsof -ti :8000 | xargs kill -9
Connecting with MCP Inspector
- Open MCP Inspector or run it locally
- Select transport: Streamable HTTP
- Enter URL:
http://localhost:8000/mcp - Click Connect
⚠️ Make sure to use
http://localhost:8000/mcpwithout a trailing slash and with theAccept: application/json, text/event-streamheader (handled automatically by MCP clients).
Claude Desktop
Add to your claude_desktop_config.json:
{
"mcpServers": {
"strava": {
"url": "http://localhost:8000/mcp",
"transport": "streamable-http"
}
}
}
MCP Primitives
Tools
🏃 Athlete
| Tool | Parameters | Description |
|---|---|---|
get_athlete_profile |
— | Full athlete profile: name, city, country, follower count, gear list |
get_athlete_stats |
— | Training totals: all-time, year-to-date, and last 4 weeks for runs, rides, and swims |
get_athlete_zones |
— | Heart rate and power zones |
🚴 Activities
| Tool | Parameters | Description |
|---|---|---|
list_activities |
limit, page, before, after |
Paginated activity list with optional time range filters |
get_activity_details |
activity_id |
Full activity details incl. segment efforts (IDs returned as strings) |
get_activity_laps |
activity_id |
Lap splits |
get_activity_zones |
activity_id |
Heart rate and power zones for a specific activity |
get_activity_comments |
activity_id, limit |
Comments on an activity |
get_activity_kudoers |
activity_id, limit |
Athletes who gave kudos |
get_activity_streams |
activity_id, keys |
Raw GPS/sensor data streams |
*
beforeandafterare Unix timestamps. Example:after=1704067200= since 2024-01-01.
🏛️ Clubs
| Tool | Parameters | Description |
|---|---|---|
list_athlete_clubs |
limit |
Clubs the athlete is a member of |
get_club |
club_id |
Club details |
get_club_activities |
club_id, limit |
Recent club activity feed |
get_club_members |
club_id, limit |
Club members |
🗺️ Routes
| Tool | Parameters | Description |
|---|---|---|
get_routes_by_athlete_id |
limit |
Athlete's created routes |
get_route_by_id |
route_id: str |
Route details including segments |
get_route_streams |
route_id: str |
Route GPS/elevation streams |
⚠️ Route IDs are returned as strings to prevent JavaScript float precision loss (IDs exceed
Number.MAX_SAFE_INTEGER).
📍 Segments
| Tool | Parameters | Description |
|---|---|---|
get_segment |
segment_id |
Segment details, grade, elevation, effort counts |
list_starred_segments |
limit |
Segments starred by the athlete |
explore_segments |
bounds, activity_type |
Discover popular segments in a geographic bounding box |
get_segment_streams |
segment_id, keys |
Segment GPS/elevation streams |
⚡ Segment Efforts
| Tool | Parameters | Description |
|---|---|---|
get_segment_effort |
effort_id: str |
Details for a specific effort — requires Strava subscription |
list_segment_efforts |
segment_id, start_date_local, end_date_local, limit |
Athlete's efforts on a segment — requires Strava subscription |
get_segment_effort_streams |
effort_id: str, keys |
Raw streams for a segment effort — requires Strava subscription |
ℹ️ Effort IDs must be copied as strings from
get_activity_detailsresponse. Do not retype the numbers manually — they exceed JavaScript's safe integer range.
⚙️ Gear
| Tool | Parameters | Description |
|---|---|---|
get_gear_by_id |
gear_id: str |
Bike or shoe details: brand, model, total distance |
Gear IDs start with
bfor bikes andgfor shoes (e.g.b12345678). Find them inget_activity_detailsundergear_id.
Prompts
Prompts pre-structure AI conversations with the right tool-calling instructions.
analyze_activity
Parameters: activity_id (str)
Triggers a structured analysis of a specific activity including summary, performance metrics, segment highlights, gear used, and key takeaways.
training_summary
Parameters: weeks (int, default: 4)
Generates a training load report for the last N weeks: volume by sport, highlights, trend vs. year-to-date, and recommendations. Automatically calculates the required Unix timestamp for filtering.
Project Structure
strava-mcp-server/
├── src/
│ └── strava_mcp/ # Installable Python package
│ ├── __init__.py
│ ├── main.py # Server entrypoint → strava-mcp command
│ ├── tools.py # All MCP tools, resources, and prompts
│ ├── strava_client.py # Strava API client with auto token rotation
│ └── get_token.py # OAuth2 flow → strava-mcp-get-token command
├── tests/
│ └── test_strava.py # Manual API integration tests
├── pyproject.toml # Package metadata, dependencies, entry points
├── .env # Credentials (not committed)
├── .env.example # Credential template
├── TODO.md # Planned next steps
└── README.md # This file
Known Strava API Limitations
The following endpoints are restricted by Strava and return errors for regular API users:
| Endpoint | Status | Reason |
|---|---|---|
GET /segments/{id}/leaderboard |
403 Forbidden |
Requires Strava API partnership |
GET /segment_efforts/{id} |
403 Forbidden |
Requires Strava API partnership |
GET /segment_efforts?segment_id=X |
200 [] (empty) |
Silently restricted since 2018 — data accessible via get_activity_details |
GET /athlete/zones |
401 Unauthorized |
Requires profile:read_all OAuth scope |
Workaround for segment efforts: Use
get_activity_detailsto access segment efforts embedded in activity data. Thesegment_efforts[]array contains effort IDs, times, heart rate, power, and PR/KOM ranks.
Troubleshooting
[Errno 48] Address already in use
Port 8000 is occupied by a previous server process:
lsof -ti :8000 | xargs kill -9
401 Unauthorized on token refresh
Your refresh token has expired or been rotated. Generate a new one:
uv run strava-mcp-get-token
Empty results from list_segment_efforts
The direct Strava endpoint is restricted. Access segment efforts via get_activity_details instead — the full activity response includes all segment efforts.
406 Not Acceptable from MCP endpoint
The MCP client is missing the required Accept: application/json, text/event-stream header. Use a proper MCP client (MCP Inspector, Claude Desktop) instead of raw curl.
Route or effort 404 Not Found with a large ID
The ID was truncated due to JavaScript float precision. Always use the string IDs returned by the server — never copy-type large numeric IDs manually.
Tech Stack
| Component | Library |
|---|---|
| MCP Server | FastMCP |
| HTTP Transport | Uvicorn + Starlette (via FastMCP) |
| Strava API Client | httpx |
| Environment Config | python-dotenv |
| Package Manager | uv |
License
MIT