Initial commit: Modularized Strava MCP Server with UV and Hatchling
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
# 🚴 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](https://github.com/jlowin/fastmcp) and the [MCP Python SDK](https://github.com/modelcontextprotocol/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](#requirements)
|
||||
- [Installation](#installation)
|
||||
- [Strava API Setup](#strava-api-setup)
|
||||
- [Configuration](#configuration)
|
||||
- [Running the Server](#running-the-server)
|
||||
- [Connecting with MCP Inspector](#connecting-with-mcp-inspector)
|
||||
- [MCP Primitives](#mcp-primitives)
|
||||
|
||||
- [Tools](#tools)
|
||||
- [Prompts](#prompts)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Known Strava API Limitations](#known-strava-api-limitations)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.10+
|
||||
- [uv](https://github.com/astral-sh/uv) (recommended) or pip
|
||||
- A [Strava account](https://www.strava.com) with API access
|
||||
- A [Strava API Application](https://www.strava.com/settings/api)
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
1. Go to [https://www.strava.com/settings/api](https://www.strava.com/settings/api)
|
||||
2. Create a new application
|
||||
3. Set **Authorization Callback Domain** to `localhost`
|
||||
4. 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:
|
||||
|
||||
```bash
|
||||
uv run strava-mcp-get-token
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Open your browser with the Strava authorization page
|
||||
2. Ask you to authorize the application
|
||||
3. Automatically capture the authorization code
|
||||
4. Exchange it for access and refresh tokens
|
||||
5. Save the `STRAVA_REFRESH_TOKEN` to your `.env` file
|
||||
|
||||
> **Required OAuth Scopes:**
|
||||
> `activity:read_all,profile:read_all,read`
|
||||
>
|
||||
> - `activity:read_all` — Access all activities including private ones
|
||||
> - `profile:read_all` — Access full profile info including heart rate zones
|
||||
> - `read` — Access public data
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Copy the example environment file and fill in your credentials:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
```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_TOKEN` is written automatically by `get_token.py`. You only need to fill in `STRAVA_CLIENT_ID` and `STRAVA_CLIENT_SECRET` manually.
|
||||
|
||||
---
|
||||
|
||||
## Running the Server
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
lsof -ti :8000 | xargs kill -9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Connecting with MCP Inspector
|
||||
|
||||
1. Open [MCP Inspector](https://inspector.modelcontextprotocol.io/) or run it locally
|
||||
2. Select transport: **Streamable HTTP**
|
||||
3. Enter URL: `http://localhost:8000/mcp`
|
||||
4. Click **Connect**
|
||||
|
||||
> ⚠️ Make sure to use `http://localhost:8000/mcp` **without a trailing slash** and with the `Accept: application/json, text/event-stream` header (handled automatically by MCP clients).
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to your `claude_desktop_config.json`:
|
||||
|
||||
```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 |
|
||||
|
||||
> *`before` and `after` are 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_details` response. 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 `b` for bikes and `g` for shoes (e.g. `b12345678`). Find them in `get_activity_details` under `gear_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_details` to access segment efforts embedded in activity data. The `segment_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:
|
||||
|
||||
```bash
|
||||
lsof -ti :8000 | xargs kill -9
|
||||
```
|
||||
|
||||
### `401 Unauthorized` on token refresh
|
||||
|
||||
Your refresh token has expired or been rotated. Generate a new one:
|
||||
|
||||
```bash
|
||||
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](https://github.com/jlowin/fastmcp) |
|
||||
| HTTP Transport | Uvicorn + Starlette (via FastMCP) |
|
||||
| Strava API Client | [httpx](https://www.python-httpx.org/) |
|
||||
| Environment Config | [python-dotenv](https://github.com/theskumar/python-dotenv) |
|
||||
| Package Manager | [uv](https://github.com/astral-sh/uv) |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Reference in New Issue
Block a user