Skip to main content
Glama
garmin-connect-mcp

garmin-connect-mcp

by Samiu1

garmin-connect-mcp

npm license CI

An MCP server for the Garmin Connect Personal API. Each user brings their own Personal API key, so the server holds no shared credentials and can be installed by anyone with a Garmin account.

Ten tools cover all 26 endpoints in the Personal API, plus a passthrough for anything Garmin adds later.

npx -y @samiul_i/garmin-connect-mcp

That needs a key first — see Get a Personal API key, then Install in a client.

Requirements

  • Node.js 20.3 or newer

  • A Garmin Connect Personal API key

Related MCP server: Garmin Coach MCP

Get a Personal API key

The Personal API gives you read access to your own Garmin Connect data with a key you issue yourself. There is no OAuth app to register and no approval process.

Before you start: you need a Garmin Connect account with at least one synced device. The key page exists only on Garmin Connect web — it is not in the mobile app.

Steps

  1. Sign in to Garmin Connect: https://connect.garmin.com

  2. Open the Personal API token page: https://connect.garmin.com/app/settings/personalApiToken In the web UI you can also reach it under Settings → Account → Personal API Token.

  3. Choose Create Key.

  4. Accept the terms. They cover personal, non-commercial use.

  5. Pick an expiration: 30 days, 90 days, 180 days, or one year. Longer means fewer rotations; shorter limits the damage if the key leaks.

  6. Choose Create.

  7. Copy the key immediately. It is shown once and cannot be retrieved afterwards. If you lose it, delete that key and create another.

  8. Store it — see Store your key. Do not paste it into an interactive terminal prompt; the warning there explains why.

  9. Check it works — see Verify the key works — before configuring any client.

What the key looks like

A URL-safe string of roughly 130 characters, containing dots. One real key measured exactly 133 characters.

That length is worth noting, because a truncated key is the hardest failure to diagnose: it has a valid shape, and Garmin rejects it with HTTP 401 and an empty body, which is identical to how it rejects an expired or deleted key. Always compare the number of characters you stored against the key you copied.

Expiry and rotation

Garmin emails a reminder about a week before a key expires. When it does, create a new one and re-store it — nothing else about your setup changes. Deleting a key invalidates it immediately, along with every access token issued from it.

Troubleshooting

What you see

Likely cause

HTTP 401, empty body, on the token request

Key truncated, expired, or deleted. Compare the stored length, and confirm a key is still listed on the token page.

HTTP 400 on the token request

No Authorization header reached Garmin.

HTTP 400 on a data request

A malformed date, or a range longer than that metric allows — see Undocumented date-range limits.

No key listed on the token page

It expired or was deleted. Create a new one.

Server exits at startup with "No Garmin Personal API key found"

None of the three sources supplied a key, or an MCP client did not pass the variable through to the server process.

Reference

Scope: the Personal API is for your own data. Garmin states it is not intended for commercial applications serving multiple end users; the developer program above covers that case.

Store your key

The server checks three sources, in this order:

  1. GARMIN_PERSONAL_API_KEY in the environment

  2. GARMIN_PERSONAL_API_KEY_COMMAND — a command whose stdout is the key

  3. ~/.config/garmin/connect-personal-api-key — the file holds the key alone; surrounding whitespace, including a trailing newline, is ignored

Personal API keys expire after 30 days to a year, so you will rotate this key on a schedule, not once. Pick a source that makes rotation one edit rather than one per client.

Option A: OS keychain or password manager (most secure)

The key stays encrypted at rest and never appears in a file you might commit.

Do not paste the key into an interactive prompt. In testing, security add-generic-password -w with no value stored only the first 128 characters of a 133-character key, silently. The result looks valid, has the right shape, and fails with HTTP 401 and an empty body — which is indistinguishable from an expired key. Store it from the clipboard instead, and verify the length round-tripped.

Copy the key from Garmin, then on macOS:

K=$(pbpaste | tr -d '\r\n'); security add-generic-password -a "$USER" -s garmin-personal-api -l "Garmin Connect Personal API" -U -w "$K"; S=$(security find-generic-password -s garmin-personal-api -w | tr -d '\n'); [ "$S" = "$K" ] && echo "stored ${#S} chars, round-trip OK" || echo "MISMATCH: clipboard ${#K}, stored ${#S}"; unset K S

That prints round-trip OK with the character count, or tells you the two lengths disagree. One caveat of passing the key as an argument this way: it is briefly visible to ps for other processes on the machine. On a single-user machine that is a small exposure; if it matters to you, use option B, which avoids it entirely.

Then point the server at it:

export GARMIN_PERSONAL_API_KEY_COMMAND="security find-generic-password -s garmin-personal-api -w"

The same hook works with any secret store that can print to stdout:

Store

Command

macOS Keychain

security find-generic-password -s garmin-personal-api -w

1Password CLI

op read "op://Private/Garmin/credential"

pass

pass show garmin/personal-api

Bitwarden CLI

bw get password garmin-personal-api

The command runs once per server start and its result is held in memory, so there is no subprocess or keychain prompt per request. Rotating the stored secret takes effect on the next restart. The command must print the key and nothing else; a failure or empty output stops startup with an explanation rather than starting a broken server.

Option B: key file (simplest)

One file, shared by every MCP client and by Garmin's official Python client, which reads the same path. Your client configuration contains no secret.

Copy the key from Garmin, then write it straight from the clipboard. This avoids both your shell history and any interactive prompt, and never passes the key as a command argument:

mkdir -p ~/.config/garmin && pbpaste | tr -d '\r\n' > ~/.config/garmin/connect-personal-api-key && chmod 600 ~/.config/garmin/connect-personal-api-key && echo "stored $(wc -c < ~/.config/garmin/connect-personal-api-key | tr -d ' ') chars, mode $(stat -f '%Lp' ~/.config/garmin/connect-personal-api-key)"

Check the character count against the key you copied. To rotate later, copy the new key and run the same command.

On Linux, replace pbpaste with xclip -o -selection clipboard or wl-paste, and stat -f '%Lp' with stat -c '%a'.

Avoid read -rs and other interactive prompts for this, for the reason given in option A: a prompt can truncate a long key without saying so.

On Windows, write the file from the clipboard with PowerShell, then confirm the length:

$dir = "$HOME\.config\garmin"; New-Item -ItemType Directory -Force -Path $dir | Out-Null; $k = (Get-Clipboard) -replace '\s',''; [IO.File]::WriteAllText("$dir\connect-personal-api-key", $k); "stored $($k.Length) chars"

Option C: environment variable

Best for a one-off run, a CI job, or a shell where you want to override the others temporarily:

GARMIN_PERSONAL_API_KEY=... npx -y @samiul_i/garmin-connect-mcp@1.0.0

Putting the key in an MCP client's env block works, since that reaches the server as this variable. It is the weakest option: it lands in plaintext in a config file that is routinely screenshotted and committed to dotfiles repositories, it has no guaranteed permissions, and it needs editing in every client each time the key expires.

Note that with options A and C, the MCP client must pass the variable through to the server process. Clients give the child a minimal environment, so set it in the client's env block rather than relying on your shell profile — export in .zshrc will not reach a server launched by a desktop app.

Whichever you choose: the server never logs the key, and warns on stderr if the key file or token cache is readable by other users.

Verify the key works

Do this before configuring a client, so a bad key fails in one obvious place rather than as a silent tool error later.

From the keychain (option A):

curl -sS -o /dev/null -w 'token exchange: HTTP %{http_code}\n' -X POST "https://personal.connectapi.garmin.com/personalapitoken-service/personal-api/personal-api-key/token" -H 'Accept: */*' -H "Authorization: ApiKey $(security find-generic-password -s garmin-personal-api -w | tr -d '\n')"

From the key file (option B):

curl -sS -o /dev/null -w 'token exchange: HTTP %{http_code}\n' -X POST "https://personal.connectapi.garmin.com/personalapitoken-service/personal-api/personal-api-key/token" -H 'Accept: */*' -H "Authorization: ApiKey $(tr -d '\n' < ~/.config/garmin/connect-personal-api-key)"

HTTP 200 means the key works and the server will too. HTTP 401 means Garmin rejected it — see Troubleshooting.

All environment variables

Variable

Default

Purpose

GARMIN_PERSONAL_API_KEY

—

The key itself (option C)

GARMIN_PERSONAL_API_KEY_COMMAND

—

A command printing the key (option A)

GARMIN_PERSONAL_API_BASE_URL

https://personal.connectapi.garmin.com

Set to https://personal.connectapi.garmin.cn for Garmin China accounts

Copy .env.example if you prefer a file. Never commit the key.

Install in a client

With your key in ~/.config/garmin/connect-personal-api-key, the configuration carries no secret:

{
  "mcpServers": {
    "garmin": {
      "command": "npx",
      "args": ["-y", "@samiul_i/garmin-connect-mcp@1.0.0"]
    }
  }
}

Where that goes:

Client

Location

Claude Desktop (macOS)

~/Library/Application Support/Claude/claude_desktop_config.json

Claude Desktop (Windows)

%APPDATA%\Claude\claude_desktop_config.json

Claude Code

.mcp.json in the project, or claude mcp add garmin -- npx -y @samiul_i/garmin-connect-mcp@1.0.0

Cursor

~/.cursor/mcp.json

Restart the client after editing, then ask it something like "what did my Garmin record yesterday?"

Pin the version rather than floating on @latest: npx -y <name> fetches the newest release every launch, and this server is handed a credential at startup. Pinning means a future release cannot receive your key until you choose to upgrade.

If you chose the keychain (option A), add the command instead. This is still a config file with no secret in it — only an instruction for fetching one:

{
  "mcpServers": {
    "garmin": {
      "command": "npx",
      "args": ["-y", "@samiul_i/garmin-connect-mcp@1.0.0"],
      "env": {
        "GARMIN_PERSONAL_API_KEY_COMMAND": "security find-generic-password -s garmin-personal-api -w"
      }
    }
  }
}

If you would rather paste the key itself, use "env": { "GARMIN_PERSONAL_API_KEY": "your-key-here" } and read the trade-offs in Store your key first.

What you can ask

Once it is connected, these all work as plain requests. Each maps onto one or two tool calls.

  • "How did I sleep last night?" → garmin_get_daily_health with metrics=["sleep"]

  • "Show my sleep trend this month" → garmin_get_health_trend, metric="sleep"

  • "What's my resting heart rate doing over the last three weeks?" → garmin_get_health_trend, metric="daily_summaries"

  • "List my runs in October" → garmin_search_activities with activity_type="running" and a date range

  • "Break down my longest ride this year" → garmin_search_activities sorted by distance, then garmin_get_activity

  • "Was yesterday a recovery day?" → garmin_get_daily_health with metrics=["summary","sleep","hrv"]

  • "Which heart-rate zone was my last race in?" → garmin_get_activity plus garmin_get_heart_rate_zones

  • "Save the GPS file from my last hike" → garmin_search_activities, then garmin_download_activity_file

Two things to expect, both deliberate:

  • Figures come back in Garmin's units — metres and seconds — regardless of your display preferences. Ask for your preferred units, or have the client call garmin_get_user_profile first.

  • Missing data is reported as missing, never as zero. If a metric your device does not record is requested, the response names it under metrics_unavailable rather than returning 0.

Develop

npm install
npm run build
npm run smoke
npm run dev
  • npm run build compiles to dist/, with dist/index.js as the entry point.

  • npm run smoke starts the built server and exercises it as a client. It passes without credentials, checking the tool manifest, argument validation, path safety, and the key-command failure modes. It prints which credential source it found, and if any of the three supplies a key it also makes live read-only calls: a month of each wide metric, the summary-versus-full size difference, and the date-range limits.

  • npm run dev runs from source with reload.

  • npx @modelcontextprotocol/inspector node dist/index.js opens an interactive UI.

Publish

prepublishOnly builds and runs the self-check, so a broken build cannot be published.

npm publish

Before the first publish:

  • The package publishes as @samiul_i/garmin-connect-mcp. Scoped packages default to restricted, so publishConfig.access is set to public in package.json; without it, npm publish fails or publishes privately.

  • Add repository, homepage, and bugs to package.json once the code is on a git host. npm renders them on the package page, and their absence is the most visible gap on a fresh listing.

  • npm whoami to confirm you are logged in.

Only dist/, README.md, and LICENSE ship. Verify with npm pack --dry-run.

Releasing from CI (preferred)

.github/workflows/release.yml publishes on a v* tag and adds npm provenance: a signed attestation tying the published tarball to this repository and the exact commit that built it. Publishing from a laptop cannot produce that, however careful you are, because the point is that an independent builder attests to it.

One-time setup: create a granular access token at https://www.npmjs.com/settings/samiul_i/tokens scoped to the @samiul_i scope with Read and write package permission, and add it as the repository secret NPM_TOKEN under Settings → Secrets and variables → Actions. This is the one place a long-lived write token is the right choice — nobody can type a one-time code in CI.

Then each release is:

npm version patch && git push --follow-tags

npm version bumps package.json, commits, and tags. The workflow checks the tag matches the version, builds, runs the self-check, and publishes. A mismatch fails before the registry call.

Provenance cannot be added to an already-published version, so it begins with the first release made this way.

Transport

stdio only, by design. The client launches the server as a subprocess, and the Personal API key never leaves the user's machine.

There is deliberately no HTTP transport. The SDK's HTTP handler verifies no tokens of its own, and this server reads a person's complete health history, so an HTTP listener would need authentication in front of it to be safe off-device. Shipping one unauthenticated in a public package would invite exactly that mistake. See docs/roadmap.md if you want to add HTTP and auth together.

Architecture

src/
  index.ts        createServer() factory, server instructions, stdio transport
  constants.ts    Hosts, paths, timeouts, character limit
  auth.ts         Key resolution, ApiKey -> JWT exchange, token cache
  api.ts          The one authenticated GET path every tool uses
  errors.ts       HTTP status -> recovery-oriented message
  format.ts       Input schemas, empty-value pruning, Markdown/JSON rendering
  tools/
    activity.ts       Search, detail, wellness activities, file download
    health.ts         Daily snapshot, single-metric trend, heart-rate zones
    profile.ts        Units and locale settings
    womens-health.ts  Menstrual cycle summary and day view
    raw.ts            Passthrough for uncovered endpoints
scripts/smoke.ts  Self-check client
evals/            Evaluation questions

Authentication

The Personal API key is not accepted on data requests. auth.ts exchanges it for a bearer token:

POST /personalapitoken-service/personal-api/personal-api-key/token
Authorization: ApiKey <your key>

The response body is the raw JWT as plain text, not JSON. Expiry comes from the token's exp claim; tokens last 24 hours and there are no refresh tokens, so a new one is obtained when under an hour remains. Tokens are cached in ~/.config/garmin/connect-personal-api-bearer-token.json with mode 0600, in the same format as Garmin's Python client, keyed by a fingerprint of the host and key. A 401 on a data request triggers one forced re-exchange, which separates a revoked token from a genuinely bad key.

The server warns on stderr if either credential file is readable by other users.

Tools

All tools are read-only except garmin_download_activity_file, which writes a ZIP to a path you give it.

Tool

Covers

garmin_search_activities

Activity search with date, type, distance, duration, elevation, power, and event filters; paginated

garmin_get_activity

One activity's detail, optionally with its structured workout steps

garmin_get_wellness_activities

Auto-detected wellness activities over a date range

garmin_download_activity_file

The original activity file, as a ZIP, saved to disk

garmin_get_daily_health

One date, any of: summary, sleep, heart rate, HRV, SpO2, fitness age, nutrition

garmin_get_health_trend

One metric over a date range: steps, stress, hydration, daily summaries, sleep, HRV, body battery, weight, all weigh-ins, weight goals

garmin_get_heart_rate_zones

Configured heart-rate training zones

garmin_get_user_profile

Display name, locale, time zone, unit preferences

garmin_get_menstrual_cycle

Cycle summary or detailed day view

garmin_get_raw

Any Personal API path, for endpoints and parameters the above omit

Every data tool takes response_format: markdown (default) for readable text, json for exact Garmin field names with structuredContent. garmin_get_activity, garmin_get_daily_health and garmin_get_health_trend also take detail_level, defaulting to summary.

Design decisions

Sparse data across devices

Garmin watches differ in sensors, and a given day's data also depends on whether a feature was enabled and whether the watch was worn. SummarizedActivityDTO alone has 230 properties, most specific to one sport.

Three things follow from that:

  • Empty values are pruned. null, undefined, "", [], and {} are removed recursively from both output formats. 0 and false are kept — zero steps and a false nap flag are real measurements, and dropping them would be indistinguishable from an absent sensor. Pass include_empty_fields=true to see the raw shape.

    Measured: this saves about 2%, not the large win originally assumed. Across 88.6 KB of real payloads from one account, pruning removed 1.7 KB. The premise was wrong: Garmin largely omits fields it has no data for rather than returning them as null, so responses are dense, not null-padded. What actually drives response size is time-series data — a single day of dailyHeartRate is 14 KB of real readings. Pruning is kept because it is free, keeps Markdown readable, and may matter more on accounts sparser than the one measured, but the levers that matter are detail_level and narrow date ranges.

  • Availability is stated, not implied. garmin_get_daily_health always returns metrics_with_data and metrics_unavailable, plus a note explaining that an unavailable metric means absent sensor, disabled feature, unworn device, or unsynced data — never a zero reading. Without this, pruning and a genuine zero look the same to a model.

  • Summary and deep dive are separate. garmin_get_activity, garmin_get_daily_health and garmin_get_health_trend all take detail_level, defaulting to "summary", and the trend tool takes one metric per call so responses stay bounded. This is where the real savings are, and unlike pruning they were worth measuring:

    Request

    full

    summary

    One day of heart_rate

    14,239 B

    ~190 B

    30 nights of sleep

    over the limit

    19,455 chars

    30 days of daily_summaries

    75,289 chars

    17,336 chars

    30 days of body_battery

    over the limit

    8,846 chars

    Daily snapshot, all 7 metrics

    over the limit

    7,419 chars

    Summary mode omits raw per-sample series — heartRateValues alone is 720 readings and 97% of a day's heart-rate payload — and keeps the figures callers ask for, such as restingHeartRate, deepSleepSeconds and averageSpO2. Omissions are replaced with a note saying how many samples were dropped and that detail_level="full" returns them, so nothing disappears silently. full is unchanged and still exceeds the limit on wide ranges, which the tool reports.

Why tools are not gated on device capability

A tempting design is to query the user's hardware at startup and register only the tools their watch supports. This server does not, for three reasons:

  1. There is no device endpoint. The Personal API has 26 paths and none of them list devices. DeviceMetaDataDTO is referenced by exactly one schema, WellnessActivitySummaryDTO, so it identifies the device that recorded one wellness activity. UserSettingsDTO carries only display and locale preferences, with no capability flags.

  2. Capability does not imply data. A top-end watch still returns nothing for sleep on a night it was not worn, or for a feature enabled last week. Gating on hardware would hide working tools and keep non-working ones.

  3. It would make startup fragile. Making tool registration depend on a network call turns an expired key into a server that will not start, instead of a per-call error the model can report. It would also add a Garmin round-trip to startup, and to every request under any future HTTP transport, where the server factory runs per request.

Grouping already keeps the manifest at ten tools, so gating would remove one or two at best.

Undocumented date-range limits

Garmin caps some ranges and answers an over-long request with a bare HTTP 400 and no explanation. The limits are not in the OpenAPI document; these were found by bisecting real requests:

Metric

Longest accepted range

steps, stress

28 days

hydration

31 days

hrv, weight, weight_all, weight_goals, wellness activities

365 days, no limit found

garmin_get_health_trend enforces the known caps locally and says how to split the request, rather than forwarding a 400 that reads like a malformed date. If Garmin changes a limit, the server's message will be wrong before the API's is — the numbers live in MAX_RANGE_DAYS in src/tools/health.ts.

Response validation

The API defines 109 response schemas. Rather than hand-maintain Zod mirrors of all of them, responses are rendered generically: a null-safe Markdown renderer, or exact JSON. Nothing calls .parse() on a Garmin payload, so a field Garmin adds or omits cannot throw. The trade-off is that upstream data is not schema-validated; response_format="json" returns what Garmin actually sent.

Known gaps between sources

Garmin's Python client wraps two endpoints that are not in the published OpenAPI document: /activity-service/activity/activityTypes/simple and /metrics-service/metrics/maxmet/daily/... (VO2 max). They are not given dedicated tools here; if your account serves them, reach them through garmin_get_raw. Conversely the spec includes /usersummary-service/usersummary/daily and /weight-service/weight/all/..., which that client does not wrap — both are covered here.

Security

  • The key is read from the environment or a 0600-style file, never hardcoded, and never logged.

  • garmin_get_raw accepts a path, not a URL: full URLs, protocol-relative paths, embedded schemes, .., and whitespace are rejected, so requests cannot be redirected off the configured host.

  • Logging goes to stderr; stdout carries only the JSON-RPC stream. The key is never written to either.

  • stdio only, so the server is reachable solely by the process that launched it. No network listener is opened.

License

MIT

Available Tools

10 tools
garmin_download_activity_fileDownload Activity FileA
Idempotent

Download an activity's original file from Garmin as a ZIP and save it to disk.

Garmin serves these as a ZIP archive containing the FIT file (and sometimes GPX/TCX). The ZIP is written to output_path and the tool reports the path and size; the file contents are not returned to the model.

Use garmin_search_activities to find activity IDs.

Inputs: activity_id (required), output_path (required), overwrite.

garmin_get_activityGet Activity DetailA
Read-onlyIdempotent

Get one activity in detail by its ID, optionally with its structured workout steps.

detail_level="summary" (the default) returns the ~22 fields that apply to any sport. detail_level="full" returns all available fields, which for a single activity can exceed 200 properties covering dive, golf, and other sport-specific sensors.

Use garmin_search_activities to find activity IDs.

Inputs: activity_id (required), detail_level, response_format, include_workouts, +1 more.

garmin_get_daily_healthGet Daily Health SnapshotA
Read-onlyIdempotent

Get several health metrics for a single calendar date in one call.

Requests each selected metric in parallel and reports which returned data. The result always lists metrics_unavailable: a metric appearing there means the sensor is absent from the device, the feature was off, the watch was not worn, or the data has not synced. It never means the measured value was zero.

Metrics: summary (steps, floors, calories, intensity minutes), sleep (stages and score), heart_rate (resting and daily range), hrv (overnight heart-rate variability), spo2 (pulse ox and acclimation), fitness_age, nutrition (logged food and goals).

Examples:

  • "How did I sleep last night?" -> date="2026-11-12", metrics=["sleep"]

  • "Full picture for yesterday" -> date="2026-11-12", metrics=["summary","sleep","heart_rate","hrv"]

For one metric across many days, use garmin_get_health_trend instead.

Inputs: date, metrics, detail_level, response_format, +1 more.

garmin_get_health_trendGet Health TrendA
Read-onlyIdempotent

Get one health metric as a series across a date range, for trends and comparisons.

One metric per call keeps the response bounded. Metrics: steps, stress, hydration, daily_summaries (full daily wellness rollup), sleep (nightly summaries), hrv (daily HRV summaries), body_battery, weight (one entry per day), weight_all (every weigh-in), weight_goals.

Examples:

  • "Step trend this month" -> metric="steps", start_date="2026-11-01", end_date="2026-11-13"

  • "Has my sleep improved?" -> metric="sleep", start_date="2026-10-01", end_date="2026-11-13"

  • "All weigh-ins this year" -> metric="weight_all", start_date="2026-01-01", end_date="2026-11-13"

An empty result means no data in that range, not zero values. Narrow the range if the response hits the character limit.

Inputs: metric (required), start_date (required), chunks, end_date, +4 more.

garmin_get_heart_rate_zonesGet Heart Rate ZonesA
Read-onlyIdempotent

Get the user's configured heart-rate training zones, including the boundary beats per minute for each zone. Useful for interpreting the averageHR and maxHR on activities.

Inputs: response_format, generate_defaults, include_empty_fields.

garmin_get_menstrual_cycleGet Menstrual Cycle DataA
Read-onlyIdempotent

Get menstrual cycle tracking data for a date.

view="summary" returns the cycle summary for the date, with cycle phase and prediction context. view="day_view" returns the detailed per-day log, which can include symptoms, flow, and pregnancy-mode baby movement (kick count) entries.

This data exists only when the user tracks cycles in Garmin Connect. An empty result means tracking is not enabled or no entry exists for that date, not a zero reading.

Inputs: date, view, today_date, response_format, +3 more.

garmin_get_rawCall a Garmin Endpoint DirectlyA
Read-onlyIdempotent

Perform an authenticated GET against any Garmin Personal API path, for endpoints and parameters the other tools do not expose.

The request goes to the configured Garmin host with the bearer token applied; only the path and query are yours to set.

Use it for:

  • Activity search filters the dedicated tool omits: upperLatitude/leftLongitude/lowerLatitude/rightLongitude bounding boxes, lowerMaxDepth/upperMaxDepth and decoDive for diving, minBottomTime/maxBottomTime, minElapsedDuration/maxElapsedDuration, excludedActivitySubType, requiredField

  • Any endpoint Garmin adds after this server was written

Example: path="/activitylist-service/activities/search/activities", query={"activityType":"diving","lowerMaxDepth":"1800","limit":"5"}

Prefer the dedicated tools when one covers the request: they format results and report metric availability.

Inputs: path (required), query, response_format, include_empty_fields.

garmin_get_user_profileGet User Profile SettingsA
Read-onlyIdempotent

Get the user's Garmin Connect profile settings: display name, locale, time zone, and the unit preferences that other responses are expressed in.

Call this first when units matter. measurementSystem tells you whether the user thinks in metric or statute, and powerFormat, heartRateFormat, hydrationMeasurementUnit, and glucoseMeasurementUnit give the per-metric display units. Garmin returns distances in meters and durations in seconds regardless, so this is about how to present them.

Note: this endpoint carries no device or sensor information. The Personal API exposes no device inventory, so which metrics a user has is discovered by requesting them; garmin_get_daily_health reports what came back.

Inputs: response_format, include_empty_fields.

garmin_get_wellness_activitiesGet Wellness ActivitiesA
Read-onlyIdempotent

List automatically detected wellness activities over a date range: the low-effort movement Garmin logs without the user starting a workout, such as walks.

These are distinct from recorded workouts; use garmin_search_activities for those. Each entry includes the device that recorded it.

Inputs: end_date (required), start_date (required), response_format, include_empty_fields.

garmin_search_activitiesSearch ActivitiesA
Read-onlyIdempotent

Search and filter the user's recorded Garmin activities (workouts), newest first by default.

Returns one page of compact activity summaries, each with an activity ID. Pass that ID to garmin_get_activity for full detail, or to garmin_download_activity_file for the original FIT/GPX file.

Examples:

  • "My runs last month" -> activity_type="running", start_date="2026-10-01", end_date="2026-10-31"

  • "Longest rides over 50km" -> activity_type="cycling", min_distance=50000, sort_by="distance", sort_order="desc"

  • "Find my race efforts" -> event_type="race"

Pagination: when has_more is true, call again with offset=next_offset. This covers the common filters; garmin_get_raw reaches the dive-depth, bounding-box, and field-requirement filters.

Inputs: limit, offset, search, sort_by, +16 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.3/5.0

    Scored across 10 tools

    Disambiguation4/5

    Most tools target clearly distinct resources: activities vs. wellness activities vs. daily health vs. trends vs. profile vs. cycle tracking, and the descriptions explicitly state the boundaries (e.g. daily_health = many metrics for one day, health_trend = one metric across days). The main soft spot is garmin_get_raw, which overlaps with nearly every other tool, though it is framed as a fallback for unexposed endpoints and parameters.

    Naming Consistency5/5

    Every tool follows the same garmin_verb_noun snake_case pattern (garmin_search_activities, garmin_get_activity, garmin_download_activity_file), with a consistent server prefix and verb vocabulary of get/search/download.

    Tool Count5/5

    Ten tools is a well-scoped set for a read-only Garmin Connect integration, with one tool per major data area plus a generic escape hatch. No tool feels redundant or padded.

    Completeness4/5

    The read surface is broad: activities (search/detail/file download), wellness activities, sleep, HRV, body battery, weight, stress, steps, profile, and cycle tracking, with garmin_get_raw covering anything unexposed. Gaps are minor — no workout/course listing (only workout steps embedded in get_activity), and no write operations such as upload or delete, which is plausible for a read-only server but leaves a lifecycle dead end.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      C
      quality
      B
      maintenance
      Enables access to Garmin Connect fitness data through 39 fine-grained tools covering body, heart, sleep, stress, activity, steps, hydration, devices, nutrition, goals, and user profile.
      40
      3
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Provides local, Keychain-secured access to all 26 Garmin Connect Personal API GET endpoints plus 8 summary tools. Enables users to query sleep, HRV, Body Battery, activities, profile, weight, and other connected health data through their own Personal API key.
      34
      1
      Apache 2.0