{
  "markdown": "# oura-mcp\n\n[![PyPI](https://img.shields.io/pypi/v/mcp-oura?label=PyPI)](https://pypi.org/project/mcp-oura/)\n[![Glama score](https://glama.ai/mcp/servers/proscar87/oura-mcp/badges/score.svg)](https://glama.ai/mcp/servers/proscar87/oura-mcp)\n[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)\n\n<!-- The Glama badge is live, not a screenshot: it renders whatever that\n     independent index scores this server at today. A badge that can only go up\n     is decoration; one that can drop is evidence. -->\n\nThe [Oura](https://ouraring.com) v2 API as an [MCP](https://modelcontextprotocol.io)\nserver. All 19 collections, three tools, no dependencies beyond the MCP SDK.\n\n### One local day of heart rate is 1,231 samples across 2 pages\n\nA client that doesn't follow Oura's `next_token` returns **1,000 of them — 81%,\nlooking complete, with nothing saying otherwise.** Measured against the real API\non 9 August 2026, one person, one ring, 24 hours.\n\n**The test to apply to any Oura MCP server, including this one:** does it take\n`next_token` — or a `cursor`, or a `limit` — as a tool parameter? If it does,\npagination is the model's job, and a model that forgets to ask again produces a\nconfident answer off partial data. This server paginates to exhaustion before it\nreturns, and tells you how many pages it took.\n\nThat's one of **four** ways Oura under-delivers without saying so. All four are\nmeasured below, and all four are corrected here.\n\n### Install\n\nDownload **`oura-mcp.mcpb`** from the\n[latest release](https://github.com/proscar87/oura-mcp/releases/latest) and\ndouble-click it. Claude Desktop does the rest — no terminal, no Python, no Node.\nIt runs on Oura's official sample data out of the box, and every sample response\nsays so, so nothing can pass for your own sleep.\n\nPrefer the command line? `uvx --from mcp-oura oura-mcp`.\n\n---\n\n## The problem, measured\n\nOura does not return errors when it can't give you what you asked for. It\nreturns something different, shaped like a correct response. These are the four\nwe found by measuring against the real API on 9 August 2026:\n\n### 1. Skip the pagination and you get a fraction\n\n```json\n{ \"data\": [ ... ], \"next_token\": \"eyJ0eXAiOi...\" }\n```\n\nIf `next_token` comes back and you don't follow it, you receive the first page\nand **nothing warns you**. One local day of `heartrate` — one person, one ring,\n24 hours — is **1,231 samples across 2 pages**. A client that doesn't paginate\ngets 1,000 of 1,231: 81%, looking complete. A month is ~37,000.\n\n### 2. Asking for a single day returned zero records\n\n`end_date` **does not behave the same across collections**:\n\n| Exclude the last day requested | Include it |\n|---|---|\n| `daily_activity`, `sleep`, `workout` | `daily_sleep`, `daily_readiness`, `daily_stress`, `daily_spo2`, `daily_resilience`, `daily_cardiovascular_age`, `sleep_time` |\n\nAnd on top of that, **`workout` filters by UTC date while reporting `day` in\nlocal time**: at `-06:00`, asking for July 16–18 returned records from the 15th\nand 16th — *before* the requested start.\n\nHere the range is inclusive on both ends, always. Two extra days are requested\non each side and then trimmed, which is correct whichever way a given collection\nbehaves — and stays correct when Oura changes it.\n\n### 3. `latest=true` is ignored where it doesn't apply\n\nOnly `heartrate` and `ring_battery_level` honor it. In the other seventeen Oura\ndoesn't error: it **returns the entire collection**. You ask for the latest\nrecord, you get ten, and you believe it's one. Here it's rejected before the\nrequest goes out.\n\n### 4. A field that doesn't exist is silently ignored\n\n`fields=does_not_exist` returns the **complete** record — the projection never\nhappens — and `fields=score,does_not_exist` applies the good one and drops the\nbad one without a word. Here, fields that never appeared are reported under\n`ignored_fields`.\n\n**The pattern is always the same:** you ask for one thing, you get another, and\nnothing warns you. That's why this package would rather shout than quietly\nunder-deliver.\n\n## Installation\n\n### Try it with no credentials\n\n```bash\npip install mcp-oura\nOURA_SANDBOX=1 oura-mcp --check\n```\n\nThe sandbox is official — it's in Oura's OpenAPI spec, with 34 mirror routes —\nand serves synthetic data without authentication. 18 of the 19 collections work\nthere: `personal_info` doesn't, which makes sense, since it's the one returning\nemail, age, weight and height.\n\nThis is the right order: first you watch the server work and learn the shape of\nthe data, then you go get credentials.\n\n### With your own data\n\n**Oura stopped issuing Personal Access Tokens in December 2025.** Existing ones\nstill work; new ones can't be created. So there are two paths:\n\n**a) OAuth2 — the one that works today.** Register an application at\n[cloud.ouraring.com/oauth/applications](https://cloud.ouraring.com/oauth/applications)\nwith the redirect `http://localhost:9876/callback/` — **the trailing slash is\nrequired**, the portal rejects the other form with `invalid_redirect_uri`.\n\n> **If you registered on `developer.ouraring.com` instead**, your app belongs to\n> Oura's newer portal, whose token endpoint is a different one. The legacy\n> endpoint rejects those apps on **every** refresh — so the registration works\n> exactly once, until the first access token expires, and then fails forever\n> with nothing explaining why. This server tries the legacy endpoint and falls\n> back to the new one automatically; nothing to configure either way.\n\n\n```bash\nexport OURA_CLIENT_ID=\"…\"\nexport OURA_CLIENT_SECRET=\"…\"\noura-mcp --authorize             # opens the browser, waits for the callback\noura-mcp --authorize --manual    # headless machines: you paste the URL back\n```\n\nThe token is stored in `~/.config/oura-mcp/credenciales.json` with mode 600 — or\nin the system keychain if you happen to have `keyring` installed, which is not a\ndependency of this package — and refreshes itself. `oura-mcp --forget` erases\nit.\n\n**b) A personal token, if you already had one.**\n\n```bash\nexport OURA_PAT=\"your-token\"\noura-mcp --check\n```\n\n`--check` is the self-check: it reports which credential you're using, which\nscopes were granted and how long the access has left, **without returning the\ntoken or a single health value**. It reports the token's length, never the\ntoken. Error messages get copied and pasted into chats and issues; they have no\nbusiness carrying anything else.\n\n### Connecting it to Claude Code\n\nWith the package installed (`pip install mcp-oura`):\n\n```bash\nclaude mcp add -s user oura --env OURA_SANDBOX=1 -- oura-mcp\n```\n\nDrop `OURA_SANDBOX` once you've run `oura-mcp --authorize`.\n\n**If you use [uv](https://docs.astral.sh/uv/)**, nothing needs to be installed\npermanently:\n\n```bash\nclaude mcp add -s user oura --env OURA_SANDBOX=1 -- uvx --from mcp-oura oura-mcp\n```\n\nThe `--from` is required because the distribution is named `mcp-oura` and the\nexecutable `oura-mcp`. *(This needs `uv`; without it the command above fails\nwith \"command not found\", and `pip install` is the path to take.)*\n\nAs a Claude Code plugin:\n\n```bash\nclaude plugin marketplace add proscar87/oura-mcp\nclaude plugin install oura@oura-mcp\n```\n\n### Connecting it to Claude Desktop\n\n**One click:** download `oura-mcp.mcpb` from the\n[releases page](https://github.com/proscar87/oura-mcp/releases) and double-click\nit. Claude Desktop installs it — no terminal, no JSON, no Python. It ships with\nsample data turned on, so it works before you have any credential at all.\n\nWhen you want your own data, just ask it for something: it opens Oura's\nauthorization page through Claude, waits for the callback, and retries what you\nasked. No terminal. That works because MCP has a mode for precisely this — URL\nelicitation — and the client does the opening.\n\nThe one thing Oura still requires is that every application be registered, so you\nneed a client ID and secret from\n[cloud.ouraring.com/oauth/applications](https://cloud.ouraring.com/oauth/applications)\nonce. That's Oura's rule, not this server's. `oura-mcp --authorize` remains for\nterminal users and for clients that can't show a URL.\n\n**Or by hand,** in `~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"oura\": {\n      \"command\": \"/full/path/to/oura-mcp\",\n      \"env\": { \"OURA_SANDBOX\": \"1\" }\n    }\n  }\n}\n```\n\n`which oura-mcp` gives you the full path. Claude Desktop does not inherit your\nterminal's `PATH`, so a bare name there fails silently — one of the most common\nmistakes when configuring an MCP server.\n\n## The tools\n\n| | |\n|---|---|\n| `oura_collections` | All 19, what each one carries and which parameters it takes |\n| `oura_query` | One collection in full over a range, paginating to the end |\n| `oura_check` | Self-check that exposes nothing |\n\n**Three, not nineteen.** A server with one tool per collection forces the model\nto pick among 19 similar names before knowing what any of them contain. Here the\ncollection is a parameter and the catalog is consulted when needed.\n\nAll three declare themselves read-only, and that isn't a promise: there is no\n`POST`, `PUT` or `DELETE` anywhere in the package, and a test reads the source to\nkeep it that way.\n\n### `oura_query` parameters\n\n| | |\n|---|---|\n| `collection` | Which of the 19. `oura_collections` lists them |\n| `day` | A single day. Shorthand for `start=end=day` |\n| `start`, `end` | The range, **inclusive on both ends** |\n| `fields` | Only these fields. Oura trims on its side, so less comes down |\n| `latest` | The most recent record. `heartrate` and `ring_battery_level` only |\n| `format` | `json` or `csv`. Savings vary by collection: 55% on `heartrate`, 10% on `daily_sleep` |\n\nAnd what the response tells you when something didn't come out clean:\n`truncated` with `continue_from` naming the last day reached, `pagination_cycle` if Oura\nrepeats a token, `ignored_fields`, `discarded_out_of_range`,\n`uneven_columns`, `empty` when a query comes back empty, and\n`large_response` when what's returned is heavy enough to matter.\n\nThree more say something happened that you'd otherwise never learn:\n\n- **`synthetic`** — this is Oura's sample data, not yours. It rides on every\n  response in sample mode, which is how the extension ships, so a model can't\n  report made-up numbers as your sleep.\n- **`rate_limited`** — Oura refused with a 429 and a retry got through. **The\n  data is complete**; the warning is about the *next* query. Oura sends no\n  rate-limit headers on successful responses, so being refused is the only\n  signal there is that you're near the ceiling.\n- **`fields_split`** — `fields` arrived as `\"day,score\"` instead of\n  `[\"day\",\"score\"]` and was split. No Oura field name contains a comma, so\n  splitting is unambiguous — but reinterpreting your input silently would be the\n  same sin this whole package is about.\n\nThat last one comes from measuring: **30 days of `daily_activity` is 252,000\ncharacters**, and 87% of it is a single field, `met`, a per-minute MET series.\nAsking for three columns with `fields` brings those same 30 days down to 5,000\ncharacters — **99% less**. The server doesn't trim on its own — that would be\nunder-delivering — but it does say what's heavy and how to ask for less.\n\n*(Parameter names are in Spanish because the codebase is. They're stable, they're\ndocumented here, and the tool descriptions the model reads carry the same\ninformation.)*\n\n## What this server does NOT do\n\n**It doesn't analyze.** No correlations, no anomaly detection, no period\ncomparison — which is exactly where other servers place their value.\n\nThe reason: an average computed in here reaches the model as a number without\nits method. Across nine years of real data, **three out of four changes between\ntwo consecutive measurements fall within the metric's own normal oscillation**. A\nserver that hands over \"your HRV is up 12%\" without saying how much that metric\nswings on its own isn't informing you: it's manufacturing a signal.\n\nHere you get the data. The analysis belongs where the method can be cited — for\ninstance with [cotejo](https://github.com/proscar87/cotejo), which draws exactly\nthat distinction for blood biomarkers.\n\n## The 19 collections\n\n**Daily summaries** — `daily_sleep`, `daily_readiness`, `daily_activity`,\n`daily_stress`, `daily_spo2`, `daily_resilience`, `daily_cardiovascular_age`,\n`vO2_max`\n\n**The detail the scores hide** — `sleep` (stages, HRV, temperature, latency),\n`sleep_time`, `workout`, `session`, `rest_mode_period`, `tag`, `enhanced_tag`\n\n**High resolution** — `heartrate`, `ring_battery_level`\n\n**No range** — `personal_info`, `ring_configuration`\n\nDate-range collections use `YYYY-MM-DD`. `heartrate` and `ring_battery_level`\nuse ISO 8601 with time.\n\n## Other Oura MCP servers\n\nThere are several as of August 2026, and it's worth being precise about the\ndifferences. [`benngermin/oura-mcp`](https://github.com/benngermin/oura-mcp)\n**paginates properly**, with a resumable cursor.\n[`daveremy/oura-mcp`](https://github.com/daveremy/oura-mcp) shipped the\n`end_date` fix the same week we did.\n[`davidmosiah/oura-mcp`](https://github.com/davidmosiah/oura-mcp) has the most\ncomplete MCP surface. Pagination no longer distinguishes anyone.\n\nWhat does, as far as we could verify: **`workout`'s UTC skew isn't documented in\nany of them**, nor is rejecting `latest` where Oura ignores it, nor warning about\nfields that were never applied. And none of them treats not analyzing as a\nstated position.\n\n## Privacy Policy\n\nThis section exists because the Claude connectors directory requires one. It is\nshort because there is little to describe: the server runs on your machine and\ntalks to a single service, the Oura API.\n\n**What is collected.** Nothing, by us. The health data you request goes from the\nOura API to your MCP client and passes through no server of ours, because there\nisn't one.\n\n**What is stored, and where.** Only your credentials, and only on your machine:\n\n| | |\n|---|---|\n| OAuth2 tokens | `~/.config/oura-mcp/credenciales.json`, mode `600` — or the system keychain if you have `keyring` |\n| Personal token | Wherever you put it: `OURA_PAT`, or the file `OURA_PAT_FILE` points to |\n\nNo health data is written to disk. There is no cache.\n\n**Who it is shared with.** No one. The only outbound connection is to\n`api.ouraring.com`, with your token, to fetch what you asked for. Oura's use of\nyour data is governed by [their privacy\npolicy](https://ouraring.com/privacy-policy), not by this one.\n\n**How long it is retained.** Credentials, until you delete them:\n`oura-mcp --forget`, or by removing the file. Health data isn't retained at all\n— it lives in the response and that's it.\n\n**Diagnostics expose nothing.** `oura_check` reports the token's length, never\nthe token; the profile's field names, never their values. The token is wrapped\nin a type that won't print even in a stack trace.\n\n**Contact.** [Repository issues](https://github.com/proscar87/oura-mcp/issues).\n\n## A note on language\n\nThe code, its comments and the internal documents (`AGENTS.md`, `ROADMAP.md`,\n`CHANGELOG.md`) are in Spanish, and so are the tool parameters. This README and\n`llms.txt` are in English because they're what a stranger — or a directory\nreviewer — reads first.\n\n## License\n\nMIT.\n\n---\n\n<!-- The MCP registry requires this line in the README of the package published\n     to PyPI: it's how it verifies that whoever publishes the server also\n     controls the package. Without it, `mcp-publisher publish` returns a 400. -->\nmcp-name: io.github.proscar87/oura-mcp\n",
  "bytes": 15536,
  "sha": "4ce651fe531382379a555c0ba4871bed5823091d9c2c60ae0877ddec0fbd53ab",
  "repo_slug": "proscar87/oura-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_proscar87_oura_mcp_b21a8bca/readme"
}