download-fit
Download the original FIT file for a Garmin Connect activity. Returns the saved file path.
Instructions
Download the original FIT file for an activity. Returns the file path.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| activityId | Yes | The activity ID | |
| outputDir | No | Directory to save the FIT file | ./fit_files |
Implementation Reference
- src/tools.ts:287-313 (handler)The main handler for the 'download-fit' tool. Downloads the FIT file from Garmin Connect API, extracts it from a zip archive, and saves it to disk.
async ({ activityId, outputDir }) => { const client = getClient(); const zipBytes = await client.getBytes( `download-service/files/activity/${activityId}` ); mkdirSync(outputDir, { recursive: true }); // The response is a zip containing the .fit file // Use a minimal zip extraction (ZIP local file header parsing) const fitFile = extractFitFromZip(zipBytes, activityId); if (fitFile) { const outPath = join(outputDir, fitFile.name); writeFileSync(outPath, fitFile.data); return textResult( `Downloaded FIT file: ${outPath} (${fitFile.data.length} bytes)` ); } // Fallback: save the raw zip const zipPath = join(outputDir, `${activityId}.zip`); writeFileSync(zipPath, zipBytes); return textResult( `No .fit file found in archive. Saved raw zip: ${zipPath}` ); } - src/tools.ts:280-286 (schema)Input schema for download-fit: requires activityId (string) and optional outputDir (string, default './fit_files').
{ activityId: z.string().describe("The activity ID"), outputDir: z .string() .default("./fit_files") .describe("Directory to save the FIT file"), }, - src/tools.ts:277-314 (registration)Registration of the 'download-fit' tool on the MCP server using server.tool().
server.tool( "download-fit", "Download the original FIT file for an activity. Returns the file path.", { activityId: z.string().describe("The activity ID"), outputDir: z .string() .default("./fit_files") .describe("Directory to save the FIT file"), }, async ({ activityId, outputDir }) => { const client = getClient(); const zipBytes = await client.getBytes( `download-service/files/activity/${activityId}` ); mkdirSync(outputDir, { recursive: true }); // The response is a zip containing the .fit file // Use a minimal zip extraction (ZIP local file header parsing) const fitFile = extractFitFromZip(zipBytes, activityId); if (fitFile) { const outPath = join(outputDir, fitFile.name); writeFileSync(outPath, fitFile.data); return textResult( `Downloaded FIT file: ${outPath} (${fitFile.data.length} bytes)` ); } // Fallback: save the raw zip const zipPath = join(outputDir, `${activityId}.zip`); writeFileSync(zipPath, zipBytes); return textResult( `No .fit file found in archive. Saved raw zip: ${zipPath}` ); } ); - src/tools.ts:986-1047 (helper)Helper function extractFitFromZip() that parses a ZIP archive's central directory to find and extract the .fit file, supporting both stored (method 0) and deflated (method 8) compression.
function extractFitFromZip( buf: Buffer, activityId: string ): { name: string; data: Buffer } | null { // Find End of Central Directory (EOCD): PK\x05\x06 let eocdOffset = buf.length - 22; while (eocdOffset >= 0) { if ( buf[eocdOffset] === 0x50 && buf[eocdOffset + 1] === 0x4b && buf[eocdOffset + 2] === 0x05 && buf[eocdOffset + 3] === 0x06 ) break; eocdOffset--; } if (eocdOffset < 0) return null; const cdOffset = buf.readUInt32LE(eocdOffset + 16); const cdEntries = buf.readUInt16LE(eocdOffset + 10); // Walk central directory entries: PK\x01\x02 let pos = cdOffset; for (let i = 0; i < cdEntries; i++) { if ( buf[pos] !== 0x50 || buf[pos + 1] !== 0x4b || buf[pos + 2] !== 0x01 || buf[pos + 3] !== 0x02 ) break; const method = buf.readUInt16LE(pos + 10); const compressedSize = buf.readUInt32LE(pos + 20); const uncompressedSize = buf.readUInt32LE(pos + 24); const nameLength = buf.readUInt16LE(pos + 28); const extraLength = buf.readUInt16LE(pos + 30); const commentLength = buf.readUInt16LE(pos + 32); const localHeaderOffset = buf.readUInt32LE(pos + 42); const name = buf.toString("utf-8", pos + 46, pos + 46 + nameLength); if (name.endsWith(".fit")) { // Read local header to find data start const localNameLen = buf.readUInt16LE(localHeaderOffset + 26); const localExtraLen = buf.readUInt16LE(localHeaderOffset + 28); const dataStart = localHeaderOffset + 30 + localNameLen + localExtraLen; if (method === 0) { const data = buf.subarray(dataStart, dataStart + uncompressedSize); return { name: `${activityId}.fit`, data: Buffer.from(data) }; } if (method === 8) { const compressed = buf.subarray(dataStart, dataStart + compressedSize); const data = inflateRawSync(compressed); return { name: `${activityId}.fit`, data }; } } pos += 46 + nameLength + extraLength + commentLength; } return null; }