strava-mcp
# strava-mcp
A [Model Context Protocol](https://modelcontextprotocol.io) server that gives AI assistants such as
Claude Desktop read access to your own Strava data. Ask about your training in plain language and
the assistant pulls the activities, splits and totals it needs.
It is built for athletes who do more than one sport: runs are described by pace, ski days by speed,
and hikes and ski tours lead with the elevation you climbed.
## Demo
<!-- demo: docs/demo.gif -->
## Example questions
- "How has my running pace developed over the last 3 months?"
- "Summarize my ski days this season."
- "How much climbing did I do on hikes and ski tours this year?"
- "Break down the splits of my last tempo run. Did I fade in the second half?"
- "How many kilometers are on my trail shoes?"
- "Compare my running volume this year with my all-time average."
## Features
- **Four read-only tools** covering profile, activity lists, activity details and totals.
- **Sport-aware formatting**: pace (min/km) for runs, speed (km/h) for skiing and cycling,
elevation-first summaries for hikes and backcountry skiing. Always metric.
- **Compact output for LLMs**: readable text plus structured content with only the fields that
matter, instead of raw API responses.
- **Automatic token refresh**: expired access tokens are refreshed and saved transparently.
- **Rate-limit aware**: limits are read from Strava's response headers, and rate-limit errors say
which limit was hit and when it resets.
- **Clear errors** for missing authorization, unknown IDs, rate limits and network failures.
- **Local and private**: runs on your machine over stdio; tokens are stored outside the repository
with owner-only file permissions.
## Tools
| Tool | Description |
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `get_athlete_profile` | Name, location, weight, preferred units and gear with total distance. |
| `get_recent_activities` | Activities newest first. Optional `sport_type` (one or a list, e.g. `Run`, `TrailRun`, `Hike`, `AlpineSki`, `BackcountrySki`, `NordicSki`), `after`, `before` and `limit`. |
| `get_activity_details` | One activity by `activity_id`: summary, description, gear, splits per km, laps and best efforts. |
| `get_athlete_stats` | Last 4 weeks, year-to-date and all-time totals for runs, rides and swims, as reported by Strava. |
`get_recent_activities` returns 10 activities by default and at most 100. Strava's API cannot
filter by sport, so the server pages through your activities and filters them itself, scanning at
most 2,000 activities per call to protect your rate limit.
`get_athlete_stats` is limited by Strava itself: it only counts activities with "Everyone"
visibility and has no totals for hiking or skiing. Ask about those sports through
`get_recent_activities` instead.
## Requirements
- Node.js 20.12 or newer
- A Strava account
- An MCP client, for example [Claude Desktop](https://claude.ai/download)
## Setup
### 1. Create a Strava API application
1. Go to [strava.com/settings/api](https://www.strava.com/settings/api).
2. Create an application. Name, category and website can be anything that describes your personal
use.
3. Set **Authorization Callback Domain** to `localhost`.
4. Note the **Client ID** and **Client Secret**.
### 2. Install and build
```sh
git clone https://github.com/<your-username>/strava-mcp.git
cd strava-mcp
npm install
npm run build
```
### 3. Configure credentials
```sh
cp .env.example .env
```
Fill in `STRAVA_CLIENT_ID` and `STRAVA_CLIENT_SECRET`. The `.env` file is only read by
`npm run auth`; the MCP client passes the same values to the server through its own configuration.
| Variable | Required | Description |
| ---------------------- | -------- | -------------------------------------------------------------------------------- |
| `STRAVA_CLIENT_ID` | yes | Client ID of your Strava application. |
| `STRAVA_CLIENT_SECRET` | yes | Client secret of your Strava application. |
| `STRAVA_TOKEN_PATH` | no | Token file location. Defaults to `~/.config/strava-mcp/tokens.json` (see below). |
| `STRAVA_AUTH_PORT` | no | Local port for the OAuth callback during `npm run auth`. Defaults to `8765`. |
| `STRAVA_MCP_DEBUG` | no | Set to `1` to log requests and rate-limit usage to stderr. |
### 4. Authorize with Strava
```sh
npm run auth
```
This starts a temporary server on `127.0.0.1`, prints the Strava authorization URL and tries to
open it in your browser. Approve the requested permissions (`read`, `activity:read_all` and
`profile:read_all`) and the tokens are saved; the temporary server then shuts down.
Tokens are stored in `$XDG_CONFIG_HOME/strava-mcp/tokens.json` (usually
`~/.config/strava-mcp/tokens.json`) on macOS and Linux, and in
`%APPDATA%\strava-mcp\tokens.json` on Windows, with `0600` permissions. Run `npm run auth` again at
any time to reconnect or switch accounts.
## Use with Claude Desktop
Open the Claude Desktop configuration file:
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
Add the server, using the absolute path to your clone:
```json
{
"mcpServers": {
"strava": {
"command": "node",
"args": ["/absolute/path/to/strava-mcp/dist/index.js"],
"env": {
"STRAVA_CLIENT_ID": "12345",
"STRAVA_CLIENT_SECRET": "your-client-secret"
}
}
}
}
```
Restart Claude Desktop. The Strava tools appear in the tools menu of a new chat.
**Troubleshooting**
- _"No Strava tokens found"_: run `npm run auth`. The server and `npm run auth` must resolve the
same token file. If they run in different environments (for example Claude Desktop on Windows
and the repository inside WSL), set `STRAVA_TOKEN_PATH` to the same file in both places.
- Server logs go to stderr, which Claude Desktop writes to its MCP log files
(`~/Library/Logs/Claude/mcp-server-strava.log` on macOS, `%APPDATA%\Claude\logs` on Windows).
## Development
```sh
npm run dev # run the server from source with tsx
npm run build # compile to dist/
npm test # run the test suite
npm run test:watch # tests in watch mode
npm run test:coverage # tests with coverage report
npm run lint # ESLint
npm run typecheck # TypeScript without emitting
npm run format # Prettier
npm run check # typecheck, lint, format check and tests
```
The tests never call the real Strava API. HTTP is mocked at the `fetch` level with realistic
fixtures for runs, hikes and ski tours in `tests/fixtures/`.
To inspect the server interactively, use the
[MCP Inspector](https://github.com/modelcontextprotocol/inspector):
```sh
npx @modelcontextprotocol/inspector node dist/index.js
```
## Architecture
```
src/
index.ts entry point: config, client, stdio transport
server.ts creates the MCP server, registers tools, maps errors to tool results
config.ts environment variables and token file location
strava/
client.ts the only place that talks to the Strava API
auth.ts token storage, OAuth code exchange and refresh
errors.ts Strava error types and HTTP status mapping
rateLimit.ts rate-limit header parsing and reset times
types.ts the subset of Strava response models in use
tools/
index.ts list of registered tools
types.ts the shared tool contract
get*.ts one file per tool
utils/
format.ts unit conversion and formatting
activity.ts sport profiles and activity summaries
logger.ts stderr-only logger
scripts/
auth.ts one-time OAuth login (npm run auth)
```
A tool call flows through three layers:
1. **Server** (`server.ts`) validates the input against the tool's zod schema and calls the handler.
Any error is converted into a tool result with `isError: true`, so the assistant can explain it
instead of the call failing silently.
2. **Tool** (`tools/*.ts`) calls typed methods on the shared client, turns the response into a
compact summary and returns it as text plus structured content.
3. **Client** (`strava/client.ts`) adds the bearer token, refreshes it when it is about to expire
(sharing one refresh between concurrent calls), and maps HTTP failures to `StravaApiError`.
Because stdout carries the MCP protocol, the server never writes to it; all logging goes to stderr,
and ESLint forbids `console` in `src/`.
### Adding a tool
Create a file in `src/tools/`:
```ts
import { z } from 'zod';
import { defineTool, READ_ONLY_ANNOTATIONS } from './types.js';
export const getGear = defineTool({
name: 'get_gear',
title: 'Get gear',
description: 'Get details for a piece of gear by its ID.',
inputSchema: {
gear_id: z.string().min(1).describe('Strava gear ID, for example "g12345".'),
},
annotations: READ_ONLY_ANNOTATIONS,
async handler({ gear_id: gearId }, { client }) {
const gear = await client.get<{ name: string; distance: number }>(`/gear/${gearId}`);
return { text: `${gear.name}: ${(gear.distance / 1000).toFixed(1)} km` };
},
});
```
Then add it to the list in `src/tools/index.ts`. Input validation, error handling and registration
come from the shared tool contract.
## Roadmap
- **Weekly training summary**: volume, time and elevation per week and sport.
- **Personal records**: best efforts over standard distances and how they changed.
- **Compare two activities**: side-by-side pace, heart rate and splits.
- **Training load trends**: acute and chronic load based on duration and heart rate.
## Strava API usage
This project is intended for personal use with your own Strava data. You create and use your own
Strava API application, and you are responsible for following the
[Strava API Agreement](https://www.strava.com/legal/api) and Strava's
[brand guidelines](https://developers.strava.com/guidelines/). This project is not affiliated with
or endorsed by Strava.
## License
[MIT](LICENSE)
TDQS
Scored across 4 tools
Each tool targets a distinct resource: athlete profile, recent activity list, individual activity details, and athlete stats. The descriptions cross-reference each other clearly (e.g., using get_recent_activities to find IDs for get_activity_details), eliminating ambiguity.
All tool names follow a consistent get_<noun> pattern, using snake_case throughout. The naming clearly distinguishes list/detail operations without introducing mixed conventions or vague verbs.
Four tools is a compact but reasonable set for a read-only Strava personal data server. While slightly small, the count is appropriate for the focused scope and each tool covers a necessary data retrieval need.
The server covers the core personal data lifecycle: profile, stats, activity list, and activity details. It lacks streams/segments or social features, but these are not obvious gaps for the described purpose of retrieving an athlete's summary data.