feat(wellness): add update_wellness write tool, computed Form (TSB), date-label fix

Add an update_wellness MCP tool that writes nutrition macros, hydration, vitals,
sleep, and subjective ratings to Intervals.icu via PUT /athlete/{id}/wellness/{date}.
Only provided fields are sent; pass -1 to clear a numeric field and locked=True to
stop device/app syncs from overwriting the values. Sleep is taken in hours and
stored as seconds (with -1 passing through as the clear sentinel).

Also surface computed Form (TSB = CTL - ATL) in wellness output, and prefer an
explicit `date` field over the record `id` for the Date label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NGzHtDvJur9U7ysgRKRUTN
This commit is contained in:
2026-07-20 09:28:31 -04:00
parent 74fb09972a
commit f2159d3ca5
7 changed files with 292 additions and 4 deletions
+5 -1
View File
@@ -32,7 +32,10 @@ from intervals_mcp_server.tools.power_curves import ( # noqa: F401
get_athlete_power_curves,
)
from intervals_mcp_server.tools.gear import get_gear_list # noqa: F401
from intervals_mcp_server.tools.wellness import get_wellness_data # noqa: F401
from intervals_mcp_server.tools.wellness import ( # noqa: F401
get_wellness_data,
update_wellness,
)
def register_tools(mcp_instance: FastMCP) -> None:
@@ -70,4 +73,5 @@ __all__ = [
"get_athlete_power_curves",
"get_gear_list",
"get_wellness_data",
"update_wellness",
]
+119 -1
View File
@@ -4,11 +4,14 @@ Wellness-related MCP tools for Intervals.icu.
This module contains tools for retrieving athlete wellness data.
"""
from datetime import datetime
from typing import Any
from intervals_mcp_server import credentials
from intervals_mcp_server.api.client import make_intervals_request
from intervals_mcp_server.credentials import CredentialError
from intervals_mcp_server.utils.formatting import format_wellness_entry
from intervals_mcp_server.utils.validation import resolve_date_params
from intervals_mcp_server.utils.validation import resolve_date_params, validate_date
# Import mcp instance from shared module for tool registration
from intervals_mcp_server.mcp_instance import mcp # noqa: F401
@@ -65,3 +68,118 @@ async def get_wellness_data(
wellness_summary += format_wellness_entry(entry, include_all_fields=include_all_fields) + "\n\n"
return wellness_summary
@mcp.tool()
async def update_wellness( # pylint: disable=too-many-arguments,too-many-positional-arguments,too-many-locals
date: str | None = None,
weight: float | None = None,
resting_hr: int | None = None,
hrv: float | None = None,
sleep_hours: float | None = None,
sleep_quality: int | None = None,
calories_consumed: int | None = None,
carbohydrates: float | None = None,
protein: float | None = None,
fat: float | None = None,
hydration_volume: float | None = None,
hydration_score: int | None = None,
soreness: int | None = None,
fatigue: int | None = None,
stress: int | None = None,
mood: int | None = None,
motivation: int | None = None,
injury: int | None = None,
comments: str | None = None,
locked: bool | None = None,
) -> str:
"""Create or update the signed-in athlete's wellness record for a single day.
Writes nutrition, hydration, vitals, sleep, and subjective ratings to
Intervals.icu (PUT /athlete/{id}/wellness/{date}). Only the fields you pass are
sent; anything omitted is left untouched. To CLEAR an existing numeric value,
pass ``-1``. Set ``locked=True`` to stop Intervals.icu from overwriting these
values on its next sync from a connected device or app.
Args:
date: Day to update in YYYY-MM-DD format (optional, defaults to today).
weight: Body weight in kg.
resting_hr: Resting heart rate in bpm.
hrv: Heart rate variability (rMSSD).
sleep_hours: Sleep duration in hours (stored by Intervals.icu as seconds).
Pass -1 to clear.
sleep_quality: Sleep quality rating, as used in the Intervals.icu app.
calories_consumed: Energy intake in kcal.
carbohydrates: Carbohydrate intake in grams.
protein: Protein intake in grams.
fat: Fat intake in grams.
hydration_volume: Fluid intake volume, in your Intervals.icu units.
hydration_score: Subjective hydration score, as used in the app.
soreness: Subjective soreness rating, as used in the Intervals.icu app.
fatigue: Subjective fatigue rating, as used in the Intervals.icu app.
stress: Subjective stress rating, as used in the Intervals.icu app.
mood: Subjective mood rating, as used in the Intervals.icu app.
motivation: Subjective motivation rating, as used in the Intervals.icu app.
injury: Injury level rating, as used in the Intervals.icu app.
comments: Free-text note for the day.
locked: If True, lock the record so device/app syncs won't overwrite it.
"""
try:
athlete_id_to_use, api_key = await credentials.resolve_caller_credentials()
except CredentialError as exc:
return str(exc)
if not date:
date = datetime.now().strftime("%Y-%m-%d")
try:
date = validate_date(date)
except ValueError as exc:
return f"Error: {exc}"
# Sleep is passed in hours but stored as seconds; -1 is the clear sentinel and
# must pass through unscaled.
sleep_secs: int | None = None
if sleep_hours is not None:
sleep_secs = -1 if sleep_hours == -1 else int(sleep_hours * 3600)
# Map snake_case tool params to the Intervals.icu camelCase wellness fields.
field_map: list[tuple[str, Any]] = [
("weight", weight),
("restingHR", resting_hr),
("hrv", hrv),
("sleepSecs", sleep_secs),
("sleepQuality", sleep_quality),
("kcalConsumed", calories_consumed),
("carbohydrates", carbohydrates),
("protein", protein),
("fatTotal", fat),
("hydrationVolume", hydration_volume),
("hydration", hydration_score),
("soreness", soreness),
("fatigue", fatigue),
("stress", stress),
("mood", mood),
("motivation", motivation),
("injury", injury),
("comments", comments),
("locked", locked),
]
payload: dict[str, Any] = {k: v for k, v in field_map if v is not None}
if not payload:
return "No wellness fields provided. Pass at least one field to update."
result = await make_intervals_request(
url=f"/athlete/{athlete_id_to_use}/wellness/{date}",
api_key=api_key,
method="PUT",
data=payload,
)
if isinstance(result, dict) and "error" in result:
return f"Error updating wellness data: {result.get('message')}"
# Intervals.icu echoes back the full updated record; render it for confirmation.
if isinstance(result, dict):
return f"Updated wellness for {date}:\n\n" + format_wellness_entry(result)
return f"Updated wellness for {date}."