strava-mcp
by elimchayseng
README.md
# strava-mcp
A remote [MCP](https://modelcontextprotocol.io) server on Cloudflare Workers
that lets an AI assistant read **your** Strava activities on demand. Tell it "I
just ran" and it pulls the run: laps, splits, heart rate, and a one-line read of
the workout's structure. For rides, it checks Strava's calorie number against
the power data.
Built for [Grok Bot](https://x.ai/bot); works with any MCP client that connects
to remote servers with OAuth. You deploy your own copy, with your own Strava API
app, and only your Strava account can connect to it.
```text
2 mi WU · 4x3 mi (6:36, 6:31, 6:23, 6:16) w/ 1 mi @ 7:50-8:05 · 3.1 mi CD
2.5 mi WU · 3x1 mi (6:40, 6:32, 6:26) w/ 3:00 rec · 4x200m (41s, 40s, 39s, 38s) w/ 2:00 rec · 2.2 mi CD
```
## Tools
| Tool | What it returns |
|---|---|
| `get_latest_activity` | Your newest activity in full detail, plus `ended_minutes_ago` so the assistant can tell when a new activity hasn't synced yet. |
| `list_activities` | Compact rows (id, date, name, sport, miles, moving time, pace or watts, HR), filtered by `since`, `until`, `sport` and `limit`. |
| `get_activity` | One activity by id. Runs: summary, laps, splits, workout structure, stops. Rides: summary plus a power-vs-calories check. |
All three are read-only. Units are miles, feet and min/mi. The full output
format is in [docs/DESIGN.md §5](docs/DESIGN.md#5-tools).
## How it works
One Worker is both the OAuth server for your MCP client and the MCP endpoint at
`/mcp`. When you connect, it shows a consent page, sends you to Strava to
approve read access, and refuses any Strava account but yours. Strava tokens
stay in a Durable Object that refreshes them under a lock, because Strava
invalidates the old refresh token when it issues a new one. The design and its
reasoning are in [docs/DESIGN.md](docs/DESIGN.md).
## Deploy your own
You need a Cloudflare account (the free plan works), a Strava account, and
Node 22.13 or later.
### 1. Work out your Worker's URL
In the Cloudflare dashboard, open **Workers & Pages** and find your account's
**workers.dev subdomain** (create one if you don't have one). Your Worker will be
at `https://strava-mcp.<subdomain>.workers.dev`.
### 2. Create a Strava API app
At <https://www.strava.com/settings/api>:
- **Authorization Callback Domain:** the host only, e.g.
`strava-mcp.<subdomain>.workers.dev`. No `https://`, no path.
- **Website:** your Worker URL.
- Copy the **Client ID** and **Client Secret**.
Use a separate app from anything else that reads your Strava data: two services
sharing one app would rotate each other's refresh tokens.
Your **athlete id** is the number in your profile URL,
`strava.com/athletes/<id>`.
### 3. Configure
```bash
npm install
```
```bash
cp wrangler.jsonc wrangler.local.jsonc
```
In `wrangler.local.jsonc` (gitignored), set:
```jsonc
"vars": {
"ALLOWED_ATHLETE_ID": "<your athlete id>",
"PUBLIC_URL": "https://strava-mcp.<subdomain>.workers.dev"
},
```
### 4. First deploy
Wrangler won't create a new Worker without its required secrets, so the first
deploy passes them in a file. Log in first:
```bash
npx wrangler login
```
Create the secrets file, readable only by you, with a generated remember-consent
secret:
```bash
(umask 077; printf 'STRAVA_CLIENT_ID=\nSTRAVA_CLIENT_SECRET=\nCONSENT_REMEMBER_SECRET=%s\n' "$(openssl rand -hex 32)" > .deploy-secrets)
```
Open `.deploy-secrets` and paste your Client ID and Client Secret after the `=`,
with no quotes or spaces. Then deploy and delete the file:
```bash
npx wrangler deploy -c wrangler.local.jsonc --secrets-file .deploy-secrets
```
```bash
rm .deploy-secrets
```
The deploy creates the `OAUTH_KV` namespace and prints its id. Pin it in
`wrangler.local.jsonc`, so later deploys reuse it:
```jsonc
"kv_namespaces": [{ "binding": "OAUTH_KV", "id": "<the id it printed>" }],
```
From now on, deploy with:
```bash
npm run deploy:local
```
### 5. Check it
```bash
curl https://strava-mcp.<subdomain>.workers.dev/health
```
It should show `{"ok":true,"athleteConfigured":true}`.
### 6. Connect
**Grok Bot:** add a connector with URL `https://strava-mcp.<subdomain>.workers.dev/mcp`
and OAuth auth, then authorize. Any other MCP client works the same way.
1. On the consent page, check the app name and the host access goes to, then
click **Continue to Strava**.
2. On Strava, keep **"View data about your private activities"** ticked.
To try it first in the MCP Inspector (Streamable HTTP transport, same URL):
```bash
npx @modelcontextprotocol/inspector
```
Suggested instructions for your assistant:
> You have a Strava connector. When I say I ran, rode or worked out, call
> `get_latest_activity` before responding. If `ended_minutes_ago` is large, tell
> me it may not have synced yet instead of reviewing an older activity. For a
> past activity, find it with `list_activities`, then call `get_activity`. For
> runs, lead with the `structure` line and use the laps to back it up. For
> rides, mention `energy.check` if the calories look off.
## Troubleshooting
| What you see | What it means |
|---|---|
| Strava says **invalid redirect_uri** | The app's callback domain doesn't exactly match your Worker's host. |
| **"Not allowed"** after Strava | You signed in to a Strava account other than `ALLOWED_ATHLETE_ID`. |
| **"Strava permission missing"** | The private-activities box was unticked. Connect again with it ticked. |
| **"Can't continue"** on the consent page | The page expired (10 minutes) or was reloaded. Start again from your client. |
| The assistant says **reconnect the Strava connector** | The stored Strava tokens are missing or were revoked. Reconnect. |
| `/health` shows `athleteConfigured: false` | `ALLOWED_ATHLETE_ID` is empty in the config you deployed. |
## Rotating secrets
To replace a secret, for example after regenerating the Client Secret on
Strava:
```bash
npx wrangler secret put STRAVA_CLIENT_SECRET -c wrangler.local.jsonc
```
It takes effect immediately. If you change `CONSENT_REMEMBER_SECRET`, remembered
consents are forgotten and the consent page shows again. If Strava access breaks,
reconnecting from your client replaces the stored Strava tokens.
## Develop
```bash
cp .dev.vars.example .dev.vars
```
```bash
npm run dev
```
```bash
npm test
```
```bash
npm run typecheck
```
After editing `wrangler.jsonc`, regenerate `worker-configuration.d.ts`:
```bash
npm run cf-typegen
```
Tests run in plain Node with a stubbed `cloudflare:workers`, `node:sqlite` in
place of the Durable Object's storage, and an in-memory KV. The test fixtures
are synthetic, built by `test/fixtures/generate.mjs`. For local OAuth against
real Strava, add `localhost` as a callback domain on your Strava app.
## Limits
- One athlete per deployment.
- Workout structure is read from lap paces: with auto-laps, rep boundaries are
only mile-accurate, and hill repeats can be mislabeled.
- Read limits are Strava's: 100 requests per 15 minutes and 1,000 per day.
## License
[MIT](LICENSE). Not affiliated with or endorsed by Strava. Your use of Strava
data is governed by the [Strava API Agreement](https://www.strava.com/legal/api),
which includes terms on using that data with AI models.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues