Skip to main content
Glama
mvilanova

Intervals.icu MCP Server

by mvilanova

get_activity_details

Retrieve comprehensive fitness activity details, including workouts and metrics, from Intervals.icu using the activity ID and optional API key.

Instructions

Get detailed information for a specific activity from Intervals.icu

Args: activity_id: The Intervals.icu activity ID api_key: The Intervals.icu API key (optional, will use API_KEY from .env if not provided)

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
activity_idYes
api_keyNo

Implementation Reference

  • The main handler function for the 'get_activity_details' tool. It is registered via the @mcp.tool() decorator, fetches activity data from the Intervals.icu API, handles errors, formats the summary using format_activity_summary helper, and appends power/HR zone details.
    @mcp.tool() async def get_activity_details(activity_id: str, api_key: str | None = None) -> str: """Get detailed information for a specific activity from Intervals.icu Args: activity_id: The Intervals.icu activity ID api_key: The Intervals.icu API key (optional, will use API_KEY from .env if not provided) """ # Call the Intervals.icu API result = await make_intervals_request(url=f"/activity/{activity_id}", api_key=api_key) if isinstance(result, dict) and "error" in result: error_message = result.get("message", "Unknown error") return f"Error fetching activity details: {error_message}" # Format the response if not result: return f"No details found for activity {activity_id}." # If result is a list, use the first item if available activity_data = result[0] if isinstance(result, list) and result else result if not isinstance(activity_data, dict): return f"Invalid activity format for activity {activity_id}." # Return a more detailed view of the activity detailed_view = format_activity_summary(activity_data) # Add additional details if available if "zones" in activity_data: zones = activity_data["zones"] detailed_view += "\nPower Zones:\n" for zone in zones.get("power", []): detailed_view += f"Zone {zone.get('number')}: {zone.get('secondsInZone')} seconds\n" detailed_view += "\nHeart Rate Zones:\n" for zone in zones.get("hr", []): detailed_view += f"Zone {zone.get('number')}: {zone.get('secondsInZone')} seconds\n" return detailed_view
  • Utility helper function used by get_activity_details to make authenticated API requests to Intervals.icu, including error handling and JSON parsing.
    async def make_intervals_request( url: str, api_key: str | None = None, params: dict[str, Any] | None = None, method: str = "GET", data: dict[str, Any] | None = None, ) -> dict[str, Any] | list[dict[str, Any]]: """ Make a request to the Intervals.icu API with proper error handling. Args: url (str): The API endpoint path (e.g., '/athlete/{id}/activities'). api_key (str | None): Optional API key to use for authentication. Defaults to the global API_KEY. params (dict[str, Any] | None): Optional query parameters for the request. method (str): HTTP method to use (GET, POST, etc.). Defaults to GET. data (dict[str, Any] | None): Optional data to send in the request body. Returns: dict[str, Any] | list[dict[str, Any]]: The parsed JSON response from the API, or an error dict. """ headers = {"User-Agent": USER_AGENT, "Accept": "application/json"} if method in ["POST", "PUT"]: headers["Content-Type"] = "application/json" # Use provided api_key or fall back to global API_KEY key_to_use = api_key if api_key is not None else API_KEY if not key_to_use: logger.error("No API key provided for request to: %s", url) return { "error": True, "message": "API key is required. Set API_KEY env var or pass api_key", } auth = httpx.BasicAuth("API_KEY", key_to_use) full_url = f"{INTERVALS_API_BASE_URL}{url}" try: if method == "POST" and data is not None: response = await httpx_client.request( method=method, url=full_url, headers=headers, params=params, auth=auth, timeout=30.0, content=json.dumps(data), ) else: response = await httpx_client.request( method=method, url=full_url, headers=headers, params=params, auth=auth, timeout=30.0, ) try: response_data = response.json() if response.content else {} except JSONDecodeError: logger.error("Invalid JSON in response from: %s", full_url) return {"error": True, "message": "Invalid JSON in response"} _ = response.raise_for_status() return response_data except httpx.HTTPStatusError as e: error_code = e.response.status_code error_text = e.response.text logger.error("HTTP error: %s - %s", error_code, error_text) return { "error": True, "status_code": error_code, "message": _get_error_message(error_code, error_text), } except httpx.RequestError as e: logger.error("Request error: %s", str(e)) return {"error": True, "message": f"Request error: {str(e)}"} except httpx.HTTPError as e: logger.error("HTTP client error: %s", str(e)) return {"error": True, "message": f"HTTP client error: {str(e)}"}
  • Supporting utility function that formats raw activity data into a detailed, human-readable multi-line string summary, called within the get_activity_details handler.
    def format_activity_summary(activity: dict[str, Any]) -> str: """Format an activity into a readable string.""" start_time = activity.get("startTime", activity.get("start_date", "Unknown")) if isinstance(start_time, str) and len(start_time) > 10: # Format datetime if it's a full ISO string try: dt = datetime.fromisoformat(start_time.replace("Z", "+00:00")) start_time = dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: pass rpe = activity.get("perceived_exertion", None) if rpe is None: rpe = activity.get("icu_rpe", "N/A") if isinstance(rpe, (int, float)): rpe = f"{rpe}/10" feel = activity.get("feel", "N/A") if isinstance(feel, int): feel = f"{feel}/5" return f""" Activity: {activity.get("name", "Unnamed")} ID: {activity.get("id", "N/A")} Type: {activity.get("type", "Unknown")} Date: {start_time} Description: {activity.get("description", "N/A")} Distance: {activity.get("distance", 0)} meters Duration: {activity.get("duration", activity.get("elapsed_time", 0))} seconds Moving Time: {activity.get("moving_time", "N/A")} seconds Elevation Gain: {activity.get("elevationGain", activity.get("total_elevation_gain", 0))} meters Elevation Loss: {activity.get("total_elevation_loss", "N/A")} meters Power Data: Average Power: {activity.get("avgPower", activity.get("icu_average_watts", activity.get("average_watts", "N/A")))} watts Weighted Avg Power: {activity.get("icu_weighted_avg_watts", "N/A")} watts Training Load: {activity.get("trainingLoad", activity.get("icu_training_load", "N/A"))} FTP: {activity.get("icu_ftp", "N/A")} watts Kilojoules: {activity.get("icu_joules", "N/A")} Intensity: {activity.get("icu_intensity", "N/A")} Power:HR Ratio: {activity.get("icu_power_hr", "N/A")} Variability Index: {activity.get("icu_variability_index", "N/A")} Heart Rate Data: Average Heart Rate: {activity.get("avgHr", activity.get("average_heartrate", "N/A"))} bpm Max Heart Rate: {activity.get("max_heartrate", "N/A")} bpm LTHR: {activity.get("lthr", "N/A")} bpm Resting HR: {activity.get("icu_resting_hr", "N/A")} bpm Decoupling: {activity.get("decoupling", "N/A")} Other Metrics: Cadence: {activity.get("average_cadence", "N/A")} rpm Calories: {activity.get("calories", "N/A")} Average Speed: {activity.get("average_speed", "N/A")} m/s Max Speed: {activity.get("max_speed", "N/A")} m/s Average Stride: {activity.get("average_stride", "N/A")} L/R Balance: {activity.get("avg_lr_balance", "N/A")} Weight: {activity.get("icu_weight", "N/A")} kg RPE: {rpe} Session RPE: {activity.get("session_rpe", "N/A")} Feel: {feel} Environment: Trainer: {activity.get("trainer", "N/A")} Average Temp: {activity.get("average_temp", "N/A")}°C Min Temp: {activity.get("min_temp", "N/A")}°C Max Temp: {activity.get("max_temp", "N/A")}°C Avg Wind Speed: {activity.get("average_wind_speed", "N/A")} km/h Headwind %: {activity.get("headwind_percent", "N/A")}% Tailwind %: {activity.get("tailwind_percent", "N/A")}% Training Metrics: Fitness (CTL): {activity.get("icu_ctl", "N/A")} Fatigue (ATL): {activity.get("icu_atl", "N/A")} TRIMP: {activity.get("trimp", "N/A")} Polarization Index: {activity.get("polarization_index", "N/A")} Power Load: {activity.get("power_load", "N/A")} HR Load: {activity.get("hr_load", "N/A")} Pace Load: {activity.get("pace_load", "N/A")} Efficiency Factor: {activity.get("icu_efficiency_factor", "N/A")} Device Info: Device: {activity.get("device_name", "N/A")} Power Meter: {activity.get("power_meter", "N/A")} File Type: {activity.get("file_type", "N/A")} """

Other Tools

Related Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mvilanova/intervals-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server