MCP server exposing Garmin health and training data from InfluxDB
The server has 20 well-named tools with comprehensive descriptions and documented parameter schemas. All tools follow verb_noun naming convention (get_*, explore_*). Descriptions are detailed and exceed the 194-char baseline in most cases. Input schemas are present with type declarations and descriptions for all parameters. However, output schemas are documented in natural language within descriptions rather than as formal JSON Schema output definitions. Error handling guidance is present but inconsistent, some tools mention graceful null/note returns (e.g., get_cycling_dynamics, get_peak_power) while others lack explicit recovery instructions. Tool names are action-oriented and distinct (no ambiguous pairs). Parameters include range constraints (e.g., 'days: 1 - 90', 'limit: 1 - 100') and defaults are sensible. The server uses fastmcp framework with HTTP transport, enabling remote accessibility. No security concerns detected (no secrets in parameters, read-only operations only). All 20 tools are explicitly registered with @mcp.tool() decorators in server.py, confirming direct visibility of definitions.
Introspect the InfluxDB schema: list measurements, field names, and sample data to understand available dimensions and metrics. No input parameters required. Returns ------- measurements : list Available measurements and their field names sample_timestamps : list Recent timestamp range in the database database_name : str Connected database name
Return detailed breakdown of a single activity including HR zones, training effect, and per-lap splits. Parameters ---------- activity_id : str The activity ID. Discoverable from get_recent_activities_tool (each activity includes an activity_id field). Returns ------- activity Core metrics (distance, duration, HR, speed) plus: - hr_zones : minutes in each zone (1–5) - training_effect : aerobic and anaerobic (0–5 scale)
Return activities with their training load and training effect scores. Parameters ---------- days : int Look-back window in days (1–90, default 14) sport_type : str Filter by Garmin sport type or "all" (default "all") limit : int Max activities returned (1–100, default 30) Returns ------- activities : list Per-activity training load, aerobic/anaerobic training effect (shows which sessions drive acute load) summary : dict - total_load - avg_load_per_session - load_by_sport - highest_load_activity - avg_aerobic_te, avg_anaerobic_te
Return cycling dynamics for a single activity. Data comes from the CyclingDynamics measurement added by the garmin-grafana CyclingDynamics patch. If the measurement does not exist or the activity has no dynamics data, a descriptive data_note is returned instead of an error so callers can handle missing hardware gracefully. Parameters ---------- activity_id : str The activity ID (from get_recent_activities or get_activity_details) Returns ------- activity_id : str Echoed back for traceability power : dict normalized_power (W), training_stress_score, intensity_factor, left_right_balance ({left_pct, right_pct}) left_pedal : dict torque_effectiveness (%), pedal_smoothness (%), platform_center_offset_mm, power_phase {start_deg, end_deg}, power_phase_peak {start_deg, end_deg} right_pedal : dict Same structure as left_pedal data_note : str Present (instead of the above) when no data found
Output schemas not formally documented as JSON Schema. Return structures are described in natural language within tool docstrings rather than as structured schema definitions. LLMs must infer field types, nullability, and nesting from prose, increasing hallucination risk on follow-up calls that depend on specific response fields.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | B | 71 | <=2025-11-25 | v2 |
Return daily time-use, energy, movement, and stress attribution data. Parameters ---------- days : int Look-back window in days (1–14, default 7) Returns ------- days : list One entry per date (newest first), each containing: - date - time_use : { sedentary_hours, active_hours, highly_active_hours, sleeping_hours } - energy : { bmr_kcal, active_kcal } - movement : { total_steps, total_distance_km, floors_ascended, floors_descended, floors_ascended_meters, floors_descended_meters } - recovery_context : { body_battery_during_sleep, body_battery_at_wake, resting_hr } - stress_attribution: { activity_stress_min, activity_stress_pct, total_stress_min, stress_pct, uncategorized_stress_min } summary : dict Period averages and trend analysis
Return daily recovery and readiness data combining sleep quality with daily health metrics. Parameters ---------- days : int Look-back window in days. Range: 1–14. Default: 7. Returns ------- days : list One entry per date (newest first), each containing: - date : ISO date string - sleep : sleep_score, total_sleep_hours, deep/light/rem/awake hours, avg_overnight_hrv, avg_sleep_stress, body_battery_change, resting_hr, SpO2, highest/lowest_respiration, highest_spo2 - daily : resting_hr, body_battery_at_wake/high/low, body_battery_during_sleep, total_steps, stress breakdown (minutes), activity_stress_min/pct, active_calories, bmr_kcal, intensity minutes, sedentary/active/highly_active/sleeping hours, SpO2, floors_ascended/descended + meters summary Period averages: avg_sleep_score, avg_sleep_hours, avg_overnight_hrv, avg_resting_hr, avg_body_battery_at_wake.
Return weekly-sampled fitness age trajectory. Parameters ---------- weeks : int Look-back window in weeks (4–52, default 12) Returns ------- weeks : list One entry per ISO week (newest first), each containing: - week_label : "YYYY-Www" - week_start_date : ISO date of Monday - fitness_age : biological fitness age (years) - chronological_age : actual age (years) - achievable_fitness_age : target achievable with training (years) - fitness_age_gap : fitness_age - chronological_age - improvement_potential : fitness_age - achievable_fitness_age trends : dict Computed changes: fitness_age_change, fitness_age_gap_change, improvement_potential_change
Return long-term fitness trajectory: VO2max, race predictions, weight, resting HR — sampled weekly to minimise tokens. Parameters ---------- weeks : int Look-back window in weeks (4–52, default 12) Returns ------- weeks : list One entry per ISO week (newest first), each containing: - week_label : "YYYY-Www" - week_start_date : ISO date of Monday - vo2max_running : VO2max in ml/kg/min (running), or null - vo2max_cycling : VO2max in ml/kg/min (cycling), or null - race_predictions : { time_5k_minutes, time_10k_minutes, time_half_marathon, time_marathon } - weight_kg : body weight, or null - avg_resting_hr : weekly average resting HR, or null trends : dict Calculated changes in key metrics over the period (if ≥2 data points) - vo2max_running_change, vo2max_running_period - weight_kg_change, weight_kg_period - avg_resting_hr_change, avg_resting_hr_period - race_5k_change_seconds (seconds, negative = faster)
Return weekly LTHR (Lactate Threshold Heart Rate) trend for running. Parameters ---------- weeks : int Look-back window in weeks (4–52, default 12) Returns ------- weeks : list One entry per ISO week (newest first), each containing: - week_label : "YYYY-Www" - week_start_date : ISO date of Monday - lthr_running : Lactate Threshold HR (bpm) for running, or null trends : dict Computed changes: lthr_change, lthr_period (if ≥2 data points)
Return the single most recent Garmin activity from InfluxDB. No input parameters required. Returns fields: timestamp, sport_type, distance_km, duration_minutes, avg_hr, max_hr, calories, avg_pace_min_per_km (runs/swims), avg_speed_kmh (cycling/other), elevation_gain_m, avg_cadence, avg_power, training_load (Garmin EPOC load), aerobic_training_effect (0–5), anaerobic_training_effect (0–5). Fields not recorded by Garmin show as null. Power note: only avg_power (duration-weighted average from lap data) is available. Normalized Power (NP) cannot be queried or calculated — the upstream database does not store the required second-by-second data in a form that can be aggregated without crashing the server. Do not attempt to compute or estimate NP.
Return rolling peak power efforts from activity GPS data. Parameters ---------- activity_id : str The activity ID (from get_recent_activities) Returns ------- activity_id : str Echoed back for traceability peak_efforts : list Rolling peak power windows (5s, 1m, 5m, 20m) with watts and timestamp data_note : str Present (instead of peak_efforts) if no ActivityGPS data found
Return Garmin personal records (best efforts) by distance and duration. No input parameters required. Returns ------- records : list Sorted by recency, each containing: - activity_id - timestamp - distance_km - duration_minutes - avg_speed_kmh (or avg_pace_min_per_km for pace sports) - avg_hr - record_date_short (YYYY-MM-DD) - distance_label (human-readable: "1 km", "5 km", "10 min", etc.)
Return per-session power statistics: average, normalized, TSS, IF. Parameters ---------- days : int Look-back window in days (1–90, default 30) sport_type : str Filter by sport (e.g. "cycling") or "all" (default "all") limit : int Max activities returned (1–100, default 30) Returns ------- activities : list Per-activity power metrics (avg, normalized, max, TSS, IF) summary : dict - total_sessions, total_np_hours, avg_np_per_session, total_tss, avg_if, highest_np_session
Return Coggan 7-zone power distribution from activity data. No input parameters required. Returns ------- zone_distribution : dict Z1 through Z7: { minutes, pct } total_minutes by_sport : dict (optional) Per-sport distribution when multiple sports in period summary : dict - anaerobic_minutes (Z7) - threshold_minutes (Z6) - hard_minutes (Z5) - steady_state_minutes (Z3–Z4) - endurance_minutes (Z1–Z2)
Return Garmin activities from the last N days. Parameters ---------- days : int Look-back window in days. Range: 1–90. Default: 7. sport_type : str Filter by sport. Any valid Garmin sport type string (e.g. "running", "cycling", "swimming", "hiking", "trail_running", "strength_training"). Supports partial matching for sub-sports (e.g. "cycling" matches "indoor_cycling"). Use "all" for no filter. Default: "all". limit : int Maximum number of records returned. Range: 1–100. Default: 20. Returns ------- activities List of activity objects (newest first), each with the same fields as get_last_activity. summary Aggregate block: total_activities, total_distance_km_by_sport, total_duration_minutes, date_range_from, date_range_to.
Return overnight autonomic nervous system metrics: HR, HRV, respiration, SpO2, and stress during sleep — a deep-dive beyond SleepSummary. Parameters ---------- days : int Look-back window in days (1–14, default 7) Returns ------- nights : list One entry per night (newest first), each containing: - night_date : ISO date - sleep_duration : hours - heart_rate : { avg, min, max } - hrv : { avg, min, max } (milliseconds) - respiration : { avg, min, max } (breaths/min) - spo2 : { avg, min, max } (percent) - stress : { avg, min, max } (0–100) - body_battery : { at_sleep, at_wake, min, max } - restless_indicator : qualitative measure if available summary : dict - nights_recorded, avg_sleep_hours, avg_hrv_ms, avg_overnight_rhr, avg_spo2_pct, nights_with_restless_episodes
Return daily stress and body battery data: current levels, trend, and recharge context. Parameters ---------- days : int Look-back window in days (1–14, default 7) Returns ------- days : list One entry per date, each containing: - date - stress_level : 0–100 (high = stressed) - body_battery_level : 0–100 (high = fresh) - overnight_recovery : true if BB increased overnight - stress_trend : rising/stable/falling - battery_trend : rising/stable/falling summary : dict - avg_stress, min_stress, max_stress - avg_battery, min_battery, max_battery - days_with_full_recovery (BB ≥ 90) - stress_pct_high (>80), moderate (50–80), low (<50) - trend analysis
Return current and historical training status and readiness scores. No input parameters required. Returns ------- current_status : dict - status_code : string code for current training status - status_label : human-readable status description - status_date : ISO date - acute_load : daily training load (acute window) - chronic_load : daily training load (chronic window) - load_balance_ratio : acute / chronic ratio - acwr_percent : load balance ratio as percentage - fitness_trend : text description of fitness trajectory - readiness_score : 0–100 readiness score (high = ready) - readiness_label : text description of readiness history : list Past 12 days (newest first), each with status_code, status_label, acute_load, chronic_load, readiness_score
Aggregate HR zone distribution across activities in the period. Parameters ---------- days : int Look-back window in days (7–180, default 30) sport_type : str Filter by Garmin sport type (e.g. "running", "cycling", "swimming", "hiking", "trail_running") or "all" for no filter. Supports partial/sub-sport matching. Returns ------- zone_distribution : dict zone_1 through zone_5: { minutes, pct } total_minutes polarization : dict low_intensity_pct, moderate_intensity_pct, high_intensity_pct, aerobic_vs_anaerobic split by_sport : dict (optional) Per-sport zone distribution when multiple sports present in period
Group Garmin activities into ISO calendar weeks and return raw aggregates. Parameters ---------- weeks : int Number of past weeks to include. Range: 1–16. Default: 4. Returns ------- weeks : list One entry per ISO week (newest first), each containing: - week_label : "YYYY-Www" - week_start_date : ISO date of the Monday - per_sport : { sport: { sessions, total_distance_km, total_duration_min } } - avg_resting_hr : weekly average resting HR, or null - hrv_weekly_avg : weekly average HRV, or null - stress_or_load_score : raw field from InfluxDB, or null No derived fitness metrics (ATL/CTL/TSB) are computed.
Sparse error recovery guidance. Several tools (get_power_zones, explore_schema) mention null/data_note returns but do not guide the LLM on what to do when data is missing or the query fails. Recovery guidance like 'If no data is found, try expanding the date range' is absent.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint) declared in the MCP schema. All tools are read-only but lack explicit annotation to signal this to clients. This is a spec alignment gap for MCP 2026-07-28.
Parameter validation rules not uniformly documented. Constraints like 'days 1 - 90', 'limit 1 - 100' are embedded in descriptions rather than formally in JSON Schema with minValue/maxValue. LLMs cannot reliably parse prose constraints, JSON Schema validation is machine-readable and prevents invalid input.
Tools returning structured lists (activities, weeks, days) do not declare pagination parameters (limit, offset, page_size). get_recent_activities has limit but no offset or page mechanism for large result sets. Unbounded list returns risk exhausting context windows.