Skip to main content
Glama

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

TableJSON Schema
NameRequiredDescriptionDefault
yearYesYear (e.g. 2026)
monthYesMonth 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);
    });
  • 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);
        }
      );
  • 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)"),
    },
  • 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));
      },
    },
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

No annotations are provided, so the description must convey behavioral traits. It only states the purpose without explicitly declaring the tool as read-only, mentioning authentication requirements, or describing any side effects. The verb 'Get' weakly implies read-only, but full transparency is lacking.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, front-loaded sentence of six words with no redundant or extraneous information. Every word contributes to the purpose, making it highly concise and easy to parse.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description mentions the output contains 'activities, workouts, and events', which gives basic context, but since there is no output schema, it lacks detail on the exact structure (e.g., list of days, event objects). It is adequate for a simple tool but not fully complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema has 100% description coverage for both parameters (year and month), so the description adds no extra meaning beyond what the schema already provides. According to rules, baseline score is 3 when schema coverage exceeds 80%.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description states 'Get monthly calendar with activities, workouts, and events', using a specific verb ('Get') and resource ('monthly calendar'). It clearly distinguishes itself from siblings like 'get-activity' or 'get-sleep' by aggregating multiple data types into a calendar view.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description does not provide any guidance on when to use this tool versus alternatives like 'list-activities' or 'get-activity'. It implies a monthly overview use case but offers no explicit when-to or when-not-to instructions.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

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/etweisberg/garmin-connect-mcp'

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