download-workout-fit
Download a workout from Garmin Connect as a FIT file by providing the workout ID. Optionally specify a directory to save the file.
Instructions
Download a workout as a FIT file
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| workoutId | Yes | The workout ID | |
| outputDir | No | Directory to save the FIT file | ./fit_files |
Implementation Reference
- src/tools.ts:757-768 (handler)The actual handler function for download-workout-fit tool. It calls getBytes on the Garmin API to fetch the FIT file as bytes, creates the output directory, writes the file, and returns a success message with the file path and byte count.
async ({ workoutId, outputDir }) => { const client = getClient(); const fitBytes = await client.getBytes( `workout-service/workout/FIT/${workoutId}` ); mkdirSync(outputDir, { recursive: true }); const outPath = join(outputDir, `workout_${workoutId}.fit`); writeFileSync(outPath, fitBytes); return textResult( `Downloaded workout FIT: ${outPath} (${fitBytes.length} bytes)` ); } - src/tools.ts:750-756 (schema)Zod schema for download-workout-fit inputs: workoutId (required string) and outputDir (optional string defaulting to './fit_files').
{ workoutId: z.string().describe("The workout ID"), outputDir: z .string() .default("./fit_files") .describe("Directory to save the FIT file"), }, - src/tools.ts:747-769 (registration)Registration of the 'download-workout-fit' tool on the MCP server using server.tool(), with name, description, schema, and handler.
server.tool( "download-workout-fit", "Download a workout as a FIT file", { workoutId: z.string().describe("The workout ID"), outputDir: z .string() .default("./fit_files") .describe("Directory to save the FIT file"), }, async ({ workoutId, outputDir }) => { const client = getClient(); const fitBytes = await client.getBytes( `workout-service/workout/FIT/${workoutId}` ); mkdirSync(outputDir, { recursive: true }); const outPath = join(outputDir, `workout_${workoutId}.fit`); writeFileSync(outPath, fitBytes); return textResult( `Downloaded workout FIT: ${outPath} (${fitBytes.length} bytes)` ); } ); - src/tools.ts:32-39 (helper)The getClient() helper that ensures a Garmin session exists before returning the shared GarminClient instance.
function getClient() { if (!sessionExists()) { throw new Error( "No Garmin session found. The user needs to run: npx garmin-connect-mcp login" ); } return getSharedClient(); } - src/garmin-client.ts:172-207 (helper)The getBytes() method on GarminClient that fetches binary data via page.evaluate, encodes it as base64 to cross the Playwright boundary, and returns a Node.js Buffer.
async getBytes(path: string): Promise<Buffer> { await this.init(); const url = `/gc-api/${path}`; const csrfToken = this.csrfToken; const result = await this.page.evaluate( async ({ url, csrfToken }: { url: string; csrfToken: string }) => { const resp = await fetch(url, { headers: { "connect-csrf-token": csrfToken, Accept: "*/*", }, }); if (!resp.ok) { return { status: resp.status, error: await resp.text(), data: null }; } const buf = await resp.arrayBuffer(); // Convert to base64 to pass through page.evaluate boundary const bytes = new Uint8Array(buf); let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return { status: resp.status, error: null, data: btoa(binary) }; }, { url, csrfToken } ); if (result.status !== 200 || !result.data) { throw new Error( `Garmin API ${result.status}: ${path} — ${result.error ?? ""}` ); } return Buffer.from(result.data, "base64"); }