import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { z } from "zod"
// Configuration schema requiring the Oura Personal Access Token
export const configSchema = z.object({
accessToken: z.string().describe("Your Oura Personal Access Token from https://cloud.ouraring.com/personal-access-tokens"),
})
const OURA_API_BASE = "https://api.ouraring.com/v2/usercollection"
// Helper function to make authenticated API requests
async function fetchOuraData(endpoint: string, accessToken: string, params?: Record<string, string>) {
const url = new URL(`${OURA_API_BASE}${endpoint}`)
if (params) {
Object.entries(params).forEach(([key, value]) => {
url.searchParams.append(key, value)
})
}
const response = await fetch(url.toString(), {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
})
if (!response.ok) {
throw new Error(`Oura API error: ${response.status} ${response.statusText}`)
}
return await response.json()
}
export default function createServer({
config,
}: {
config: z.infer<typeof configSchema>
}) {
const server = new McpServer({
name: "Oura Ring API",
version: "1.0.0",
})
// Tool: Get Personal Information
server.registerTool(
"get-personal-info",
{
title: "Get Personal Information",
description: "Retrieve your personal information from Oura Ring including age, weight, height, and biological sex",
inputSchema: {},
},
async () => {
try {
const data = await fetchOuraData("/personal_info", config.accessToken)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching personal info: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Daily Sleep Data
server.registerTool(
"get-daily-sleep",
{
title: "Get Daily Sleep Data",
description: "Retrieve daily sleep data including total sleep time, sleep stages, efficiency, and sleep score",
inputSchema: {
start_date: z.string().optional().describe("Start date in YYYY-MM-DD format (optional)"),
end_date: z.string().optional().describe("End date in YYYY-MM-DD format (optional)"),
},
},
async ({ start_date, end_date }) => {
try {
const params: Record<string, string> = {}
if (start_date) params.start_date = start_date
if (end_date) params.end_date = end_date
const data = await fetchOuraData("/daily_sleep", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching sleep data: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Daily Activity Data
server.registerTool(
"get-daily-activity",
{
title: "Get Daily Activity Data",
description: "Retrieve daily activity data including steps, calories, distance, and activity score",
inputSchema: {
start_date: z.string().optional().describe("Start date in YYYY-MM-DD format (optional)"),
end_date: z.string().optional().describe("End date in YYYY-MM-DD format (optional)"),
},
},
async ({ start_date, end_date }) => {
try {
const params: Record<string, string> = {}
if (start_date) params.start_date = start_date
if (end_date) params.end_date = end_date
const data = await fetchOuraData("/daily_activity", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching activity data: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Daily Readiness Data
server.registerTool(
"get-daily-readiness",
{
title: "Get Daily Readiness Data",
description: "Retrieve daily readiness data including readiness score, temperature deviation, and recovery indicators",
inputSchema: {
start_date: z.string().optional().describe("Start date in YYYY-MM-DD format (optional)"),
end_date: z.string().optional().describe("End date in YYYY-MM-DD format (optional)"),
},
},
async ({ start_date, end_date }) => {
try {
const params: Record<string, string> = {}
if (start_date) params.start_date = start_date
if (end_date) params.end_date = end_date
const data = await fetchOuraData("/daily_readiness", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching readiness data: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Heart Rate Data
server.registerTool(
"get-heart-rate",
{
title: "Get Heart Rate Data",
description: "Retrieve heart rate measurements including resting heart rate and heart rate variability",
inputSchema: {
start_datetime: z.string().optional().describe("Start datetime in ISO 8601 format (optional)"),
end_datetime: z.string().optional().describe("End datetime in ISO 8601 format (optional)"),
},
},
async ({ start_datetime, end_datetime }) => {
try {
const params: Record<string, string> = {}
if (start_datetime) params.start_datetime = start_datetime
if (end_datetime) params.end_datetime = end_datetime
const data = await fetchOuraData("/heartrate", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching heart rate data: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Workout Data
server.registerTool(
"get-workouts",
{
title: "Get Workout Data",
description: "Retrieve workout sessions including type, duration, intensity, and calories burned",
inputSchema: {
start_date: z.string().optional().describe("Start date in YYYY-MM-DD format (optional)"),
end_date: z.string().optional().describe("End date in YYYY-MM-DD format (optional)"),
},
},
async ({ start_date, end_date }) => {
try {
const params: Record<string, string> = {}
if (start_date) params.start_date = start_date
if (end_date) params.end_date = end_date
const data = await fetchOuraData("/workout", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching workout data: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Sleep Sessions
server.registerTool(
"get-sleep-sessions",
{
title: "Get Sleep Sessions",
description: "Retrieve detailed sleep session data including individual sleep periods with stages and metrics",
inputSchema: {
start_date: z.string().optional().describe("Start date in YYYY-MM-DD format (optional)"),
end_date: z.string().optional().describe("End date in YYYY-MM-DD format (optional)"),
},
},
async ({ start_date, end_date }) => {
try {
const params: Record<string, string> = {}
if (start_date) params.start_date = start_date
if (end_date) params.end_date = end_date
const data = await fetchOuraData("/sleep", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching sleep sessions: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Tool: Get Daily Stress Data
server.registerTool(
"get-daily-stress",
{
title: "Get Daily Stress Data",
description: "Retrieve daily stress measurements and recovery indicators",
inputSchema: {
start_date: z.string().optional().describe("Start date in YYYY-MM-DD format (optional)"),
end_date: z.string().optional().describe("End date in YYYY-MM-DD format (optional)"),
},
},
async ({ start_date, end_date }) => {
try {
const params: Record<string, string> = {}
if (start_date) params.start_date = start_date
if (end_date) params.end_date = end_date
const data = await fetchOuraData("/daily_stress", config.accessToken, params)
return {
content: [
{
type: "text",
text: JSON.stringify(data, null, 2),
}
],
}
} catch (error) {
return {
content: [
{
type: "text",
text: `Error fetching stress data: ${error instanceof Error ? error.message : String(error)}`,
}
],
isError: true,
}
}
},
)
// Resource: Oura API Documentation
server.registerResource(
"oura-api-docs",
"docs://oura-api",
{
title: "Oura API Documentation",
description: "Official Oura API V2 documentation and reference",
},
async uri => ({
contents: [
{
uri: uri.href,
text: `Oura API V2 Documentation
The Oura API provides access to data generated by the Oura Ring, including:
- Personal Information: Age, weight, height, biological sex
- Sleep Data: Sleep stages, duration, efficiency, sleep score
- Activity Data: Steps, calories, distance, activity score
- Readiness Data: Readiness score, temperature, recovery indicators
- Heart Rate: Resting HR, HRV, continuous measurements
- Workouts: Exercise sessions with type, duration, intensity
- Stress Data: Daily stress levels and recovery
Authentication: Requires Personal Access Token
Base URL: https://api.ouraring.com/v2/usercollection
Date formats:
- Dates: YYYY-MM-DD (e.g., 2024-01-15)
- Datetimes: ISO 8601 format (e.g., 2024-01-15T10:30:00Z)
Official documentation: https://cloud.ouraring.com/docs/`,
mimeType: "text/plain",
},
],
}),
)
// Prompt: Analyze Sleep Quality
server.registerPrompt(
"analyze-sleep",
{
title: "Analyze Sleep Quality",
description: "Get an analysis of your recent sleep quality and patterns",
argsSchema: {
days: z.number().default(7).describe("Number of days to analyze (default: 7)"),
},
},
async ({ days }) => {
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - days)
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Please analyze my sleep quality over the last ${days} days. Get my sleep data from ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]} and provide insights on:
1. Average sleep duration and quality
2. Sleep stage distribution (deep, REM, light sleep)
3. Sleep efficiency trends
4. Any notable patterns or concerns
5. Recommendations for improvement`,
},
},
],
}
},
)
// Prompt: Activity Summary
server.registerPrompt(
"activity-summary",
{
title: "Activity Summary",
description: "Get a summary of your recent activity and movement patterns",
argsSchema: {
days: z.number().default(7).describe("Number of days to analyze (default: 7)"),
},
},
async ({ days }) => {
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - days)
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Please provide an activity summary for the last ${days} days. Get my activity data from ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]} and analyze:
1. Daily step counts and trends
2. Total calories burned
3. Activity score patterns
4. Most active vs least active days
5. Suggestions to increase activity levels`,
},
},
],
}
},
)
// Prompt: Readiness Check
server.registerPrompt(
"readiness-check",
{
title: "Readiness Check",
description: "Check your current readiness and recovery status",
argsSchema: {
days: z.number().default(3).describe("Number of days to analyze (default: 3)"),
},
},
async ({ days }) => {
const endDate = new Date()
const startDate = new Date()
startDate.setDate(startDate.getDate() - days)
return {
messages: [
{
role: "user",
content: {
type: "text",
text: `Please check my readiness and recovery status for the last ${days} days. Get my readiness data from ${startDate.toISOString().split('T')[0]} to ${endDate.toISOString().split('T')[0]} and provide:
1. Current readiness score and trend
2. Key factors affecting readiness (sleep, HRV, temperature)
3. Recovery indicators
4. Recommendations for today's activity level
5. Any warning signs or areas of concern`,
},
},
],
}
},
)
return server.server
}