Skip to content

v0.9.0 — OAuth2 migration + three silent API bugs fixed

Choose a tag to compare

@schimmmi schimmmi released this 29 Aug 11:36
· 6 commits to main since this release

⚠️ Breaking: Personal Access Tokens are gone — this release moves to OAuth2

Oura deprecated Personal Access Tokens in August 2026. Existing ones keep working
for a while, new ones cannot be created, and they will be switched off. If you
run this server, you need to migrate.

What you have to do

  1. Register an application at https://developer.ouraring.com/applications
    (redirect URI http://localhost:8080/callback)
  2. Put OURA_CLIENT_ID and OURA_CLIENT_SECRET in .env
  3. Run python generate_tokens.py once

The README walks through it field by field,
including the Privacy Policy and Terms of Service URLs the registration form
demands.

How refresh is handled

Oura refresh tokens are single-use — every refresh invalidates the previous
one. Two things guard your access:

  • Refreshes are serialized with a file lock. Several server processes can run
    at once (one per client session, plus cron jobs); without it two would spend the
    same single-use token and one would be left with a dead credential. Inside the
    lock the stored tokens are re-read first, so a rotation another process just
    completed is adopted rather than duplicated.
  • The new pair is written atomically (temp file + os.replace, mode 600), so a
    crash mid-write cannot truncate the only copy of your refresh token.

A 401 drives one refresh and one retry. A second 401 means the credentials are
dead rather than stale, and the error tells you to re-run generate_tokens.py.


🐛 Three silent bugs fixed

Found by diffing the client against the official OpenAPI spec (v2.0 / 1.37).
None of them ever raised an error — they returned wrong data, no data, or a
placeholder, which is why none had been noticed.

Fix Before After
get_workout_sessions queried /usercollection/session empty list, always — that endpoint is Oura's guided breathing / meditation collection, not sport 9 workouts in a 9-day window
vo2_maxvO2_max HTTP 404 — the spec spells it with a capital O HTTP 200
workout renderer read the field type Type: Unknown on every entry Activity: walking · Intensity: moderate · Duration: 12m · Calories: 41 kcal

The first one has a tidy root cause: the docstring on get_sessions said
"Get workout/activity sessions."a wrong description produced a wrong call.
It now says what the endpoint actually returns.

The renderer had also been written against the session schema, so it looked for
total_duration and heart_rate, neither of which exists in PublicWorkout.
Duration is now computed from the timestamps, and calories are rounded to whole
kcal instead of 15 decimal places of float noise.

Also added: get_ring_battery_level, which is in the spec and returns data but
had no client method.


🔒 Protocol-safe logging — thanks to @gjsduarte

Logging now defaults to stderr, and stdout logging is redirected to stderr
whenever the stdio transport is in use, so a log line can no longer corrupt the
JSON-RPC stream. Includes regression tests. From #2 — thank you.

The tracked machine-specific config/claude_desktop_config.json is gone with it.


Upgrading

git pull
pip install -r requirements.txt
python generate_tokens.py     # one-time authorization

If you were using a Personal Access Token, revoke it afterwards — it is not needed
any more and will stop working regardless.