Skip to main content
Glama
README.md
# strava-server

Two servers over one Strava client:

- an **Express HTTP server** that runs the OAuth 2.0 flow and exposes the
  [Strava v3 API](https://developers.strava.com/docs/reference/) as JSON endpoints;
- an **MCP server** (stdio) that exposes the same data as tools for an AI agent.

Both share `src/strava/*` and the same token file, so you authorize once in the browser and the
MCP server picks the tokens up automatically.

## 1. Create a Strava API application

1. Sign in at <https://www.strava.com/settings/api> and create an app (a Strava subscription is required).
2. Set **Authorization Callback Domain** to `localhost`.
3. Copy the **Client ID** and **Client Secret**.

New apps start in *single-player mode* — only your own account can authorize them, which is exactly
what this server needs.

## 2. Configure

```powershell
Copy-Item .env.example .env
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"  # paste into SESSION_SECRET
```

Fill in `STRAVA_CLIENT_ID` and `STRAVA_CLIENT_SECRET` in `.env`.

`.env` and `.tokens.json` are gitignored — never commit them.

## 3. Run

```powershell
npm install
npm run dev          # HTTP server, watch mode
# or
npm run build; npm start
```

Then open <http://localhost:3000/auth/login> and click **Authorize**. Strava redirects back to
`/auth/callback`, the server exchanges the one-time code for a refresh token + access token, and
persists them to `.tokens.json` (mode `600`). Access tokens expire every 6 hours; the server
refreshes them automatically before each request, so you only authorize once.

## 4. MCP server

```powershell
npm run mcp          # stdio, via tsx
# or
npm run build; npm run mcp:start
```

VS Code picks up [.vscode/mcp.json](.vscode/mcp.json) automatically — open the Chat view, switch to
Agent mode and start the `strava` server. For other clients:

```json
{
  "mcpServers": {
    "strava": {
      "command": "node",
      "args": ["C:/path/to/stravamcp/dist/mcp/index.js"],
      "env": {
        "STRAVA_CLIENT_ID": "...",
        "STRAVA_CLIENT_SECRET": "..."
      }
    }
  }
}
```

`.env` and the token file are resolved relative to the package directory, not the working
directory, so the MCP server works no matter where the client launches it from.

### Tools

| Tool | Strava endpoint |
| --- | --- |
| `strava_auth_status` | Connection state, granted scopes, token expiry |
| `strava_get_athlete_summary` | Profile + totals + recent activities in one call |
| `strava_get_athlete` | `GET /athlete` |
| `strava_get_athlete_stats` | `GET /athletes/{id}/stats` |
| `strava_get_athlete_zones` | `GET /athlete/zones` |
| `strava_list_activities` | `GET /athlete/activities` (`before`/`after` accept ISO dates) |
| `strava_get_activity` | `GET /activities/{id}` |
| `strava_get_activity_laps` | `GET /activities/{id}/laps` |
| `strava_get_activity_zones` | `GET /activities/{id}/zones` |
| `strava_get_activity_streams` | `GET /activities/{id}/streams` (downsampled to `max_points`) |
| `strava_get_activity_comments` | `GET /activities/{id}/comments` |
| `strava_get_activity_kudoers` | `GET /activities/{id}/kudos` |
| `strava_list_clubs` / `strava_get_club` | `GET /athlete/clubs`, `GET /clubs/{id}` |
| `strava_get_gear` | `GET /gear/{id}` |
| `strava_list_routes` / `strava_get_route` | `GET /athletes/{id}/routes`, `GET /routes/{id}` |
| `strava_list_starred_segments` / `strava_get_segment` | `GET /segments/starred`, `GET /segments/{id}` |
| `strava_get_rate_limit` | Last-seen rate-limit headers |

All tools are read-only and annotated as such. Responses that would blow up an agent's context are
trimmed by default: segment efforts and full-resolution polylines are opt-in, and streams are
evenly downsampled (200 points by default).

If a tool reports "Not connected to Strava", run the HTTP server once and complete
`/auth/login` — the MCP server reads the same `.tokens.json`.

## HTTP endpoints

### Auth

| Method | Path | Description |
| --- | --- | --- |
| GET | `/auth/login` | Redirect to Strava's consent screen (CSRF-protected via a signed `state` cookie) |
| GET | `/auth/callback` | Code → token exchange |
| GET | `/auth/status` | Connection state, granted scope, token expiry |
| POST | `/auth/logout` | Deauthorize with Strava and delete the local tokens |

### Data

| Method | Path | Strava endpoint |
| --- | --- | --- |
| GET | `/api/me` | Aggregated profile + totals + last 10 activities (formatted in km/min) |
| GET | `/api/athlete` | `GET /athlete` |
| GET | `/api/athlete/stats` | `GET /athletes/{id}/stats` |
| GET | `/api/athlete/zones` | `GET /athlete/zones` |
| GET | `/api/athlete/clubs` | `GET /athlete/clubs` |
| GET | `/api/athlete/routes` | `GET /athletes/{id}/routes` |
| GET | `/api/activities` | `GET /athlete/activities` (`page`, `per_page`, `before`, `after`) |
| GET | `/api/activities/:id` | `GET /activities/{id}` (`include_all_efforts=true`) |
| GET | `/api/activities/:id/laps` | `GET /activities/{id}/laps` |
| GET | `/api/activities/:id/zones` | `GET /activities/{id}/zones` |
| GET | `/api/activities/:id/comments` | `GET /activities/{id}/comments` |
| GET | `/api/activities/:id/kudos` | `GET /activities/{id}/kudos` |
| GET | `/api/activities/:id/streams` | `GET /activities/{id}/streams` (`keys=time,distance,heartrate`) |
| GET | `/api/clubs/:id` | `GET /clubs/{id}` |
| GET | `/api/gear/:id` | `GET /gear/{id}` |
| GET | `/api/routes/:id` | `GET /routes/{id}` |
| GET | `/api/segments/starred` | `GET /segments/starred` |
| GET | `/api/segments/:id` | `GET /segments/{id}` |
| GET | `/api/rate-limit` | Last-seen `X-RateLimit-*` / `X-ReadRateLimit-*` values |

Example:

```powershell
curl http://localhost:3000/api/me
curl "http://localhost:3000/api/activities?per_page=50&after=1704067200"
curl "http://localhost:3000/api/activities/123456789/streams?keys=time,heartrate,watts"
```

## Scopes

`STRAVA_SCOPE` defaults to `read,read_all,profile:read_all,activity:read_all`:

- `profile:read_all` — detailed athlete representation and `/athlete/zones`
- `activity:read_all` — "Only Me" activities, otherwise they are filtered out
- `read_all` — private routes and segments

If you change the scope you must re-run `/auth/login`; `/auth/callback` reports any scope the
athlete declined under `missing_scope`.

## Rate limits

Default limits are 200 requests / 15 min and 2,000 / day. The client records the limit headers
from every response; a `429` is surfaced as a JSON error with the current usage. From the MCP
server, call `strava_get_rate_limit` before issuing a burst of requests.

## Notes

- `/activities/{id}/zones` and `/segment_efforts` require a Strava subscription.
- Tokens are stored on disk in plain JSON. Keep the file local; for a multi-user deployment
  replace `src/strava/token-store.ts` with an encrypted, per-user store.

TDQS

A3.9/5.0

Scored across 20 tools

Disambiguation5/5

Each tool targets a distinct resource (athlete, activity, club, gear, route, segment) and action (get/list), with utility tools (rate limit, auth status) clearly separated. Even similar tools like get_athlete_summary and get_athlete_stats differ in granularity and purpose, making misselection unlikely.

Naming Consistency4/5

Most tools follow a consistent verb_noun pattern (get_*, list_*), but strava_auth_status breaks the pattern by using a noun phrase instead of a verb. Additionally, get_athlete_zones vs get_activity_zones could be mistaken without careful reading, though the resource is explicit in each name.

Tool Count3/5

With 20 tools, the server is on the heavy side, exceeding the 15-tool threshold for a well-scoped set. However, each tool maps to a specific Strava API endpoint, and the count is justified by the wide read-only surface of the Strava API, so it is borderline rather than excessive.

Completeness4/5

The server covers the primary Strava resources (athlete, activities, clubs, gear, routes, segments) with both list and detail views, plus utility tools. Obvious gaps like activity upload or a dedicated segment efforts tool are absent, but agents can obtain segment efforts via activity detail and upload is not a standard read-only operation.

Maintenance

ActivitySlowing
ResponsivenessNo issues