Words

mcp • strava • ai • railway • hono • nextjs

Stride MCP: when your MLP hits the athlete cap before anyone runs

10 May 2026·8 min read

Stride MCP is a sequel I did not want to write until I had to.

I wrote about the first version of Stride as an AI running coach that stitched together Strava, Whoop, and Google Calendar into something that felt, on a good day, like a coach who actually read your diary. Sharing it felt electric. Roughly fifty people signed up in the space of about a day, and LinkedIn filled up with pings that said the same thing in different words: Strava would not connect. Not because my code forgot a redirect URI or because PostgreSQL shrugged at me during dinner. Because new Strava API applications arrive with an athlete quota that is aggressively small, and fifty curious humans slammed into that ceiling like a polite brick wall.

The product story flipped in an hour from "maybe this has legs" to "the API provider is silently rate-limiting me by headcount unless I plead for expansion." Friends who normally send memes suddenly sounded like SOC 2 auditors. My friend Sean, who takes running seriously in the way engineers take uptime seriously, summed it up in one pragmatic sentence after he hit the failure: everybody needs Strava eventually, which means somebody has to lose the multiplayer game of shared keys versus shared quotas.

Stride MCP is the escape hatch. Try it at app.runstri.de.

It is intentionally less romantic than the coaching concept. Users bring their own Strava developer application. They paste their own Client ID and Secret into my dashboard (encrypted at rest). They OAuth into Strava with their app, so quota is their problem in the nicest possible way: a free developer app tied to their training data, routed through assistants they already trust to read messy human language.

That is the customer promise. The engineering promise, if I am honest, was different: I wanted to understand Model Context Protocol end to end, ship something that Claude.ai and ChatGPT would both talk to, and learn Railway by paying the small tax of real deploy pain instead of imagining it.

The problem

Two problems, really, braided together.

Product: A shared Strava application is convenient for onboarding and dreadful for unpredictability at scale. The exact cap number moves with Strava policy, but the shape of the panic does not move at all: you advertise a connector, strangers click "authorize," and Athlete Slot N plus one gets a shrug emoji from the OAuth server you do not fully control.

Personal: The first Stride validated interest. It did not validate that I understood how assistants call tools over HTTPS in 2026, or how OAuth needs to behave when Claude is acting like a peculiar browser that also wants programmatic access, or why "works on localhost" reliably lies to you about TLS, cookies, and CORS once the hostname changes.

Stride MCP swaps the bottleneck. Instead of routing everyone through Tom's quota, Tom routes everyone through Tom's plumbing and punts quota to each human's Strava dashboard.

Sometimes the morally correct architecture is petty in the spreadsheet and beautiful in reality.

What I was actually testing

The side-bet was understanding MCP end to end, shipping two production services on Railway with Prisma, and confirming that BYO keys was architecture, not a workaround.

What I built

Two services, one repo. apps/web is the Next.js dashboard where users sign in, paste their Strava credentials, and complete OAuth. apps/mcp is a Hono server that implements MCP over Streamable HTTP at POST /mcp. Shared packages handle the database (Prisma + Postgres), encryption (AES-256-GCM), and a thin Strava client.

The tools are deliberately boring: get_athlete, list_activities, get_activity. Boring tools keep rate limits quiet.

Stride MCP: runs list in ClaudeGive me my latest runs from StravaLoaded tools, used Stride integration ›List activities ›🏃Tempo (small)23 Mar 202613.09 kmDistance48m 27sTime0 mElevation16.2 km/hPace173 bpm🏃Morning Run3 Apr 202615.06 kmDistance1h 03mTime165 mElevation14.3 km/hPace163 bpm🏃Morning Run6 May 202613.39 kmDistance58m 44sTime46 mElevation13.7 km/hPace161 bpm🏃Lunch Run10 May 202610.11 kmDistance45m 07sTime80 mElevation13.5 km/hPace159 bpmWrite a message...Sonnet 4.6
Claude loading the Stride integration, calling list_activities, and returning structured run data from Strava with elevation, pace, and heart rate per activity.
Stride MCP: training insights in ClaudeAny thoughts on improving my runs?Based on your recent data, a few observations:Heart rate is the main flag. All four runs sit between 172–177 bpm, solidly Zone 4–5.80/20 rule: most runs should feel almost uncomfortably easy — around 130–145 bpm.Pace is very consistent. Hovering around 10 km/h across all distances and times of day.Try running by heart rate rather than pace for a month.The gap is the bigger issue. 6 weeks Jul–Aug, then nothing since August 2025 — about 9 months off.3 easy 5 km runs a week would rebuild the base quickly given your trail background.A few practical suggestions:Add one genuinely easy run per week — if you can't hold a conversation, slow down.Mix hilly efforts to build strength; you're already doing 96–165 m elevation.For the Queenstown trip: a 6–8 week build focused on elevation gain beats chasing speed.Want me to sketch out a simple weekly structure to get back into it?Write a message...Sonnet 4.6
Follow-up on the same tool results. Claude identifies the heart rate pattern, flags the training gap, and closes with a practical question — all without a second API call to Strava.

The engineering bits

OAuth: how the assistant finds Stride and gets access

The mental model is small:

  1. User proves identity to Stride (Google).
  2. User connects Strava with BYO credentials (Strava OAuth using their client).
  3. User adds the MCP URL in Claude or ChatGPT.
  4. Assistant registers and runs OAuth against Stride.
  5. Stride maps the successful login to a connector access token the assistant stores.
  6. Assistant calls POST /mcp with Authorization: Bearer ....
Stride MCP auth flowUser browserStrava developer appStride web appNext.js · Better Auth · Auth ServerGoogle + BYO StravaClaude or ChatGPTassistant clientRFC 7591 + OAuthBearer tokenStride MCPHono · mcp.runstri.dePOST /mcp + Bearer1 User signs in with Google, pastes BYO Strava credentials2 User adds mcp.runstri.de/mcp to Claude or ChatGPT3 Assistant registers via RFC 7591 + runs OAuth against Stride4 Stride issues a Bearer access token to the assistant5 Assistant calls POST /mcp with Bearer on every tool request
The user provisions credentials once. After that, Claude calls tools autonomously using the Bearer token it received from Stride.
RFC 9728 protected resource metadataClaude connectorclaude.aimcp.runstri.deGET /.well-known/oauth-protected-resourceMetadata JSONresource: https://mcp.runstri.de/mcpauthorization_servers[0]:app.runstri.deapp.runstri.deAuthorization Serverprobeauthorization_servers[0]resource ===exact URLresource must be the exact stringhttps://mcp.runstri.de/mcpNot the base domain. Literally this.Exact resource URL matching for protected-resource metadata so the assistant binds OAuth to the right MCP surface.
Claude probes the MCP host for OAuth metadata. The resource field in the response must exactly match the MCP endpoint URL. One character off and the assistant treats your server as unauthenticated.

BYO quota

Users paste their own Strava Client ID and Secret. Stride encrypts them, each user completes Strava OAuth under their own app, and rate limits become their problem in a good way.

BYO quota architectureShared Strava app keysingle athlete cap · everyone blocked togetherTom's Strava appquota: 50 athletesUser 1User 2User 3User 4User 5Hard stopUser 4, 5 see OAuth errorPer-user developer appsBYO quota · each user holds their own capUser 1own Strava appUser 2own Strava appUser 3own Strava appUser 4own Strava appUser 5own Strava appShared app: one cap blocks everyone. BYO: quota fans out, no user can exhaust another.
A shared Strava developer app means one quota chokes everyone simultaneously. Per-user BYO apps distribute the cap so no single user can exhaust another's allowance.

MCP request path: question to tool to Strava to answer

When a user asks "what was my longest run last month," the assistant does not phone me. It calls list_activities with arguments, my server resolves userId from the bearer token, pulls Strava tokens for that user only, calls Strava REST, returns JSON. The model turns JSON into human sentences.

MCP request pathUser questionnatural languageClaude modelselects tool + argsPOST /mcptool call JSON-RPCStrava REST APIuser's own quotaTool result JSONraw activity dataClaude modelinterprets resultNatural languageanswer to useruserId resolved from Bearer token at the MCP layer. No tool ever handles auth plumbing.
End-to-end path from chat text to Strava-backed tool output and back to the user. The MCP server resolves identity from the Bearer token before any tool runs.

Railway: two services, two Dockerfiles, two hostnames

Production is two Railway services from one repo:

  • Web image built from apps/web/Dockerfile
  • MCP image built from apps/mcp/Dockerfile

Custom domains hang off GoDaddy DNS to app.runstri.de and mcp.runstri.de.

Prisma on Alpine needed the classic fix: apk add openssl in both Dockerfiles so database clients stop acting mysterious at runtime.

Railway two-service deploymentstride-mcppnpm workspace monorepoapps/web/DockerfileNext.js · Better Authapps/mcp/DockerfileHono · MCP SDKRailway: webapp.runstri.decustom domain via GoDaddyRailway: mcpmcp.runstri.decustom domain via GoDaddyPostgreSQLPrisma + AES-256-GCMshared by both servicesapk add openssl in both Dockerfiles: the Alpine + Prisma fix that silences midnight database mystery.
Two Railway services built from one repo, each with its own Dockerfile, sharing a single PostgreSQL instance. Custom domains wired through GoDaddy DNS.

The sharp edges that wasted my evening

GET /mcp returning 405 broke reachability checks in a way that feels silly only after you fix it. Assistants and browsers do lightweight probes. If your router says "Method Not Allowed" where the product expects a tiny JSON hello, you look offline even when POST works. I added a GET /mcp JSON info response that advertises streamable-http and points humans at POST semantics.

CORS mattered because Claude.ai issues browser-side requests to the MCP host. A server-only mental model will lie to you. Open CORS for the MCP surface with explicit allowed methods and headers (Authorization, session headers the MCP stack uses) saved me from chasing ghosts in server logs that only the browser could see.

Spec gaps versus client behavior showed up as whack-a-mole: one client tolerates sloppy metadata ordering, another does not; one probes GET, another assumes SSE; documentation updates faster than intuition. Treat the golden path as empirical: reproduce with the exact product, sniff headers, compare WWW-Authenticate reality to the cheerful diagram in your notebook.

CORS and browser-side MCP callsClaude browser tabclaude.aimcp.runstri.deMCP serverapp.runstri.deOAuth / ASOPTIONS /mcp (preflight)200 OK · CORS headersPOST /mcp (JSON-RPC + Bearer)tool result streamOAuth redirects (human consent)authorization_codeClaude.ai issues browser-side requests.Server-only CORS models will fail here.Browser-side MCP calls crossing origins: why CORS is not pretend work.
Claude.ai issues MCP requests directly from the browser, not a server. The MCP host needs explicit CORS headers including Authorization and MCP session headers, or the preflight kills the connection before any tool runs.

Three patterns worth a closer look

1. Treat MCP and OAuth like a miniature API gateway

Your MCP surface is just another HTTPS API with sharper typing. Middleware that resolves identity before tools run is non-negotiable. I validate the Bearer token against stored connector token rows, derive userId, and only then register tools for that session. Anything that reaches a tool already has a clean identity. Tools themselves stay ignorant of auth plumbing.

2. Metadata-first debugging

When a connector silently fails, work backward from discovery documents rather than forward from vibes. Protected resource metadata, authorization server URLs, matching resource strings, consent screens: if any link in that chain rattles, the assistant never reaches Strava regardless of tool quality. Add a curl recipe for every discovery endpoint to your debug playbook before you need it at eleven at night.

3. BYO quota as sociology

Technical people reach for abstraction when policy reaches for quotas. Giving every user their own Strava developer app trades onboarding friction for survivability at small scale without waiting on approvals from a distant API program. Fifty signups hitting one cap is misery. Fifty signups each holding their own key is bureaucracy distributed humanely across humans who already tolerate Strava quirks. It also means my app never has a conversation with Strava about "intended use at scale." Their program office stays out of my deploy pipeline.

What's next

Stride MCP proves the handshake: assistants, authenticated remote tools, real Strava payloads, users who actually converse with their mileage.

The next lifts are predictable and fun: narrower tools with cheaper Strava API answers, sharper guardrails around rate limiting, richer prompts in the connector docs so newcomers land their Strava callbacks on the first try, maybe a quieter onboarding path while keeping BYO doctrine intact.

Try it if any of this sounds like your rabbit hole:

  1. Spin a free Strava API app under your own account (clipboard steps and callback domain vigilance matter).
  2. Sign in at app.runstri.de and paste your Strava credentials.
  3. Complete Strava OAuth in the dashboard.
  4. Drop https://mcp.runstri.de/mcp into Claude or ChatGPT connectors.

If you were one of those fifty enthusiastic humans from the launch hour: thank you for the messages. Stride MCP exists because strangers cared enough to report a wall honestly. Plug in your own developer app once, curse the bureaucratic clipboard dance once, run your questions afterward as many times as you like.

Tell me how it behaves on your strangest long run weeks. Sean already has opinions about mine.