Oura MCP Server
A Model Context Protocol (MCP) server for accessing Oura Ring data.
It runs in two modes from the same codebase:
| Mode | Entrypoint | Transport | Use it for |
|---|---|---|---|
| Local | npm run start:stdio |
stdio | Claude Code and Claude Desktop on your own machine |
| Hosted | npm start |
Streamable HTTP + OAuth | claude.ai in the browser and the Claude mobile apps |
Prerequisites
- Node.js 20+
- An Oura account
Installation
npm install
npm run build
Oura credentials
- Log in to the Oura Cloud Console
- Create a Personal Access Token
Set it as OURA_PERSONAL_ACCESS_TOKEN. See .env.example for the full list ofvariables. Set OURA_TIMEZONE to your IANA zone (e.g. America/Phoenix) — itdecides what "the last 7 days" means for the resource defaults. It falls back toUTC, which shifts the window by a day for part of every day anywhere west ofGreenwich, so it is worth setting explicitly.
Personal access tokens are the only supported credential. Earlier versions alsoaccepted OAuth2 client id/secret, but nothing ever ran the authorization-codeflow against Oura, so that path could only fail at request time; it has beenremoved rather than left as a trap.
Local mode (stdio)
Claude Code
claude mcp add oura -s user \
-e OURA_PERSONAL_ACCESS_TOKEN=your_token \
-- "$(command -v node)" /absolute/path/to/oura-mcp/build/index.js
Claude Desktop
Settings → Developer → Edit Config:
{
"mcpServers": {
"oura": {
"command": "/absolute/path/to/node",
"args": ["/absolute/path/to/oura-mcp/build/index.js"],
"env": { "OURA_PERSONAL_ACCESS_TOKEN": "your_token" }
}
}
}
Pass the token in env rather than relying on a .env file. dotenv resolves.env against the current working directory, which for a client-launchedserver is wherever the client happened to start — not this repo.
Testing
npm test # builds first, then runs the suite
22 tests across two suites, none of which need a live Oura token: oauth.test.tsdrives the real OAuth flow over HTTP (discovery, dynamic registration, PKCE,single-use codes, refresh, bearer rejection), and tools.test.ts checks the tooland resource surface over stdio — including a regression test that stdout carriesnothing but JSON-RPC.
For a manual probe against real data:
node test.js get_daily_sleep 2026-08-01
Hosted mode (HTTP + OAuth)
claude.ai and the mobile apps only talk to remote MCP servers over HTTPS, soreaching your data from a phone means deploying this somewhere.
What the auth actually does
The server is its own OAuth 2.1 authorization server. Your Oura token stays inserver-side env and is never handed to the client; the OAuth flow exists only toprove that whoever is calling /mcp knows MCP_AUTH_PASSWORD.
Client ids, authorization codes, and tokens are all HMAC-signed payloads ratherthan database rows, so a redeploy doesn't sign you out and no storage needsprovisioning. Clients are registered as public clients and authenticate withPKCE. Authorization codes are single-use.
Deploying to Railway
Create a new Railway project from this repo.
railway.jsonpins the buildand start commands and points the healthcheck at/healthz.Generate a signing secret:
openssl rand -hex 32Set these variables in the Railway service:
Variable Value OURA_PERSONAL_ACCESS_TOKENyour Oura token OAUTH_SIGNING_SECRETthe hex string from step 2 MCP_AUTH_PASSWORDthe password you'll type when connecting You do not need to set
PUBLIC_URLon Railway. The server falls back toRAILWAY_PUBLIC_DOMAIN, which Railway injects once the service has a domain(Settings → Networking → Public Networking → Generate Domain). SetPUBLIC_URLexplicitly only when hosting elsewhere, or to override theadvertised origin — it must then match the real origin exactly, since it'swhat the server publishes in its OAuth metadata.Confirm the deploy:
curl https://your-app.up.railway.app/healthz
PORT is injected by Railway; don't set it yourself.
Connecting Claude
In claude.ai → Settings → Connectors → Add custom connector, use:
https://your-app.up.railway.app/mcp
Leave the OAuth client fields blank — the server supports dynamic clientregistration, so Claude registers itself. You'll be redirected to a sign-in pageasking for MCP_AUTH_PASSWORD, and after that the connector is available in thebrowser and on the mobile apps under the same account.
Endpoints
| Path | Purpose |
|---|---|
POST /mcp |
The MCP endpoint. Requires a bearer token and the oura:read scope. |
/authorize, /token, /register, /revoke |
OAuth, mounted by the MCP SDK |
POST /login |
Password form posted from the authorize page |
/.well-known/oauth-authorization-server |
AS metadata |
/.well-known/oauth-protected-resource/mcp |
Protected-resource metadata |
GET /healthz |
Healthcheck |
GET and DELETE on /mcp return 405: the server runs the transport instateless mode, so there's no long-lived SSE stream or session to tear down.Every request gets a fresh server instance, which is what lets a redeploy or asecond replica pick up mid-conversation.
Available resources
personal_info, daily_activity, daily_readiness, daily_sleep, sleep,sleep_time, workout, session, daily_spo2, rest_mode_period,ring_configuration, daily_stress, daily_resilience,daily_cardiovascular_age, vO2_max
Date-based resources default to the last 7 days, bounded by OURA_TIMEZONE.
Response shape
Results are paginated by Oura via next_token; the server follows it tocompletion, so a wide date range returns every record rather than the first page.It stops after 25 pages and marks the response truncated rather than looping.
Interval-sample fields — the per-30-second and per-5-minute arrays on sleep(heart_rate, hrv, movement_30_sec, sleep_phase_5_min) anddaily_activity (class_5_min, met) — are stripped by default, since a monthof them runs to megabytes and crowds out the conversation. PassincludeIntervalSamples: true on a narrow range when you need them.
Available tools
Every date-based resource above has a matching get_<name> tool takingstartDate and endDate in YYYY-MM-DD form — 13 in total.