My WHOOP scores my recovery every morning. It has no idea what my day looked like.
whoop-relay: a remote MCP server on the 2026-07-28 spec that connects WHOOP to Claude, answers from its own copy of the data, and renders a live dashboard in the chat.
“Most of what pushes my body around never touches the strap.”
My WHOOP tracker reads signals from my body all day: heart rate, HRV, sleep stages, movement. Combined with the journal entries I log in its app, it gives me a daily report on strain, stress, recovery, and sleep. Those reports are good. They’re also incomplete, because most of what pushes my body around never touches the strap.
The temperature and humidity in the room. How hard my working day was. The number of meetings, the type of meetings, sometimes the conversation inside the meeting itself. All of that already exists as timestamped data. None of it lives in a fitness platform.
My hypothesis: overlay those external factors onto the WHOOP timeline, and the reports become much more useful because they show not just what my body did but what it was reacting to. That’s a hypothesis, not a result. This project is the plumbing that makes testing it possible.
To be precise about what’s connected today: the work side is live, because my calendar, meetings, messages, and issues each already have their own MCP. The environment side is one weather call per day away: temperature, humidity, cloud cover, and daylight hours, with my location fixed to Berlin. And one boundary for the meeting-content part: transcripts contain other people’s words, so any correlation happens privately in my own Claude context. Nothing gets republished.
Three constraints, one server
Before writing code, I set three constraints, and every architecture decision in this post traces back to one of them:
- My health data goes to no new vendor. It lives in exactly two places: WHOOP, and a database I own in my own Cloudflare account, encrypted at rest with AES-256 and TLS in transit (Cloudflare D1 data security, 2026). No aggregator app, no analytics product, no share links.
- Work with WHOOP’s API as it is. Webhooks for freshness, and hard respect for the rate limits instead of fighting them.
- No servers, near-zero cost. Cloudflare’s serverless platform, which at my scale fits inside the free tier (Cloudflare Workers pricing, 2026).
The result is whoop-relay, a remote MCP server on the newest spec that connects WHOOP to Claude and renders a live dashboard inside the chat. You can use it today: add https://whoop.promptmetrics.dev/mcp as a custom connector in Claude (this needs a paid Claude plan) and log in with your WHOOP account. One honest limit: WHOOP caps a new app at 10 users while it reviews it, and as I write this, 8 spots are left. If you miss a spot, you don’t need me. Clone the repo, create a free app in the WHOOP developer dashboard, and deploy to your own free Cloudflare account. The README has every step. The code is public at github.com/promptmetrics/whoop-relay. Read it alongside this post.
Key takeaways
- WHOOP allows an app 100 API calls per minute and 10,000 per day, so the server answers every question from its own copy of the data, not from the API
- The 2026-07-28 MCP spec removed sessions entirely, which is what lets one stateless endpoint serve Claude Desktop, Code, Cowork, and mobile (MCP blog, 2026)
- MCP Apps (SEP-1865, Final status) is how the dashboard renders inside the conversation as a sandboxed single-file HTML app
Why Not Just Call the WHOOP API Directly?
WHOOP gives an application 100 API calls per minute and 10,000 per day, shared across every user of the app. That budget dies fast if an AI assistant calls the API for every question. One curious user asking Claude about their sleep ten times burns ten calls. Ten users doing that during a backfill week ends the day early.
So the first decision was the biggest one: the assistant never touches the WHOOP API to answer a question. The server keeps its own read model, and Claude reads that.
Here is the whole system in one picture.
This is the pattern I’d hand any team putting an agent on top of a rate-limited SaaS API, and most SaaS APIs are rate-limited. Do not point the agent at the vendor’s API. Put a buffered copy between them. Questions become free and instant. Quota gets spent only when data actually changes. The same shape applies to a CRM, a support desk, or an analytics tool.
What Does the Ingest Pipeline Look Like?
Webhooks are push notifications for servers. The moment something changes in my WHOOP account, WHOOP sends the relay a short message: a sleep ended, a recovery was recomputed.
What happens to that message, step by step. First the server checks it really came from WHOOP, because anyone can send a request to a public URL. It drops duplicates, because webhook deliveries can repeat. The event then waits in a queue instead of being handled on the spot. A consumer works through that queue at its own pace, fetches each full record from WHOOP once, and writes it into the database. If a fetch fails, it retries by itself. If an event is broken, it moves to a separate holding queue so it can’t block the healthy ones behind it.
You don’t have to trust the diagram. Open the WHOOP app on your phone and edit a past sleep’s end time by one minute. WHOOP recomputes the recovery, sends the notifications, and both the updated sleep and the new recovery land in D1 on their own. No manual API call, no refresh button.
Why a queue at all for one wearable? Because notifications arrive in bursts and the rate budget is global. Without a buffer, a burst of events competes with live user requests for the same 100 calls per minute. The queue turns a burst into a drip.
One free-tier detail worth knowing: on Cloudflare’s free plan, queued messages are retained for 24 hours (Cloudflare Queues pricing, 2026). A consumer outage longer than that would lose events. The six-hour check described below is what makes that acceptable: anything the queue drops gets rebuilt from the API on the next pass.
Now the honest part. Push notifications have two failure modes, and WHOOP’s webhooks have both.
The first is permanent: some changes are never announced. WHOOP sends no webhook for strain or daily cycles. If the server only waited for notifications, those tables would go stale forever.
The second is occasional: a notification that never arrives. A workout logged after the fact, an event lost somewhere along the way. Like the notifications your phone drops while it’s off, they’re just gone, and nothing tells you.
So every six hours, a scheduled job stops waiting and checks for itself. It asks WHOOP directly what changed, compares that against what’s in D1, and fills the gaps. It draws from the bulk lane of the rate budget, the quiet one, so the check never delays a question someone is asking right now.
The rule underneath this: assume your notifications drop things, and build the double-check on day one. Every event pipeline I’ve worked on eventually taught me this lesson. This time I built the check before the lesson arrived.
Why Cloudflare Workers Instead of a Server?
The entire system is serverless: Workers for compute, D1 (SQLite at the edge) for storage, Queues for buffering, and a cron trigger for reconciliation. Nothing of mine is running when nothing is happening.
The reason isn’t hype. It’s abandonment risk. A personal data pipeline that needs patching, monitoring, and restarts is a pipeline that dies the first month I’m busy. This one has no machine to maintain and costs close to nothing at my scale. One deploy command ships the ingest handler, the MCP endpoint, the dashboard, and the cron together, because they’re one Worker.
There’s a business translation here for anyone running an operations stack. The question isn’t “can we host this?” It’s “who maintains it in month six?” Serverless moves that answer from “someone on the team” to “nobody”, and for glue infrastructure that’s the right answer.
How Do Durable Objects Handle Tokens and Rate Budgets?
Two jobs in this system need memory and coordination, and each gets a single-purpose Durable Object: one for OAuth tokens, one for the rate budget. Everything else stays stateless. Durable Objects are Cloudflare’s answer to “I need exactly one instance of this thing, globally”, and both jobs are exactly that shape.
The TokenManager is the only code allowed to touch WHOOP’s token endpoint. One instance exists per user, so two concurrent requests can never race to refresh the same token and invalidate each other. More importantly: refresh tokens never pass through the stateless request handlers at all. A bug or compromise in the query path never had the keys.
The RateBudget object is one global token bucket for the whole app, because WHOOP’s limits are per app, not per user. Every single WHOOP call, from the webhook consumer, the cron, or a user’s manual sync, draws from the same counter. Bulk work like the six-month backfill uses a slow lane. A live sync_now request uses an interactive lane. The result: a new user’s backfill can never starve a question someone else is asking right now.
Without Durable Objects, this needs Redis or an external locking service, which means a vendor, a connection pool, and a bill. Here it’s two small classes in the same codebase.
What Changed With the 2026-07-28 MCP Spec, and Why Does It Matter?
The 2026-07-28 revision of the Model Context Protocol removed sessions entirely: no initialize handshake, no session header, every request self-contained with its protocol version and client identity (MCP blog, “The 2026-07-28 Specification”, 2026). For a Workers deployment, this is the difference between fighting the platform and matching it. A stateless protocol on a stateless runtime means any request can land on any isolate with zero shared state.
Three parts of the spec earned their place in this build:
Statelessness is why one /mcp endpoint serves Claude Desktop, Claude Code, Cowork, and mobile identically. There’s no session to keep alive and nothing to break when a client reconnects.
Cache metadata (ttlMs and cacheScope on list responses) lets the server tell clients “this tool list is valid for an hour, and it’s the same for everyone”. Clients stop re-asking, and the tool list gets cached publicly. The trade-off is real: after a deploy, clients can take up to an hour to see new tools unless they reconnect. I accepted that trade knowingly.
Multi-round-trip requests replace the old held-open streams. When someone calls sync_now and the rate budget is empty, the tool returns input_required and asks the user whether to wait or use cached data. A tool that can say “not now, and here’s why” beats a tool that times out. In 2026, with the legacy HTTP+SSE transport deprecated on a 12-month offramp (MCP blog, 2026), this is also simply where the ecosystem is going.
How Does a Dashboard Render Inside Claude?
MCP Apps, standardized as SEP-1865 and now at Final status, lets an MCP server predeclare an HTML resource under the ui:// scheme that hosts render in a sandboxed iframe (Model Context Protocol, SEP-1865, 2025). whoop-relay uses it for the dashboard: recovery bands, HRV trends, sleep stages, strain against next-day recovery.
The flow matters because of one design choice: the HTML template contains no user data.
Separating template from data buys three things. The template caches publicly, so it loads fast for everyone. The data arrives per call, scoped to the authenticated user. And interactions inside the frame are ordinary MCP tool calls, so changing the date range in the UI goes through the same audited path as typing a question. Terminal clients that can’t render an iframe get the structured data and a live URL instead. Same tool, and each surface gets what it can use.
How Do You Handle Auth When the Data Is Someone’s Health?
WHOOP itself is the identity provider, through workers-oauth-provider with PKCE and RFC 9207 issuer identification. One consent screen does two jobs at once: it authenticates the MCP client and links the WHOOP account, then triggers a six-month backfill. That single-screen onboarding was a deliberate goal. Every extra auth step in a personal tool is a place users quit.
Two rules are absolute in this codebase. Identity never derives from a URL, only from a verified bearer token or a signed cookie. And there are no share links anywhere, because a share link to health data is a leak with a friendly name. If a route serves health data, it authenticates. No exceptions for demos, screenshots, or convenience.
What Can the Overlay Actually Correlate?
Three data layers land on one timeline, and the honest granularity is the day. The WHOOP API exposes daily cycles, recovery, sleep, and workouts. It does not expose intraday heart rate, minute-level HRV, or the app’s stress monitor (WHOOP API docs, 2026). That limit sets the shape of everything the overlay can claim.
The three layers:
- Body layer from whoop-relay: recovery score, HRV, resting heart rate, sleep stages, day strain.
- Environment layer from one weather call per day: temperature, humidity, cloud cover percentage, and daylight hours. Daylight is pure math from date and location; cloud cover comes with the same API call. This is a proxy for light available, not light that reached my eyes, and I treat it as one.
- Work layer from the MCPs I already run: meeting count and type from the calendar, per-call sentiment from meeting transcripts, message volume, issue churn.
The correlation runs day against night: work and environment of day N against sleep of night N and recovery of morning N+1. That’s where WHOOP itself says the damage shows up.
What I explicitly cannot do, and want on the record: measure stress during a specific meeting. The question “what did that 2pm call do to my heart rate?” has no answer in the current API, because no intraday series exists to query. Anyone who claims call-level stress measurement from WHOOP’s public API hasn’t read the docs. Day-level cause against night-level effect is what the data supports, so that’s what I built for.
One planned extension, clearly labeled as future work: journaling through Claude. WHOOP’s own journal entries never leave its app, since the API doesn’t expose them. But I can already speak a journal entry into Claude, or photograph a meal or supplements and have it estimate calories and macronutrients. Those numbers are estimates with wide error bars, fine for trends, useless for gram precision. When I build this, the entries will live in a table in my own D1, under the same encryption and the same no-share-links rule as everything else. Constraint one doesn’t bend for convenience features.
What Would I Do Differently?
Everything above describes a working pilot at the 10-user cap, not a scaled system. Two things I’d change already.
I’d design the reconciliation cron’s queries around WHOOP’s missing webhooks from the start instead of discovering entity by entity what never pushes. And I’d budget real time for host differences: the spec is one document, but claude.ai, Claude Desktop, and terminal clients each render MCP Apps content differently, and testing across all of them took longer than building the dashboard itself.
What I would not change: the buffered read model, the two Durable Objects, and the no-share-links rule. Those three carried the whole design.
Frequently Asked Questions
Do I need the 2026-07-28 spec to build a remote MCP server?
No, but it removes the hardest part. Earlier revisions required session state, which fights serverless platforms. The 2026-07-28 revision made every request self-contained and deprecated the HTTP+SSE transport with a 12-month offramp (MCP blog, 2026), so new servers should start there.
Why Cloudflare D1 instead of Postgres or a hosted database?
Scale and proximity. One user’s WHOOP history is thousands of rows, not millions, and D1 lives on the same platform as the Worker, so reads are local, and there’s no connection pool to manage. A hosted Postgres would add a vendor and a network hop to solve a problem this system doesn’t have.
Does the buffered-copy pattern work for write operations too?
Reads, yes; writes need more care. The relay is read-only by design, which is why the pattern is clean. For write paths against an external API, you need previews, approval gates, and an audit log on top. That’s the approach in my HubSpot MCP work, where every write produces a preview and nothing changes until a person approves it.
Can you measure stress during a specific meeting with WHOOP?
Not through the public API. WHOOP’s developer API exposes daily cycles, recovery, sleep, and workouts, but no intraday heart rate, minute-level HRV, or stress monitor data (WHOOP API docs, 2026). The supported alternative: correlate a day’s meeting load and call sentiment against that night’s sleep and the next morning’s recovery.
Can other assistants use this server, or only Claude?
Any client that speaks current MCP over streamable HTTP can connect to the endpoint. The MCP Apps dashboard renders in hosts that support SEP-1865; other clients fall back to structured data plus the live dashboard URL. That fallback is part of the tool’s contract, not an afterthought.
Conclusion
Every decision in whoop-relay traces back to the three constraints this post opened with. Owning my health data forced the auth rules and the no-share-links policy. Respecting WHOOP’s API forced the webhook pipeline and the buffered read model. Serverless at near-zero cost forced Workers, D1, and the correction loop that makes the free tier safe. The 2026 spec is what let all three fit on one edge platform. Constraints made this design; I mostly wrote them down.
If you’re building a remote MCP server this year, start stateless on the newest spec, buffer the vendor API, and give the few stateful jobs their own small components. And if you’ve built one and made different calls, I would love to hear where and why.
Get the next MCP-gap post in your inbox. Every two weeks, no fluff.
Almost done. Check your inbox.
Click the link in the email to confirm. No confirmation, no emails.
Comments
Loading comments…No comments yet. The form is right below.
Your connection or our server hiccuped. Your draft below is safe.
Your comment will appear after review.