get-athlete-zones
Retrieve heart rate and power zones for the authenticated athlete, providing both a formatted summary and raw JSON data. Integrates with the Strava MCP Server for seamless data access.
Instructions
Retrieves the authenticated athlete's configured heart rate and power zones.
Output includes both a formatted summary and the raw JSON data.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/tools/getAthleteZones.ts:59-113 (handler)MCP tool definition and handler (execute function) for 'get-athlete-zones'. Fetches zones using Strava client, formats output with summary and raw JSON, handles auth and errors.export const getAthleteZonesTool = { name, description: description + "\n\nOutput includes both a formatted summary and the raw JSON data.", inputSchema, execute: async (_input: GetAthleteZonesInput) => { const token = process.env.STRAVA_ACCESS_TOKEN; if (!token) { console.error("Missing STRAVA_ACCESS_TOKEN environment variable."); return { content: [{ type: "text" as const, text: "Configuration error: Missing Strava access token." }], isError: true }; } try { console.error("Fetching athlete zones..."); const zonesData = await fetchAthleteZones(token); // Format the summary const formattedText = formatAthleteZones(zonesData); // Prepare the raw data const rawDataText = `\n\nRaw Athlete Zone Data:\n${JSON.stringify(zonesData, null, 2)}`; console.error("Successfully fetched athlete zones."); // Return both summary and raw data return { content: [ { type: "text" as const, text: formattedText }, { type: "text" as const, text: rawDataText } ] }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); console.error(`Error fetching athlete zones: ${errorMessage}`); let userFriendlyMessage; // Check for common errors like missing scope (403 Forbidden) if (errorMessage.includes("403")) { userFriendlyMessage = "🔒 Access denied. This tool requires 'profile:read_all' permission. Please re-authorize with the correct scope."; } else if (errorMessage.startsWith("SUBSCRIPTION_REQUIRED:")) { // In case Strava changes this later userFriendlyMessage = `🔒 Accessing zones might require a Strava subscription. Details: ${errorMessage}`; } else { userFriendlyMessage = `An unexpected error occurred while fetching athlete zones. Details: ${errorMessage}`; } return { content: [{ type: "text" as const, text: `❌ ${userFriendlyMessage}` }], isError: true }; } } };
- src/server.ts:156-161 (registration)Registers the 'get-athlete-zones' tool with the MCP server using server.tool().server.tool( getAthleteZonesTool.name, getAthleteZonesTool.description, getAthleteZonesTool.inputSchema?.shape ?? {}, getAthleteZonesTool.execute );
- src/stravaClient.ts:1238-1243 (schema)Zod schema (AthleteZonesSchema) and type (StravaAthleteZones) for validating the Strava /athlete/zones API response used by the tool.const AthleteZonesSchema = z.object({ heart_rate: HeartRateZoneSchema.optional(), // Heart rate zones might not be set power: PowerZoneSchema.optional(), // Power zones might not be set }); export type StravaAthleteZones = z.infer<typeof AthleteZonesSchema>;
- src/stravaClient.ts:1250-1277 (helper)Low-level helper function that makes the Strava API call to /athlete/zones, validates with Zod schema, and handles errors/token refresh. Called by the tool handler.export async function getAthleteZones(accessToken: string): Promise<StravaAthleteZones> { if (!accessToken) { throw new Error("Strava access token is required."); } try { const response = await stravaApi.get<unknown>("/athlete/zones", { headers: { Authorization: `Bearer ${accessToken}` }, }); const validationResult = AthleteZonesSchema.safeParse(response.data); if (!validationResult.success) { console.error(`Strava API validation failed (getAthleteZones):`, validationResult.error); throw new Error(`Invalid data format received from Strava API: ${validationResult.error.message}`); } return validationResult.data; } catch (error) { // Note: This endpoint requires profile:read_all scope // Handle potential 403 Forbidden if scope is missing, or 402 if it becomes sub-only? return await handleApiError<StravaAthleteZones>(error, `getAthleteZones`, async () => { // Use new token from environment after refresh const newToken = process.env.STRAVA_ACCESS_TOKEN!; return getAthleteZones(newToken); }); } }
- src/tools/getAthleteZones.ts:28-57 (helper)Helper function to format the athlete zones data into a readable markdown summary string, used in the tool handler.function formatAthleteZones(zonesData: StravaAthleteZones): string { let responseText = "**Athlete Zones:**\n"; if (zonesData.heart_rate) { responseText += "\n❤️ **Heart Rate Zones**\n"; responseText += ` Custom Zones: ${zonesData.heart_rate.custom_zones ? 'Yes' : 'No'}\n`; zonesData.heart_rate.zones.forEach((zone, index) => { responseText += ` Zone ${index + 1}: ${formatZoneRange(zone)} bpm\n`; }); if (zonesData.heart_rate.distribution_buckets) { responseText += " Time Distribution:\n" + formatDistribution(zonesData.heart_rate.distribution_buckets) + "\n"; } } else { responseText += "\n❤️ Heart Rate Zones: Not configured\n"; } if (zonesData.power) { responseText += "\n⚡ **Power Zones**\n"; zonesData.power.zones.forEach((zone, index) => { responseText += ` Zone ${index + 1}: ${formatZoneRange(zone)} W\n`; }); if (zonesData.power.distribution_buckets) { responseText += " Time Distribution:\n" + formatDistribution(zonesData.power.distribution_buckets) + "\n"; } } else { responseText += "\n⚡ Power Zones: Not configured\n"; } return responseText; }