get-calendar
Fetch a monthly calendar displaying your Garmin Connect activities, workouts, and events by specifying a year and month.
Instructions
Get monthly calendar with activities, workouts, and events
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| year | Yes | Year (e.g. 2026) | |
| month | Yes | Month number 0-11 (0=January, 11=December) |
Implementation Reference
- src/index.ts:1-35 (registration)Entry point that imports registerTools from tools.ts and calls it to register all tools including get-calendar on the McpServer.
#!/usr/bin/env node import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { registerTools, registerResources } from "./tools.js"; async function startMcpServer(): Promise<void> { const server = new McpServer({ name: "garmin-connect-mcp", version: "0.1.0", }); registerTools(server); registerResources(server); const transport = new StdioServerTransport(); await server.connect(transport); console.error("garmin-connect-mcp server running on stdio"); } async function main(): Promise<void> { const command = process.argv[2]; if (command === "login") { const { runLogin } = await import("./auth.js"); await runLogin(); } else { await startMcpServer(); } } main().catch((err) => { console.error("Fatal error:", err); process.exit(1); }); - src/tools.ts:624-638 (handler)The handler for the 'get-calendar' tool. It accepts 'year' and 'month' parameters, calls the Garmin Connect calendar-service API, and returns the result as JSON.
server.tool( "get-calendar", "Get monthly calendar with activities, workouts, and events", { year: z.number().describe("Year (e.g. 2026)"), month: z.number().describe("Month number 0-11 (0=January, 11=December)"), }, async ({ year, month }) => { const client = getClient(); const data = await client.get( `calendar-service/year/${year}/month/${month}` ); return jsonResult(data); } ); - src/tools.ts:41-638 (registration)The registerTools function that registers all tools via server.tool(). The get-calendar tool is registered at line 624-638.
export function registerTools(server: McpServer): void { // ── garmin-login ──────────────────────────────────────────────────── server.tool( "garmin-login", "Returns step-by-step instructions for authenticating with Garmin Connect. Requires the Playwright MCP server to be installed. After following these steps, ALWAYS call the check-session tool to verify the login worked.", {}, async () => { const sessionFile = getSessionFile(); return textResult( `# Garmin Connect Login To authenticate, you need the Playwright MCP server installed (\`@playwright/mcp\`). ## Steps (execute these in order): 1. **Open Garmin Connect** using the Playwright MCP browser_navigate tool: \`\`\` browser_navigate → https://connect.garmin.com/app/activities \`\`\` 2. **Tell the user** to log in to Garmin Connect in the browser window that opened. Wait for them to confirm they are logged in and can see their activities. 3. **Navigate to the activities page** (the login may redirect elsewhere): \`\`\` browser_navigate → https://connect.garmin.com/app/activities \`\`\` 4. **Extract the CSRF token** using browser_evaluate (NOT browser_run_code — the meta tag needs the page to be fully rendered): \`\`\`javascript () => { const meta = document.querySelector('meta[name="csrf-token"]'); return meta ? meta.getAttribute('content') : 'NOT_FOUND'; } \`\`\` Save this value — you'll need it in step 6. 5. **Extract cookies** using browser_run_code: \`\`\`javascript async (page) => { const cookies = await page.context().cookies(); const garminCookies = cookies .filter(c => c.domain && c.domain.includes('garmin')) .map(c => ({ name: c.name, value: c.value, domain: c.domain })); return JSON.stringify(garminCookies); } \`\`\` 6. **Write the session file** to: ${sessionFile} - Create the directory \`~/.garmin-connect-mcp/\` if it doesn't exist (mkdir -p) - Combine the CSRF token from step 4 and cookies from step 5 into: \`{ "csrf_token": "<from step 4>", "cookies": <from step 5> }\` - Write this JSON to the session file 7. **IMPORTANT: Call the \`check-session\` tool** to verify the login worked. ## Notes - Session cookies expire after a few hours — re-run this flow when they do. - The Playwright browser must stay open during steps 4-5 (don't close it before extracting). ` ); } ); // ── check-session ────────────────────────────────────────────────── server.tool( "check-session", "Check if the saved Garmin Connect session is still valid. MUST be called after garmin-login to verify authentication worked.", {}, async () => { if (!sessionExists()) { return errorResult( "No session file found. Call the garmin-login tool for instructions." ); } try { const client = getClient(); const profile = await client.get( "userprofile-service/userprofile/user-settings/" ); return jsonResult({ status: "ok", profile }); } catch (e) { const msg = e instanceof Error ? e.message : String(e); // Reset the singleton so the next attempt re-reads the session file await resetSharedClient(); return errorResult( `Session invalid or expired: ${msg}\nCall the garmin-login tool to re-authenticate.` ); } } ); // ── list-activities ──────────────────────────────────────────────── server.tool( "list-activities", "List your Garmin Connect activities with pagination", { limit: z .number() .default(20) .describe("Max activities to return (1-100)"), start: z.number().default(0).describe("Pagination offset"), }, async ({ limit, start }) => { const client = getClient(); const data = await client.get( "activitylist-service/activities/search/activities", { limit, start } ); return jsonResult(data); } ); // ── get-activity ─────────────────────────────────────────────────── server.tool( "get-activity", "Get full activity summary (name, type, distance, duration, HR, calories, etc.)", { activityId: z.string().describe("The activity ID"), }, async ({ activityId }) => { const client = getClient(); const data = await client.get(`activity-service/activity/${activityId}`); return jsonResult(data); } ); // ── get-activity-details ─────────────────────────────────────────── server.tool( "get-activity-details", "Get time-series metrics for an activity (HR, cadence, elevation, pace over time)", { activityId: z.string().describe("The activity ID"), maxChartSize: z .number() .default(10000) .describe("Max data points to return"), }, async ({ activityId, maxChartSize }) => { const client = getClient(); const data = await client.get( `activity-service/activity/${activityId}/details`, { maxChartSize, maxPolylineSize: 0, maxHeatMapSize: 2000 } ); return jsonResult(data); } ); // ── get-activity-splits ──────────────────────────────────────────── server.tool( "get-activity-splits", "Get lap/split data for an activity", { activityId: z.string().describe("The activity ID"), }, async ({ activityId }) => { const client = getClient(); const data = await client.get( `activity-service/activity/${activityId}/splits` ); return jsonResult(data); } ); // ── get-activity-hr-zones ────────────────────────────────────────── server.tool( "get-activity-hr-zones", "Get heart rate time-in-zone breakdown for an activity", { activityId: z.string().describe("The activity ID"), }, async ({ activityId }) => { const client = getClient(); const data = await client.get( `activity-service/activity/${activityId}/hrTimeInZones` ); return jsonResult(data); } ); // ── get-activity-polyline ────────────────────────────────────────── server.tool( "get-activity-polyline", "Get full-resolution GPS track/polyline for an activity", { activityId: z.string().describe("The activity ID"), }, async ({ activityId }) => { const client = getClient(); const data = await client.get( `activity-service/activity/${activityId}/polyline/full-resolution/` ); return jsonResult(data); } ); // ── get-activity-weather ─────────────────────────────────────────── server.tool( "get-activity-weather", "Get weather conditions during an activity", { activityId: z.string().describe("The activity ID"), }, async ({ activityId }) => { const client = getClient(); const data = await client.get( `activity-service/activity/${activityId}/weather` ); return jsonResult(data); } ); // ── get-user-profile ─────────────────────────────────────────────── server.tool( "get-user-profile", "Get your Garmin Connect user profile and settings", {}, async () => { const client = getClient(); const data = await client.get( "userprofile-service/userprofile/user-settings/" ); return jsonResult(data); } ); // ── download-fit ─────────────────────────────────────────────────── 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}` ); } ); // ══════════════════════════════════════════════════════════════════ // Daily Health // ══════════════════════════════════════════════════════════════════ server.tool( "get-daily-summary", "Get daily summary: steps, calories, distance, intensity minutes, floors, etc.", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const displayName = await client.getDisplayName(); const data = await client.get( `usersummary-service/usersummary/daily/${displayName}`, { calendarDate: d } ); return jsonResult(data); } ); server.tool( "get-daily-heart-rate", "Get heart rate data throughout the day (resting HR, HR timeline)", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get( "wellness-service/wellness/dailyHeartRate", { date: d } ); return jsonResult(data); } ); server.tool( "get-daily-stress", "Get stress level data throughout the day", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get( `wellness-service/wellness/dailyStress/${d}` ); return jsonResult(data); } ); server.tool( "get-daily-summary-chart", "Get daily wellness summary chart data (combined health metrics)", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get( "wellness-service/wellness/dailySummaryChart/", { date: d } ); return jsonResult(data); } ); server.tool( "get-daily-intensity-minutes", "Get intensity minutes earned for a date", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get(`wellness-service/wellness/daily/im/${d}`); return jsonResult(data); } ); server.tool( "get-daily-movement", "Get daily movement/activity data", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get("wellness-service/wellness/dailyMovement", { calendarDate: d, }); return jsonResult(data); } ); server.tool( "get-daily-respiration", "Get respiration rate data for a date", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get( `wellness-service/wellness/daily/respiration/${d}` ); return jsonResult(data); } ); // ══════════════════════════════════════════════════════════════════ // Sleep, Body Battery, HRV // ══════════════════════════════════════════════════════════════════ server.tool( "get-sleep", "Get sleep data: score, duration, stages, SpO2, HRV during sleep", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get("sleep-service/sleep/dailySleepData", { date: d, nonSleepBufferMinutes: 60, }); return jsonResult(data); } ); server.tool( "get-body-battery", "Get today's body battery charged/drained values", {}, async () => { const client = getClient(); const data = await client.get( "wellness-service/wellness/bodyBattery/messagingToday" ); return jsonResult(data); } ); server.tool( "get-hrv", "Get heart rate variability (HRV) data for a date", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get(`hrv-service/hrv/${d}`); return jsonResult(data); } ); // ══════════════════════════════════════════════════════════════════ // Weight // ══════════════════════════════════════════════════════════════════ server.tool( "get-weight", "Get weight measurements over a date range", { startDate: z.string().describe("Start date YYYY-MM-DD"), endDate: z.string().describe("End date YYYY-MM-DD"), }, async ({ startDate, endDate }) => { const client = getClient(); const data = await client.get( `weight-service/weight/range/${startDate}/${endDate}`, { includeAll: "true" } ); return jsonResult(data); } ); // ══════════════════════════════════════════════════════════════════ // Personal Records // ══════════════════════════════════════════════════════════════════ server.tool( "get-personal-records", "Get all personal records with history (fastest mile, longest run, etc.)", {}, async () => { const client = getClient(); const displayName = await client.getDisplayName(); const data = await client.get( `personalrecord-service/personalrecord/prs/${displayName}`, { includeHistory: "true" } ); return jsonResult(data); } ); // ══════════════════════════════════════════════════════════════════ // Fitness Stats / Reports // ══════════════════════════════════════════════════════════════════ server.tool( "get-fitness-stats", "Get aggregated fitness stats by activity type over a date range", { startDate: z.string().describe("Start date YYYY-MM-DD"), endDate: z.string().describe("End date YYYY-MM-DD"), aggregation: z .string() .default("daily") .describe("Aggregation period: daily, weekly, monthly"), metric: z .string() .default("duration") .describe("Metric: duration, distance, calories"), }, async ({ startDate, endDate, aggregation, metric }) => { const client = getClient(); const data = await client.get("fitnessstats-service/activity", { aggregation, startDate, endDate, groupByActivityType: "true", standardizedUnits: "true", groupByParentActivityType: "false", userFirstDay: "sunday", metric, }); return jsonResult(data); } ); server.tool( "get-vo2max", "Get latest VO2 Max / fitness level estimate", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get( `metrics-service/metrics/maxmet/latest/${d}` ); return jsonResult(data); } ); server.tool( "get-hr-zones-config", "Get your configured heart rate zone boundaries", {}, async () => { const client = getClient(); const data = await client.get("biometric-service/heartRateZones/"); return jsonResult(data); } ); // ══════════════════════════════════════════════════════════════════ // Training & Recovery // ══════════════════════════════════════════════════════════════════ server.tool( "get-training-readiness", "Get training readiness score for a date (based on sleep, recovery, training load)", { date: z.string().optional().describe("YYYY-MM-DD, defaults to today"), }, async ({ date }) => { const client = getClient(); const d = date ?? todayDate(); const data = await client.get( `metrics-service/metrics/trainingreadiness/${d}` ); return jsonResult(data); } ); server.tool( "get-sleep-stats", "Get sleep statistics over a date range (averages, trends)", { startDate: z.string().describe("Start date YYYY-MM-DD"), endDate: z.string().describe("End date YYYY-MM-DD"), }, async ({ startDate, endDate }) => { const client = getClient(); const data = await client.get( `sleep-service/stats/sleep/daily/${startDate}/${endDate}` ); return jsonResult(data); } ); // ══════════════════════════════════════════════════════════════════ // Calendar, Goals, Badges // ══════════════════════════════════════════════════════════════════ server.tool( "get-calendar", "Get monthly calendar with activities, workouts, and events", { year: z.number().describe("Year (e.g. 2026)"), month: z.number().describe("Month number 0-11 (0=January, 11=December)"), }, async ({ year, month }) => { const client = getClient(); const data = await client.get( `calendar-service/year/${year}/month/${month}` ); return jsonResult(data); } ); - src/tools.ts:627-630 (schema)Input schema for get-calendar: year (number) and month (number, 0-11) defined using Zod.
{ year: z.number().describe("Year (e.g. 2026)"), month: z.number().describe("Month number 0-11 (0=January, 11=December)"), }, - src/test.ts:449-459 (helper)Test harness for the get-calendar tool, calling it with year=2026, month=2.
// ── Calendar, Goals, Badges ──────────────────────────────────────── { name: "get-calendar", run: async (server) => { const result = await callTool(server, "get-calendar", { year: 2026, month: 2, }); if (result.isError) throw new Error(getToolText(result)); }, },