Fitness MCP
Provides read-only access to a Strava athlete's activities and profile data, including private activities, through the Strava REST API.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Fitness MCPHow many miles did I run last week?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Fitness MCP · Strava
A private MCP server built with Node.js 22, TypeScript, and PostgreSQL. ChatGPT connects to /mcp over Streamable HTTP and authenticates through our OAuth Authorization Code + PKCE S256 flow. The server uses the official Strava REST API and refreshes Strava tokens automatically. It does not use mcp.strava.com.
ChatGPT → HTTPS / Cloudflare Tunnel → Fastify /mcp → Strava REST API
↕
PostgreSQLThe service provides read-only access. Only Strava athlete IDs listed in STRAVA_ALLOWED_ATHLETE_IDS can connect; an arbitrary first visitor cannot become the owner. Users and connections are separate database entities, but the initial deployment is intended for one server instance and one owner. Redis is not required.
Quick start with Docker Compose
cp .env.example .env
chmod 600 .env
openssl rand -hex 24
openssl rand -hex 32
openssl rand -hex 32Use the first generated value as the PostgreSQL password in both POSTGRES_PASSWORD and DATABASE_URL. Use the second for SESSION_SECRET and the third for TOKEN_ENCRYPTION_KEY. Fill in the remaining required settings described below. .env contains secrets and must not be committed to git.
docker compose up -d --build
docker compose ps
curl --fail http://127.0.0.1:3000/healthz
curl --fail http://127.0.0.1:3000/readyzCompose starts PostgreSQL, applies SQL migrations through the separate migrate service, and then starts the application. /healthz checks the process; /readyz checks database readiness and connectivity. The application port is published only on 127.0.0.1:3000 on the host; the database port is not published. The application container runs as a non-root user with a read-only filesystem and a tmpfs mount for /tmp.
Related MCP server: Strava MCP Server
Set up a Strava application
Create an application in Strava API settings.
Set Authorization Callback Domain to
strava-mcp.example.com, without a scheme or path.Set
STRAVA_CLIENT_ID,STRAVA_CLIENT_SECRET, andSTRAVA_REDIRECT_URI=https://strava-mcp.example.com/oauth/strava/callbackin.env.Set
STRAVA_ALLOWED_ATHLETE_IDSto your numeric athlete ID from your Strava profile URL. Separate multiple allowed IDs with commas.Grant
read,activity:read_allduring authorization. If you need private profile fields and gear, addprofile:read_alland reconnect your account.
read and activity:read_all are official read-only scopes; the latter also grants access to the owner's private activities. No write scopes are requested. Field availability depends on the granted permissions and the account's data. See Strava OAuth.
Find your athlete ID
Sign in to the Strava website in a browser, click your profile picture, and open your profile. The number after /athletes/ in the address bar is your athlete ID. For example:
https://www.strava.com/athletes/12345678For this example, set:
STRAVA_ALLOWED_ATHLETE_IDS=12345678Use your own profile's number. The athlete ID is separate from your Strava application Client ID and from an activity ID. Mobile sharing may produce a shortened strava.app.link URL; use the profile URL in a web browser to find the numeric ID. See Strava: Where Do I Find My Profile URL?.
Environment variables
Variable | Value / purpose |
| Required public HTTPS origin, such as |
| Credentials for your Strava API application |
| Exactly |
| Required list of allowed numeric athlete IDs |
| Defaults to |
| PostgreSQL password for Compose; use a random hexadecimal value |
| In Compose: |
| Random cookie-signing secret, at least 32 characters |
| AES-256-GCM key: exactly 64 hexadecimal characters |
| Comma-separated list of exact allowed MCP client callback URIs |
| Local defaults: |
| Defaults to |
| Defaults to |
| Required only for the optional |
| Separate integration-test database; never use production |
Keep the encryption key unchanged during normal restarts: existing Strava tokens are encrypted with that key. Keep backups of the database and the key outside the public repository. Changing POSTGRES_PASSWORD in .env does not change the password of an existing PostgreSQL user in an initialized volume.
Cloudflare Tunnel
If cloudflared already runs on the host, add the public hostname strava-mcp.example.com with origin service http://127.0.0.1:3000. Configure HTTPS for the public hostname in Cloudflare. Example ingress for a locally managed tunnel:
ingress:
- hostname: strava-mcp.example.com
service: http://127.0.0.1:3000
- service: http_status:404For a separate connector container, set TUNNEL_TOKEN, configure the Cloudflare service as http://app:3000, and run:
docker compose --profile tunnel up -d --buildInside the connector container, 127.0.0.1 refers to the connector itself, so use the app service's DNS name. An existing connector in a different Docker project needs a shared Docker network or access to the host port. See the official Cloudflare Tunnel setup guide.
For the MCP hostname, disable Cloudflare Cache Everything and browser challenges / Access login on /mcp, /oauth/*, and /.well-known/*: machine clients must receive application responses. The server's OAuth protects access to data. Standard Cloudflare filtering and limits can remain enabled as long as they do not block OAuth and MCP requests.
TRUST_PROXY=false is sufficient for constructing OAuth URLs: the server uses PUBLIC_BASE_URL. To rate-limit by the visitor's actual IP address, configure the trusted address of the immediate proxy and ensure it sets forwarded headers correctly. Otherwise, the rate limiter may count the entire tunnel as a single source.
If you also use nginx, preserve the original path and Authorization header, disable buffering for MCP, and allow sufficient request time:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_read_timeout 120s;
}Connect ChatGPT
Create a custom MCP app/connector in ChatGPT with OAuth and this URL:
https://strava-mcp.example.com/mcpOAUTH_REDIRECT_URIS must contain the exact callback URI shown by ChatGPT. For flows using iss, OpenAI documents https://chatgpt.com/connector_platform_oauth_redirect; this is the default in .env.example. If the interface shows a different callback, add that exact URI and restart the application. Wildcards, prefix matching, and arbitrary redirect URIs are not allowed. The Strava Client ID and secret stay on the server: ChatGPT registers its own public OAuth client through /oauth/register, without a client secret. See OpenAI: OAuth for MCP.
Keep the two OAuth callbacks separate:
# ChatGPT receives our authorization code at its own callback URL.
OAUTH_REDIRECT_URIS=https://chatgpt.com/connector_platform_oauth_redirect
# Our server receives Strava's authorization code here.
STRAVA_REDIRECT_URI=https://strava-mcp.example.com/oauth/strava/callbackDo not replace the ChatGPT hostname in OAUTH_REDIRECT_URIS with your MCP hostname. After changing .env, apply it with docker compose up -d --no-deps app; docker compose restart does not reload environment variables.
The browser completes Strava OAuth, then displays a local consent page for the specific MCP client. After consent, ChatGPT receives our single-use authorization code and exchanges it with the PKCE verifier for our token. Strava tokens are never returned to the client.
Discovery endpoints:
https://strava-mcp.example.com/.well-known/oauth-authorization-server
https://strava-mcp.example.com/.well-known/oauth-protected-resource
https://strava-mcp.example.com/.well-known/oauth-protected-resource/mcpExpected authorization server metadata:
{
"issuer": "https://strava-mcp.example.com",
"authorization_endpoint": "https://strava-mcp.example.com/oauth/authorize",
"token_endpoint": "https://strava-mcp.example.com/oauth/token",
"registration_endpoint": "https://strava-mcp.example.com/oauth/register",
"revocation_endpoint": "https://strava-mcp.example.com/oauth/revoke",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"token_endpoint_auth_methods_supported": ["none"],
"revocation_endpoint_auth_methods_supported": ["none"],
"code_challenge_methods_supported": ["S256"],
"scopes_supported": ["strava:read"],
"authorization_response_iss_parameter_supported": true
}Protected Resource Metadata specifies resource=https://strava-mcp.example.com/mcp, authorization_servers=["https://strava-mcp.example.com"], and the strava:read scope. Requests to /mcp without a token receive 401 with a WWW-Authenticate header pointing to the resource metadata. Tokens are restricted to this resource and validated on every MCP request. The contract follows MCP Authorization.
Tools
Tool | Purpose and arguments |
| Compact profile of the connected athlete |
|
|
| Activity details by |
| Available Strava running / cycling / swimming aggregates |
|
|
|
|
|
|
|
|
| Available bikes / shoes and distance covered |
All distances are in meters, durations in seconds, and speeds in meters per second. Analytics dates in YYYY-MM-DD format include both boundaries in UTC; weeks start on Monday UTC. start_date_local is preserved for display but does not determine the aggregation timezone. Requests are limited to 366 days and 2,000 activities: narrow the period when the limit is reached to avoid silently incomplete summaries.
get_athlete_stats returns Strava's built-in aggregates only for activities with Everyone visibility, according to the official endpoint description. get_training_summary, compare_training_periods, and get_weekly_training calculate metrics from the owner's accessible activities, including private activities when activity:read_all is granted; their results may therefore differ from Strava's built-in statistics.
Running pace is calculated from total running duration and distance; cycling speed from distance and duration. Average heart rate includes only activities with heart-rate data. Missing measurements are not converted to zeros; percentage changes with a zero baseline are not reported as infinity. The longest activity is determined by distance.
Streams are normalized and downsampled using shared indices: at most 100 / 300 / 1,000 points per stream for low / medium / high resolution. The stream list and response size are bounded; GPS is included only when latlng is requested. Responses do not contain the complete raw Strava JSON.
Profile, stats, gear, and activity-detail caches have bounded TTLs; activity lists use a short TTL. Caches and rate limiting are local to the process. Multiple instances would need shared rate limiting / caching and a review of the combined Strava API load. Strava token refresh is synchronized with PostgreSQL row locks.
Local development and tests
You need Node.js 22+ and PostgreSQL 17. To start a separate local database:
docker run -d --name fitness-mcp-dev-db \
-e POSTGRES_USER=fitness -e POSTGRES_PASSWORD=local-dev-only \
-e POSTGRES_DB=fitness -p 127.0.0.1:5433:5432 postgres:17-alpine
npm ciSet DATABASE_URL=postgres://fitness:local-dev-only@127.0.0.1:5433/fitness in your local .env. Then run:
node --env-file=.env --import tsx src/db/migrate.ts
npm run dev
npm run typecheck
npm test
npm run buildTest browser OAuth through a public HTTPS tunnel: authorization cookies require a secure connection. HTTP on localhost is suitable for health/readiness checks and route tests, but the public MCP endpoint must use an HTTPS origin.
PostgreSQL integration tests are enabled by TEST_DATABASE_URL. Use a dedicated test database, never the production database: tests create and remove their own isolated schemas. Strava HTTP requests are mocked in tests.
docker exec fitness-mcp-dev-db createdb -U fitness fitness_test
TEST_DATABASE_URL=postgres://fitness:local-dev-only@127.0.0.1:5433/fitness_test npm testWithout TEST_DATABASE_URL, the integration suite is skipped. CI runs tests with PostgreSQL, type checking, the TypeScript build, and a Docker build. Successful local validation does not confirm a live ChatGPT connection or Strava authorization.
Operations
The production command inside the image is node dist/index.js. The migration command is node dist/db/migrate.js (npm run migrate:prod). SQL migrations are included in the Docker image. To run migrations from a local dist directory after npm run build, copy src/db/migrations to dist/db/migrations first, or run migrations from source using the command above.
Before updating, back up PostgreSQL, then run:
docker compose build
docker compose run --rm migrate
docker compose up -d
curl --fail https://strava-mcp.example.com/readyz
curl --fail https://strava-mcp.example.com/.well-known/oauth-authorization-serverdocker compose down preserves the named volume; docker compose down -v deletes the database. Migrations are applied explicitly and sequentially; there is no automatic SQL rollback. Back up the database before schema changes.
OAuth access tokens last one hour; refresh tokens last 30 days and rotate on use. Reusing a refresh token revokes its token family. Our OAuth access/refresh tokens are stored as SHA-256 hashes; upstream Strava credentials are encrypted with AES-256-GCM. The browser flow is bound to a signed cookie and protected by CSRF/state checks; authorization codes are short-lived and single-use. Reconnect after revoking access in Strava.
Logs include request IDs, tools, endpoints, status, latency, and cache / refresh events. Do not enable URL query-string or request/response body logging on an external proxy: OAuth callbacks contain authorization codes. Secrets and credential headers must not enter the logging system.
Troubleshooting
code_challenge_methods_supported containing S256: check the public/.well-known/oauth-authorization-server; the field must be["S256"]. The endpoint must return the application's JSON with HTTP 200, without a Cloudflare login/challenge or stale cached metadata. Recreate the connection after changing the domain.invalid_redirect_uri/invalid_client_metadata: compare ChatGPT's callback URI character-for-character withOAUTH_REDIRECT_URIS. ChatGPT's callback and/oauth/strava/callbackare separate addresses with different purposes.invalid_grant: the code has expired, has already been used, or the PKCE verifier is incorrect; restart OAuth. During refresh, this error can also indicate token rotation or family revocation.State / cookie error: restart the flow on the same HTTPS hostname; check
PUBLIC_BASE_URL, cookies, and any proxy redirects to a different domain.Athlete access denied: check your athlete ID in
STRAVA_ALLOWED_ATHLETE_IDSand restart the application container.STRAVA_TOKEN_REFRESH_FAILED/STRAVA_NOT_CONNECTED: reconnect the account and check the Client ID/Secret and granted scopes.STRAVA_RATE_LIMITED: retry after the indicated delay or narrow the period. The client respects upstream rate-limit headers, bounded retries, timeouts, andRetry-After.ACTIVITY_NOT_FOUND: check the activity ID, its owner, and theactivity:read_allscope.Readiness check fails: check that
dbis running,migratecompleted successfully, and the password inDATABASE_URLmatches the actual PostgreSQL user's password.
Before production use, complete these manual steps: create a Strava application, fill in the secrets and athlete allowlist, configure DNS/Tunnel, set the exact ChatGPT callback URI, and complete OAuth. The live ChatGPT → Tunnel → Strava flow must be verified with your account; repository tests do not replace that check.
This server cannot be deployed
Maintenance
Related MCP Connectors
Garmin data in Claude & ChatGPT via the Garmin Health API. OAuth sign-in, no password sharing.
Connect your Oura Ring account securely in minutes. Enable authorized access to your sleep, activi…
Strava MCP tools for AI: athletes, activities, segments, clubs, routes. Powered by HAPI MCP server.
- JotiOAuthcom.kompetic
Read your workouts, history, and stats; create and schedule new workouts. Writes are additive only.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceEnables Claude to access and analyze your Strava activities through OAuth authentication. Supports retrieving activity lists and detailed workout data for fitness tracking and analysis.2-
- AlicenseBqualityDmaintenanceEnables interaction with Strava's API to access and manage activities, athlete data, routes, segments, clubs, and gear through natural language.2210 npmMIT
- FlicenseNot gradedqualityDmaintenanceMCP server to fetch Strava activities using OAuth authentication with automatic token refresh. Allows retrieving recent activities, activities by date range, activity details, and athlete stats.-
- AlicenseBqualityBmaintenanceEnables MCP clients to interact with the Strava REST API through tools generated from Strava's official Swagger spec, supporting OAuth, activity and athlete data retrieval, and configurable write/delete operations.34MIT