"""
Oura Ring MCP Server
A Model Context Protocol server that provides access to Oura Ring health data.
Supports both Personal Access Token and OAuth2 authentication.
"""
import os
from datetime import datetime, timedelta
from typing import Optional
import httpx
from mcp.server.fastmcp import FastMCP
from dotenv import load_dotenv
# Load environment variables from .env file
load_dotenv()
# Initialize FastMCP server
mcp = FastMCP("oura-ring")
# Oura API Configuration
OURA_API_BASE = "https://api.ouraring.com/v2/usercollection"
OURA_PERSONAL_ACCESS_TOKEN = os.getenv("OURA_PERSONAL_ACCESS_TOKEN")
OURA_CLIENT_ID = os.getenv("OURA_CLIENT_ID")
OURA_CLIENT_SECRET = os.getenv("OURA_CLIENT_SECRET")
class OuraClient:
"""Client for interacting with Oura API"""
def __init__(self):
self.base_url = OURA_API_BASE
self.token = OURA_PERSONAL_ACCESS_TOKEN
if not self.token:
raise ValueError(
"OURA_PERSONAL_ACCESS_TOKEN environment variable is required. "
"Get your token from https://cloud.ouraring.com/personal-access-tokens"
)
def _get_headers(self) -> dict:
"""Get authorization headers"""
return {
"Authorization": f"Bearer {self.token}",
"Content-Type": "application/json"
}
async def make_request(self, endpoint: str, params: Optional[dict] = None) -> dict:
"""Make an authenticated request to Oura API"""
url = f"{self.base_url}/{endpoint}"
async with httpx.AsyncClient() as client:
response = await client.get(
url,
headers=self._get_headers(),
params=params,
timeout=30.0
)
response.raise_for_status()
return response.json()
# Global client instance
client = OuraClient()
def format_date(date_str: Optional[str] = None) -> str:
"""Format date string or return default (30 days ago)"""
if date_str:
# Validate date format
datetime.strptime(date_str, "%Y-%m-%d")
return date_str
# Default to 30 days ago
default_date = datetime.now() - timedelta(days=30)
return default_date.strftime("%Y-%m-%d")
@mcp.tool()
async def get_daily_activity(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily activity data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily activity metrics including steps, calories, and activity levels
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_activity", params)
return str(data)
@mcp.tool()
async def get_daily_readiness(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily readiness scores from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily readiness scores and contributing factors
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_readiness", params)
return str(data)
@mcp.tool()
async def get_daily_sleep(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily sleep data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily sleep scores, duration, efficiency, and sleep stages
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_sleep", params)
return str(data)
@mcp.tool()
async def get_sleep_sessions(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get detailed sleep session data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Detailed sleep session data including heart rate, HRV, and movement
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("sleep", params)
return str(data)
@mcp.tool()
async def get_sleep_time(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get sleep time data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Sleep timing information including bedtime and wake time
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("sleep_time", params)
return str(data)
@mcp.tool()
async def get_workouts(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get workout data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Workout sessions with activity type, duration, and intensity
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("workout", params)
return str(data)
@mcp.tool()
async def get_sessions(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get session data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Session data including meditation, breathing exercises, etc.
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("session", params)
return str(data)
@mcp.tool()
async def get_daily_spo2(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily SpO2 (blood oxygen) data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily blood oxygen saturation levels
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_spo2", params)
return str(data)
@mcp.tool()
async def get_daily_stress(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily stress data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily stress levels and recovery status
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_stress", params)
return str(data)
@mcp.tool()
async def get_daily_resilience(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily resilience data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily resilience scores and trends
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_resilience", params)
return str(data)
@mcp.tool()
async def get_daily_cardiovascular_age(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get daily cardiovascular age data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Daily cardiovascular age estimates
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("daily_cardiovascular_age", params)
return str(data)
@mcp.tool()
async def get_vo2_max(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get VO2 max data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
VO2 max measurements and trends
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("vO2_max", params)
return str(data)
@mcp.tool()
async def get_ring_configuration(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get Oura Ring configuration data.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Ring hardware configuration and settings
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("ring_configuration", params)
return str(data)
@mcp.tool()
async def get_rest_mode_period(
start_date: Optional[str] = None,
end_date: Optional[str] = None
) -> str:
"""
Get rest mode period data from Oura Ring.
Args:
start_date: Start date in YYYY-MM-DD format (defaults to 30 days ago)
end_date: End date in YYYY-MM-DD format (defaults to today)
Returns:
Rest mode periods when enabled
"""
params = {
"start_date": format_date(start_date),
"end_date": format_date(end_date)
}
data = await client.make_request("rest_mode_period", params)
return str(data)
@mcp.tool()
async def get_personal_info() -> str:
"""
Get personal information from Oura Ring account.
Returns:
User profile information including age, weight, height, and biological sex
"""
data = await client.make_request("personal_info")
return str(data)
if __name__ == "__main__":
# Run the MCP server
mcp.run()