We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Marholoubek/health_mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Sleep-related MCP tools."""
from datetime import datetime, timedelta
from typing import Any
from mcp.server import Server
from mcp.types import TextContent, Tool
from ..adapters.whoop import WhoopAdapter
def _format_duration(ms: int) -> str:
"""Format milliseconds as human-readable duration."""
hours = ms // (1000 * 60 * 60)
minutes = (ms % (1000 * 60 * 60)) // (1000 * 60)
return f"{hours}h {minutes}m"
def _format_sleep_record(record) -> dict[str, Any]:
"""Format a sleep record for output."""
result = {
"date": record.start.strftime("%Y-%m-%d"),
"start_time": record.start.strftime("%H:%M"),
"end_time": record.end.strftime("%H:%M"),
"duration": _format_duration(record.duration_ms),
"duration_hours": round(record.duration_hours, 2),
"is_nap": record.is_nap,
}
if record.performance_percentage is not None:
result["sleep_performance"] = f"{record.performance_percentage:.0f}%"
if record.efficiency_percentage is not None:
result["sleep_efficiency"] = f"{record.efficiency_percentage:.1f}%"
if record.consistency_percentage is not None:
result["sleep_consistency"] = f"{record.consistency_percentage:.0f}%"
if record.respiratory_rate is not None:
result["respiratory_rate"] = f"{record.respiratory_rate:.1f} breaths/min"
if record.stages:
stages = record.stages
actual_sleep_ms = stages.light_sleep_ms + stages.deep_sleep_ms + stages.rem_sleep_ms
result["sleep_stages"] = {
"time_in_bed": _format_duration(stages.total_in_bed_ms),
"awake": _format_duration(stages.awake_ms),
"light_sleep": _format_duration(stages.light_sleep_ms),
"deep_sleep": _format_duration(stages.deep_sleep_ms),
"rem_sleep": _format_duration(stages.rem_sleep_ms),
"sleep_cycles": stages.sleep_cycles,
"disturbances": stages.disturbances,
}
if actual_sleep_ms > 0:
result["sleep_stages"]["light_percentage"] = f"{stages.light_sleep_ms / actual_sleep_ms * 100:.0f}%"
result["sleep_stages"]["deep_percentage"] = f"{stages.deep_sleep_ms / actual_sleep_ms * 100:.0f}%"
result["sleep_stages"]["rem_percentage"] = f"{stages.rem_sleep_ms / actual_sleep_ms * 100:.0f}%"
if record.needs:
needs = record.needs
result["sleep_needs"] = {
"baseline_need": _format_duration(needs.baseline_ms),
"additional_from_debt": _format_duration(needs.debt_ms),
"additional_from_strain": _format_duration(needs.strain_ms),
"credit_from_naps": _format_duration(needs.nap_credit_ms),
"total_need": _format_duration(needs.total_need_ms),
}
return result
def register_sleep_tools(server: Server, whoop: WhoopAdapter) -> None:
"""Register sleep-related tools with the MCP server.
Args:
server: MCP server instance.
whoop: Whoop adapter instance.
"""
@server.list_tools()
async def list_sleep_tools() -> list[Tool]:
"""List available sleep tools."""
return [
Tool(
name="get_sleep_summary",
description="Get a summary of recent sleep including performance, stages, efficiency, and sleep needs. Returns the most recent sleep record by default.",
inputSchema={
"type": "object",
"properties": {
"days_back": {
"type": "integer",
"description": "Number of days to look back (default: 1 for last night's sleep)",
"default": 1,
},
},
},
),
Tool(
name="get_sleep_history",
description="Get sleep history over a date range with trends and statistics. Useful for analyzing sleep patterns over time.",
inputSchema={
"type": "object",
"properties": {
"days": {
"type": "integer",
"description": "Number of days of history to retrieve (default: 7)",
"default": 7,
},
"start_date": {
"type": "string",
"description": "Start date in YYYY-MM-DD format (alternative to days parameter)",
},
"end_date": {
"type": "string",
"description": "End date in YYYY-MM-DD format (defaults to today)",
},
"include_naps": {
"type": "boolean",
"description": "Include naps in the results (default: false)",
"default": False,
},
},
},
),
]
@server.call_tool()
async def handle_sleep_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle sleep tool calls."""
if name == "get_sleep_summary":
return await _get_sleep_summary(whoop, arguments)
elif name == "get_sleep_history":
return await _get_sleep_history(whoop, arguments)
return []
async def _get_sleep_summary(whoop: WhoopAdapter, args: dict) -> list[TextContent]:
"""Get sleep summary."""
if not await whoop.is_authenticated():
return [TextContent(
type="text",
text="Not authenticated with Whoop. Please use the 'whoop_authenticate' tool first.",
)]
days_back = args.get("days_back", 1)
start = datetime.now() - timedelta(days=days_back)
try:
records = await whoop.get_sleep(start=start, limit=5)
# Filter out naps for summary
main_sleeps = [r for r in records if not r.is_nap]
if not main_sleeps:
return [TextContent(type="text", text="No sleep records found for the specified period.")]
latest = main_sleeps[0]
formatted = _format_sleep_record(latest)
# Build summary text
summary_parts = [
f"## Sleep Summary for {formatted['date']}",
"",
f"**Duration:** {formatted['duration']} ({formatted['duration_hours']} hours)",
f"**Time:** {formatted['start_time']} - {formatted['end_time']}",
]
if "sleep_performance" in formatted:
summary_parts.append(f"**Sleep Performance:** {formatted['sleep_performance']}")
if "sleep_efficiency" in formatted:
summary_parts.append(f"**Sleep Efficiency:** {formatted['sleep_efficiency']}")
if "sleep_consistency" in formatted:
summary_parts.append(f"**Sleep Consistency:** {formatted['sleep_consistency']}")
if "respiratory_rate" in formatted:
summary_parts.append(f"**Respiratory Rate:** {formatted['respiratory_rate']}")
if "sleep_stages" in formatted:
stages = formatted["sleep_stages"]
summary_parts.extend([
"",
"### Sleep Stages",
f"- Light Sleep: {stages['light_sleep']} ({stages.get('light_percentage', 'N/A')})",
f"- Deep Sleep: {stages['deep_sleep']} ({stages.get('deep_percentage', 'N/A')})",
f"- REM Sleep: {stages['rem_sleep']} ({stages.get('rem_percentage', 'N/A')})",
f"- Awake: {stages['awake']}",
f"- Sleep Cycles: {stages['sleep_cycles']}",
f"- Disturbances: {stages['disturbances']}",
])
if "sleep_needs" in formatted:
needs = formatted["sleep_needs"]
summary_parts.extend([
"",
"### Sleep Needs",
f"- Baseline Need: {needs['baseline_need']}",
f"- Additional from Sleep Debt: {needs['additional_from_debt']}",
f"- Additional from Strain: {needs['additional_from_strain']}",
f"- **Total Sleep Need:** {needs['total_need']}",
])
return [TextContent(type="text", text="\n".join(summary_parts))]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching sleep data: {str(e)}")]
async def _get_sleep_history(whoop: WhoopAdapter, args: dict) -> list[TextContent]:
"""Get sleep history with trends."""
if not await whoop.is_authenticated():
return [TextContent(
type="text",
text="Not authenticated with Whoop. Please use the 'whoop_authenticate' tool first.",
)]
# Parse date range
if "start_date" in args:
start = datetime.strptime(args["start_date"], "%Y-%m-%d")
end = datetime.strptime(args.get("end_date", datetime.now().strftime("%Y-%m-%d")), "%Y-%m-%d")
end = end.replace(hour=23, minute=59, second=59)
else:
days = args.get("days", 7)
end = datetime.now()
start = end - timedelta(days=days)
include_naps = args.get("include_naps", False)
try:
records = await whoop.get_sleep(start=start, end=end, limit=100)
if not include_naps:
records = [r for r in records if not r.is_nap]
if not records:
return [TextContent(type="text", text="No sleep records found for the specified period.")]
# Calculate statistics
durations = [r.duration_hours for r in records]
performances = [r.performance_percentage for r in records if r.performance_percentage is not None]
efficiencies = [r.efficiency_percentage for r in records if r.efficiency_percentage is not None]
deep_sleep_percentages = []
rem_sleep_percentages = []
for r in records:
if r.stages:
total_sleep = r.stages.light_sleep_ms + r.stages.deep_sleep_ms + r.stages.rem_sleep_ms
if total_sleep > 0:
deep_sleep_percentages.append(r.stages.deep_sleep_ms / total_sleep * 100)
rem_sleep_percentages.append(r.stages.rem_sleep_ms / total_sleep * 100)
avg_duration = sum(durations) / len(durations) if durations else 0
avg_performance = sum(performances) / len(performances) if performances else 0
avg_efficiency = sum(efficiencies) / len(efficiencies) if efficiencies else 0
avg_deep = sum(deep_sleep_percentages) / len(deep_sleep_percentages) if deep_sleep_percentages else 0
avg_rem = sum(rem_sleep_percentages) / len(rem_sleep_percentages) if rem_sleep_percentages else 0
# Build response
summary_parts = [
f"## Sleep History: {start.strftime('%Y-%m-%d')} to {end.strftime('%Y-%m-%d')}",
f"**Total Records:** {len(records)} nights",
"",
"### Averages",
f"- **Average Duration:** {avg_duration:.1f} hours",
f"- **Average Performance:** {avg_performance:.0f}%",
f"- **Average Efficiency:** {avg_efficiency:.1f}%",
f"- **Average Deep Sleep:** {avg_deep:.0f}%",
f"- **Average REM Sleep:** {avg_rem:.0f}%",
"",
"### Trends",
]
# Analyze trend (comparing first half to second half)
if len(durations) >= 4:
mid = len(durations) // 2
first_half_avg = sum(durations[:mid]) / mid
second_half_avg = sum(durations[mid:]) / (len(durations) - mid)
trend = "improving" if second_half_avg > first_half_avg else "declining" if second_half_avg < first_half_avg else "stable"
summary_parts.append(f"- Duration trend: **{trend}** ({first_half_avg:.1f}h → {second_half_avg:.1f}h)")
if len(performances) >= 4:
mid = len(performances) // 2
first_half_avg = sum(performances[:mid]) / mid
second_half_avg = sum(performances[mid:]) / (len(performances) - mid)
trend = "improving" if second_half_avg > first_half_avg else "declining" if second_half_avg < first_half_avg else "stable"
summary_parts.append(f"- Performance trend: **{trend}** ({first_half_avg:.0f}% → {second_half_avg:.0f}%)")
# Daily breakdown
summary_parts.extend(["", "### Daily Breakdown"])
for record in sorted(records, key=lambda r: r.start, reverse=True)[:10]:
formatted = _format_sleep_record(record)
perf = formatted.get('sleep_performance', 'N/A')
summary_parts.append(
f"- **{formatted['date']}**: {formatted['duration']} | Performance: {perf}"
)
if len(records) > 10:
summary_parts.append(f" ... and {len(records) - 10} more records")
return [TextContent(type="text", text="\n".join(summary_parts))]
except Exception as e:
return [TextContent(type="text", text=f"Error fetching sleep history: {str(e)}")]