Oura Ring MCP Server

Oura Ring MCP Server

Enables Claude to access and query personal Oura Ring health data, including activity, readiness, sleep, workouts, heart rate, stress, SpO2, sessions, and tags, via a self-hosted MCP server.

Category
Visit Server

README

Oura Ring MCP Server

Tests

A self-hosted MCP server that exposes your Oura Ring data — daily activity, readiness, sleep, workouts, heart rate, stress, SpO2, sessions, and tags — to Claude (claude.ai connectors or Claude Code) over HTTP.

Change type: Feature. This is a fork of camji55/oura-mcp at commit c8db34f (2026-08-12), MIT © 2026 Cameron Ingham (see LICENSE). Upstream's 494-line server exists only as a Python string embedded in docker-compose.yml (configs.oura_server_py.content) — nothing can import it, so it had zero tests and no CI. This fork extracts it into an importable package with a pytest suite, without changing behavior at the MCP tool boundary.

What changed vs. upstream

  • Package extraction. The inline server is now src/oura_mcp/config.py, client.py, auth.py, tools.py, server.py. All 11 tools keep identical names, signatures, field names, units, and docstrings. docker-compose.yml mounts ./src and a requirements.lock instead of embedding the server as a Compose config.
  • Test harness. 42 tests: unit tests for pagination, field-mapping fidelity, and the auth accept/reject matrix, plus a 13-scenario integration suite at the real MCP protocol boundary (real JSON-RPC over HTTP through the real ASGI app, including the auth middleware). See Testing.
  • What this fork did not do: the four deployment security gaps this fork set out to check (loopback-only bind, constant-time token compare, fail-closed on missing MCP_AUTH_TOKEN, pinned dependency lockfile) were already fixed upstream by commit c8db34f, two commits after the SHA originally targeted for this fork. This change verifies and test-locks that inherited hardening — see Security posture — it did not implement it from scratch.

Tools

Tool Description
get_daily_activity Steps, calories, MET minutes by intensity, sedentary/resting time, activity score
get_daily_readiness Readiness score, temperature deviation from baseline, contributor scores (HRV balance, resting HR, etc.)
get_daily_sleep Daily sleep scores and contributors
get_sleep_periods Detailed sleep periods: bedtimes, stage durations, efficiency, avg HR/HRV, lowest HR
get_workouts Logged workouts with type, intensity, calories, and start/end times
get_activity_summary Compact multi-day summary with per-day rows and period averages
get_heart_rate Per-day HR summaries from the intraday timeseries: min/avg/max bpm and averages by source
get_daily_stress Time in high-stress and high-recovery zones, plus Oura's day classification
get_daily_spo2 Nightly average SpO2 and breathing disturbance index
get_sessions Meditation, breathing, nap, and relaxation sessions with type, mood, and times
get_tags User-entered tags and notes (illness, travel, alcohol, custom)

Date-range tools default to the last 7 days when called without arguments. Fields are raw Oura values with explicit unit suffixes and full ISO 8601 timestamps — never human-formatted durations or times.

Requirements

  • Docker with Compose v2 (to run the server)
  • uv (to run tests / develop locally)
  • An Oura account with a personal access token

Quick start

  1. Clone the repo and create your .env:

    cp .env.example .env
    
  2. Edit .env:

    • OURA_ACCESS_TOKEN — your personal access token from cloud.ouraring.com/personal-access-tokens

    • MCP_AUTH_TOKEN — a long random secret that gates access to the server. Generate one:

      openssl rand -hex 32
      
  3. Start it:

    docker compose up -d
    

    First start takes ~30s while pip installs dependencies from requirements.lock (cached in a volume afterwards). The server listens on 127.0.0.1:8000; override with OURA_MCP_PORT / OURA_MCP_BIND in .env.

  4. Check health:

    curl http://localhost:8000/health
    

    {"status": "ok"} means the server is up. To also verify your Oura token works, authenticate the same endpoint:

    source .env && curl -H "Authorization: Bearer $MCP_AUTH_TOKEN" http://localhost:8000/health
    

    {"status": "ok", "oura_api": true} means the server can reach the Oura API with your token.

Connecting Claude

claude.ai (custom connector): add a connector with the URL

https://<your-host>/mcp/<MCP_AUTH_TOKEN>

Claude Code:

claude mcp add --transport http oura https://<your-host>/mcp --header "Authorization: Bearer <MCP_AUTH_TOKEN>"

Configuration

All configuration is via environment variables, loaded from .env by Docker Compose:

Variable Required Description
OURA_ACCESS_TOKEN yes Oura personal access token
MCP_AUTH_TOKEN yes Secret gating all /mcp requests (path segment or Bearer header)
OURA_TIMEOUT no Oura API request timeout in seconds (default 30)
OURA_MCP_PORT no Host port the server is published on (default 8000)
OURA_MCP_BIND no Host interface to bind (default 127.0.0.1; set 0.0.0.0 to expose beyond this machine)

Compose fails fast with a clear error if either required variable is missing; the server itself also refuses to start (RuntimeError) if MCP_AUTH_TOKEN is unset, whether run under Compose or via uvicorn oura_mcp.server:app directly.

Testing

uv sync
uv run pytest

Runs in CI on every push and PR to main via .github/workflows/test.yml (uv sync --locked && uv run pytest, Python 3.12 — matching the container's python:3.12-slim). The docker-marked container smoke test is excluded from the default run (see below) and does not run in CI.

42 tests, ~99% coverage on client.py/tools.py (gate held at 98%). Every Oura API call is stubbed from synthetic fixtures in tests/fixtures/ (generated against Oura's public OpenAPI spec, vendored as openapi-1.37.json — no real health data, no live PAT). An autouse fixture wraps every test in respx's network guard, which raises immediately on any unmocked HTTP call rather than letting it reach the network.

  • tests/test_client.pyOuraClient pagination, including the two-page concatenation test (the single highest-value test in the suite).
  • tests/test_tools.py — per-tool field-mapping fidelity, including a golden-record check for get_sleep_periods.
  • tests/test_auth.py — the auth middleware accept/reject matrix.
  • tests/test_fixtures_validate.py — fixtures validated against the vendored OpenAPI spec (scripts/validate_fixtures.py).
  • tests/test_integration.py — 13 end-to-end scenarios at the real MCP protocol boundary (JSON-RPC over HTTP through the real ASGI app and auth middleware): handshake, single- and multi-page tool calls, unit/timestamp fidelity, heart-rate summarization, the full auth matrix including fail-closed-on-unset-token, health endpoint behavior, and explicit error propagation on upstream 401/429/timeout (no silent failures).

The container smoke test (test_container_smoke_docker_compose_up: real docker compose up, poll /health, one real initialize against 127.0.0.1:8000, and a check that the published port is loopback-only) is marked @pytest.mark.docker + @pytest.mark.slow and skipped by default — run it explicitly with uv run pytest -m docker. Docker isn't installed on the machine this fork is normally developed on, so this test can't run there; it instead runs on demand in CI via the docker-smoke job in .github/workflows/test.yml (workflow_dispatch, since GitHub-hosted runners ship Docker) — verified passing.

Security posture

All of the following were already present in upstream at the c8db34f fork point — this fork verifies them with tests and preserves them through the package extraction, it did not introduce them:

  • Loopback-only by default. docker-compose.yml binds 127.0.0.1:8000:8000 unless you set OURA_MCP_BIND=0.0.0.0.
  • Fail-closed auth. The server refuses to start if MCP_AUTH_TOKEN is unset — RuntimeError at import time, so this holds under uvicorn directly, not just under Compose's ${VAR:?} guard.
  • Constant-time token comparison (hmac.compare_digest), so response timing leaks nothing about the token.
  • Pinned dependencies. requirements.lock is fully pinned (via uv pip compile); the container installs from it at start rather than resolving version ranges fresh each time.
  • Auth-gated health detail. /health answers anonymous callers with bare liveness ({"status": "ok"}) only. Oura connectivity detail — which would reveal whether your token is currently valid — requires the auth token, and the upstream check behind it is cached 60s so it can't be used to burn your Oura API quota.
  • Container hardening carried over unchanged: no-new-privileges, 256 MB memory limit, log rotation.

Still the operator's responsibility: TLS termination. The auth token travels in the URL path (for claude.ai connectors) or a header — put the server behind a TLS-terminating reverse proxy (Caddy, nginx, Cloudflare Tunnel, Tailscale) before exposing it beyond localhost. Path-based tokens can end up in proxy access logs — treat those logs as sensitive. Keep .env out of version control (already covered by .gitignore); if a token leaks, revoke it at cloud.ouraring.com and generate a new MCP_AUTH_TOKEN.

Known gaps / follow-ups

  • health() blocks the event loop. The handler is async def but calls synchronous httpx under the hood via OuraClient — a pre-existing upstream characteristic, carried over unchanged rather than fixed inline, per this fork's behavior-preserving-extraction scope. Worth a dedicated follow-up if /health latency ever matters under load.

How it works

docker-compose.yml starts a stock python:3.12-slim container, bind-mounts src/oura_mcp and requirements.lock, installs dependencies from the lockfile at boot, and runs uvicorn oura_mcp.server:app. Auth is a small Starlette middleware (oura_mcp.auth.TokenPathAuthMiddleware) that accepts either POST /mcp/<token> (claude.ai) or POST /mcp with a Bearer header (Claude Code), and OuraClient transparently follows next_token pagination to exhaustion on every collection endpoint.

License

MIT © 2026 Cameron Ingham (upstream). Fork point: c8db34f.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured
E2B

E2B

Using MCP to run code via e2b.

Official
Featured