Skip to main content
Glama
mcp-server.ts8.09 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import { GarminAuth } from "./garmin-auth"; import { createGarminWorkout } from "./garmin-workout-creator"; // Types for workout parsing interface WorkoutStep { name: string; duration: string; target: string; intensity: 'warmup' | 'active' | 'rest' | 'cooldown'; notes?: string; } interface WorkoutData { name: string; sport: 'running' | 'cycling' | 'swimming'; steps: WorkoutStep[]; } const server = new Server( { name: "garmin-workouts", version: "1.0.0", }, { capabilities: { tools: {}, }, } ); // Initialize auth manager const garminAuth = new GarminAuth(); server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "create_garmin_workout", description: "Create a workout in Garmin Connect from structured workout data. Claude should parse the natural language description and pass structured workout steps.", inputSchema: { type: "object", properties: { name: { type: "string", description: "Name of the workout", }, sport: { type: "string", enum: ["running", "cycling", "swimming"], description: "Sport type (defaults to running)", }, steps: { type: "array", description: "Array of workout steps", items: { type: "object", properties: { name: { type: "string", description: "Step name (e.g., 'Warm-up', 'Sprint 1', 'Recovery 1')", }, duration: { type: "string", description: "Duration in MM:SS format (e.g., '10:00') or distance (e.g., '1.5 km')", }, target: { type: "string", description: "Target zone or intensity. Use 'Zone 1' for recovery/easy, 'Zone 2' for aerobic/base, 'Zone 3' for tempo, 'Zone 4' for threshold, 'Zone 5' for VO2 max/all-out efforts. IMPORTANT: Default warmup and cooldown steps to 'Zone 2' unless user specifically requests otherwise. Use 'Open' only when no specific target is mentioned.", }, intensity: { type: "string", enum: ["warmup", "active", "rest", "cooldown"], description: "Step intensity type", }, }, required: ["name", "duration", "target", "intensity"], }, }, }, required: ["name", "sport", "steps"], }, }, { name: "check_garmin_auth", description: "Check if Garmin authentication is valid", inputSchema: { type: "object", properties: {}, }, }, { name: "authenticate_garmin", description: "Authenticate with Garmin Connect (opens browser)", inputSchema: { type: "object", properties: {}, }, }, ], }; }); server.setRequestHandler(CallToolRequestSchema, async (request: any) => { const { name, arguments: args } = request.params; switch (name) { case "create_garmin_workout": { const { name: workoutName, sport = "running", steps } = args as { name: string; sport?: string; steps: WorkoutStep[]; }; try { // Check auth first const authData = await garminAuth.getValidAuth(); if (!authData) { return { content: [ { type: "text", text: "❌ Authentication required. Please run the 'authenticate_garmin' tool to authenticate with Garmin Connect.", }, ], }; } // Create workout data from structured input const workoutData: WorkoutData = { name: workoutName, sport: sport as 'running' | 'cycling' | 'swimming', steps }; // Create LLM parser that just returns the structured data const llmParser = async () => workoutData; // Create workout with structured data const result = await createGarminWorkout( workoutName, // description not needed anymore authData.authToken, authData.cookies, workoutName, sport, llmParser ); if (result.success) { return { content: [ { type: "text", text: `✅ Workout created successfully!\n\n**${result.name}** (ID: ${result.workoutId})\n\n🔗 **View in Garmin Connect:** ${result.url}\n\nThe workout is now available in your Garmin Connect account and ready to sync to your device.`, }, ], }; } else { return { content: [ { type: "text", text: `❌ Failed to create workout: ${result.error}`, }, ], }; } } catch (error) { return { content: [ { type: "text", text: `❌ Error creating workout: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } case "check_garmin_auth": { try { const authData = await garminAuth.getValidAuth(); if (authData) { const expiresAt = new Date(authData.expiresAt); return { content: [ { type: "text", text: `✅ Garmin authentication is valid and ready to use.\nToken expires: ${expiresAt.toLocaleString()}`, }, ], }; } else { return { content: [ { type: "text", text: "❌ No valid Garmin authentication found. Please run the 'authenticate_garmin' tool to authenticate.", }, ], }; } } catch (error) { return { content: [ { type: "text", text: `❌ Authentication error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } case "authenticate_garmin": { try { console.error("🔐 Starting Garmin authentication..."); const authData = await garminAuth.authenticate(); if (authData) { const expiresAt = new Date(authData.expiresAt); return { content: [ { type: "text", text: `✅ Successfully authenticated with Garmin Connect! You can now create workouts.\nToken expires: ${expiresAt.toLocaleString()}`, }, ], }; } else { return { content: [ { type: "text", text: "❌ Authentication failed. Please ensure you can access Garmin Connect in your browser.", }, ], }; } } catch (error) { return { content: [ { type: "text", text: `❌ Authentication error: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } default: throw new Error(`Unknown tool: ${name}`); } }); async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("Garmin Workouts MCP Server running on stdio"); } main().catch((error) => { console.error("Server error:", error); process.exit(1); });

Implementation Reference

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/charlesfrisbee/garmin-workouts-mcp'

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