# oura-mcp

> The Oura v2 API as an MCP server. All 19 collections, six tools, no
> dependencies beyond the MCP SDK.

It exists because Oura under-delivers without saying so, in four distinct ways,
and this server corrects all four. Oura never returns an error when it can't
give you what you asked for: it returns something different, shaped like a
correct response.

  1. Not following `next_token` returns a fraction. One local day of heartrate
     is 1,231 samples across 2 pages; a client that doesn't paginate gets 81%.
  2. `end_date` is inconsistent ACROSS collections —daily_activity, sleep and
     workout exclude the last day; the others don't— and workout filters by UTC
     date while reporting `day` in local time.
  3. `latest=true` on a collection that doesn't support it returns the entire
     collection, with no error.
  4. `fields=made_up` returns the complete record, without projecting.

## Tools

- `oura_collections` — all 19, what each carries and which parameters it takes.
- `oura_query` — one collection in full over a range, paginating to the end.
  The range is INCLUSIVE on both ends.
- `oura_today` — last night's sleep and today's readiness, with the days before
  them, in one call. Computes nothing: the records come back raw.
- `oura_compare` — whether two periods of one metric (`collection.field`)
  differ by more than that metric's own noise, measured from the person's
  preceding 120 days. Returns `verdict` (`outside_noise`, `within_noise`,
  `cannot_tell`) with `noise_band`. `within_noise` is NOT «no change»: a real
  change smaller than the band would be missed more often than seen. Report both.
- `oura_relate` — whether two metrics' day-to-day changes (`x`, `y`, both
  `collection.field`) move together beyond chance, at one `lag` in days (0-7),
  after removing weekday levels. Returns `correlation`, `interval` and the same
  verdicts, plus `first_pair` showing the alignment. Oura files sleep and
  readiness under the morning you woke up: activity on day D meets that night's
  sleep at lag=1. Needs about three months. Co-movement, NEVER cause.
- `oura_check` — self-check: auth mode, granted scopes, expiry. Returns
  neither the token nor any health value.

All six are read-only. There is no POST, PUT or DELETE in the package.
Parameter names match Oura's own API: `collection`, `day`, `start`, `end`,
`fields`, `latest`, `format`.

## Resource

- `oura://collections` — the same catalog as `oura_collections`, attachable
  instead of called. Static: no network, no credentials, no health data.

## Warning keys in the response

- `truncated` + `continue_from` — data is missing; `continue_from` is the last
  day reached, so you ask again with `start` set to the day after. There is no
  cursor parameter on purpose: a cursor makes pagination the model's job.
- `pagination_cycle` — Oura repeated a token; what's there may be incomplete.
- `ignored_fields` — you asked for fields Oura didn't apply.
- `discarded_out_of_range` — records from the ±2-day safety margin that were
  cut. NORMAL, and it appears on almost every dated query: nothing you asked
  for is missing.
- `fields_split` — `fields` arrived as a comma-separated string and was split.
- `uneven_columns` — in CSV, not every record has the same keys.
- `rate_limited` — Oura refused with 429 and a retry recovered. The data is
  complete; the warning is about the NEXT query, which may fail.
- `synthetic` — sample data, not this person's. Never report it as theirs.
- `cached` — answered from this session's memory. Only for a range that
  closed BEFORE today, which cannot gain records; today is never held, and
  neither is an empty answer. The numbers are identical to what Oura
  returned earlier in the same session, not a stale approximation.
- `empty` — the query came back empty; explains what's knowable about why.
- `large_response` — the payload is heavy; names the field that dominates it.

## Environment variables

- `OURA_SANDBOX=1` — Oura's official synthetic data, no credentials needed.
- `OURA_CLIENT_ID`, `OURA_CLIENT_SECRET` — for `oura-mcp --authorize` (OAuth2).
- `OURA_PAT`, `OURA_PAT_FILE` — personal token, if you already had one. Oura
  stopped issuing them in December 2025.
- `OURA_CREDENTIALS` — where the 0600 OAuth file lives.
- `OURA_API_BASE_URL` — point at another origin (testing).
- `OURA_TIMEZONE` — IANA zone that decides which day is «today». Defaults to
  the machine's; required on the remote Worker, whose clock is UTC.

## Remote

claude.ai and ChatGPT connect by URL. `ts/worker` is the same server as a
Cloudflare Worker each person deploys on their own account, for their own Oura
account only, behind OAuth and a consent page. No shared instance exists.

## What it does NOT do

It doesn't analyze beyond `oura_compare` and `oura_relate`: no trends, no
anomalies, no advice. An average computed inside reaches the model as a number
without its method; those two are the only places a number leaves with its
method — the band or interval — attached. The
data is delivered raw and the analysis belongs where the method can be cited.

## Links

- Repository: https://github.com/proscar87/oura-mcp
- PyPI: https://pypi.org/project/mcp-oura/
- MCP registry: io.github.proscar87/oura-mcp
