Skip to main content

Garmin Training MCP

A local, read-only Garmin Personal API MCP server for Claude Code and Codex on macOS. It supplies compact training and wellness data for client-side analysis through stdio. This project is unofficial and is not affiliated with Garmin.

Uses Garmin's official Python client and official MCP Python SDK (mcp.server.fastmcp.FastMCP), not the separately distributed fastmcp project. MCP is pinned to 1.30.0 because the official 2.x SDK renamed this interface to MCPServer.

Scope and verified capabilities

Tool Inputs Output
list_activities start_date, end_date, optional parent sport, offset=0, limit=20 IDs, name/sport/start, duration, distance, speed, HR, power, activity training load/effect where present; next page information
get_activity Decimal-string activity_id, split_offset=0, split_limit=100 Activity metrics, reported recovery HR, one page of feature/group split summaries
get_sleep start_date, optional end_date Duration/stages in seconds, numeric Garmin score components, sleep window times
get_hrv start_date, optional end_date Garmin overnight/weekly summaries and reported baseline/status
get_daily_summary start_date, optional end_date Steps, Garmin resting-HR field, stress and Body Battery where present

The capability matrix separates official-source confirmation, implemented features needing account verification, and unavailable features.

Split summaries can represent groups of splits, and do not promise individual laps or a heart-rate time series. Detailed samples and GPS tracks are unavailable in v1; include_samples=true or include_gps=true on get_activity returns unsupported. No start/end coordinates are returned. The official ZIP download endpoint could support future FIT parsing, which is outside v1. Daily readiness/load/status/recovery-time endpoints were not found; activity training load is a different reported metric.

Requirements

  • macOS.
  • Python 3.12 or newer. The examples request Python 3.13, which was used for testing.
  • uv, including its uvx command.
  • A Garmin Connect account and a Personal API Key.
  • Internet access for Garmin requests and initial dependency downloads.

Quick start with uvx

Clone or download this repository, then open a terminal in its root directory. Check that the local package launches:

uvx --python 3.13 --from . garmin-mcp --help

--from . uses the current checkout as the package source. No PyPI publication, HTTP server or cloud hosting is required. See uv's tools guide.

Find the absolute paths needed by your MCP client:

pwd -P
command -v uvx

In the configurations below, replace:

  • /absolute/path/to/garmin-connect-mcp with the repository directory printed by pwd -P.
  • /absolute/path/to/uvx with the executable printed by command -v uvx.
  • YOUR_GARMIN_PERSONAL_API_KEY with your own key, only in your local client configuration.

These are placeholders, not paths to copy unchanged. MCP clients do not necessarily expand ~ or shell variables in commands/arguments, so use actual absolute paths. Keep the checkout in place; the client must be able to load it when starting the server.

The first uvx launch may build the package and download dependencies; later launches use uv's cached tool environment. uvx resolves dependencies independently of this project's uv.lock. For exact locked dependencies, use the project installation instead.

Configure credentials and connect your client

Create a key on the Garmin Connect Personal API Key page. Garmin lets you choose 30, 90, 180 days or one year; the key is displayed only once. Deleting a key revokes it and its associated access tokens. See the official FAQ.

Choose one of the credential methods below. Use the Personal API Key, not a temporary bearer token. Do not put real credentials in this repository, shared examples, chat messages, tool arguments or shell-history commands.

Option 1: GARMIN_PERSONAL_API_KEY in client configuration

Set GARMIN_PERSONAL_API_KEY in the client's local env configuration. No separate garmin-mcp setup command is required with this method.

Codex CLI and local desktop app

Merge the following server entry into your user configuration at ~/.codex/config.toml:

[mcp_servers.garmin-training]
command = "/absolute/path/to/uvx"
args = ["--python", "3.13", "--from", "/absolute/path/to/garmin-connect-mcp", "garmin-mcp"]
tool_timeout_sec = 180

[mcp_servers.garmin-training.env]
GARMIN_PERSONAL_API_KEY = "YOUR_GARMIN_PERSONAL_API_KEY"

If garmin-training already exists, edit that entry instead of creating a duplicate TOML table. If only the key is changing, add or update the .env table and keep your existing command/arguments. The timeout is optional and allows time for bounded retries.

Save the configuration and restart the server or app. Inspect /mcp, then ask:

Use Garmin to list my runs from the last week.

Useful CLI checks:

codex mcp get garmin-training
codex mcp list

Codex CLI and the local desktop app share MCP configuration for the same host. See the official OpenAI MCP documentation.

Clients accepting mcpServers JSON

Merge this entry into the client's local MCP configuration:

{
  "mcpServers": {
    "garmin-training": {
      "type": "stdio",
      "command": "/absolute/path/to/uvx",
      "args": [
        "--python", "3.13",
        "--from", "/absolute/path/to/garmin-connect-mcp",
        "garmin-mcp"
      ],
      "env": {
        "GARMIN_PERSONAL_API_KEY": "YOUR_GARMIN_PERSONAL_API_KEY"
      }
    }
  }
}

For Claude Code, the user MCP configuration is in ~/.claude.json; preserve all other settings when adding the entry under mcpServers. Restart Claude Code, run /mcp, and verify the server connects and exposes five tools. You can also run:

claude mcp get garmin-training
claude mcp list

See the official Claude Code MCP documentation for configuration scopes and supported clients.

The key is stored as plaintext in the client's local configuration and passed to the server through its environment. Restrict that configuration's permissions and keep it out of version control. The server does not copy an environment key to its personal-api-key file; temporary bearer tokens still use the protected local cache.

An actual successful tool call verifies that the server can authenticate. If a saved key file is also present, success alone does not establish which source was used; a nonempty environment setting takes precedence. A key configured only in the client's env block is not automatically available to a separate terminal command.

Option 2: hidden-prompt key file

From the repository root, run:

uvx --python 3.13 --from . garmin-mcp setup

The prompt hides your input and refuses noninteractive input or an echoing fallback. It stores the key outside the repository:

~/Library/Application Support/garmin-training-mcp/personal-api-key

Use the same client configurations above, omitting the env block or .env table if you want the saved key file to be used. An empty GARMIN_PERSONAL_API_KEY is an error, not a fallback to the key file.

Alternatively, register clients from the repository root using these shell commands, which resolve the launch paths before saving the registration:

GARMIN_MCP_DIR="$(pwd -P)"
GARMIN_UVX_BIN="$(command -v uvx)"
claude mcp add --transport stdio --scope user garmin-training -- "$GARMIN_UVX_BIN" --python 3.13 --from "$GARMIN_MCP_DIR" garmin-mcp
codex mcp add garmin-training -- "$GARMIN_UVX_BIN" --python 3.13 --from "$GARMIN_MCP_DIR" garmin-mcp

Register only the clients you use. If an entry already exists, update its configuration or remove it before adding it again. Both clients launch the same implementation, with separate stdio processes sharing the local credential store safely.

Credential storage and renewal

The application directory has mode 700; key, token and lock files have mode 600. Symlinked, nonregular, wrong-owner or permissive credential files are rejected. The bearer-token.json file in the same directory uses the official SDK's cache format and expiry logic, with atomic writes and a cross-process lock.

Variable Behavior
GARMIN_PERSONAL_API_KEY Overrides the saved key. An empty value is an error with no fallback.
GARMIN_MCP_CONFIG_DIR Overrides the storage directory; must be an absolute path. Configure identically in both clients when sharing a cache.
GARMIN_MCP_TIMEZONE Presentation timezone as an IANA identifier; defaults to America/Vancouver.

GARMIN_PERSONAL_API_BASE_URL overrides are rejected except for the identical official origin. Requests ignores proxy/netrc environment settings and follows no redirects, so credentials are sent only to https://personal.connectapi.garmin.com.

The official authenticator exchanges the key for a bearer token valid for up to 24 hours or key expiry, whichever is earlier. It renews the cache when a token has one hour or less remaining. There are no refresh tokens. A data-call 401 triggers one forced exchange and one replay. If authentication still fails, replace the key in the client's environment setting or run setup again, depending on your method. Changing the key prevents reuse of a token cached for a different key.

Install with the project lockfile

For development or exact locked dependencies, run from the repository root:

uv sync --locked
uv run --locked garmin-mcp --help

This creates .venv and the garmin-mcp executable. For a client to launch this installation directly, use /absolute/path/to/garmin-connect-mcp/.venv/bin/garmin-mcp as its command and [] as its arguments. Keep the same credential configuration. The direct launcher does not install or resolve dependencies during client startup.

Dates, units and output semantics

  • Dates are exactly YYYY-MM-DD, with ordered inclusive bounds. Wellness accepts up to 31 days; omitted end equals start. Activity search accepts up to 366 days. These conservative local limits are separate from endpoint limits in the matrix.
  • Activity filters are Garmin's activity-local start dates. Garmin wellness calendarDate is preserved. The presentation timezone does not shift filters or rebucket wellness data. Official Swagger does not establish whether sleep/HRV calendar dates consistently refer to sleep start or wake date; verify against timestamps before aligning nights and workouts.
  • Documented GMT activity/sleep fields are UTC even when the API omits a suffix. Output instants are ISO timestamps with offsets in the configured IANA zone. Naive Garmin local start times remain garmin_local_start; no offset is invented. DST behavior follows the machine's timezone database. Sleep schema declarations conflict about strings versus milliseconds; ISO date-time and numeric epoch milliseconds are accepted for GMT sleep fields pending live verification.
  • Distance is meters, duration/stages seconds, speed meters/second, heart rate bpm, power watts. There are no derived metrics or pace estimates. Load, training effect, stress and Body Battery retain Garmin's reported scales. HRV values also retain Garmin's scale: the reviewed Swagger does not state the measurement unit, so output names intentionally do not claim a unit.
  • resting_heart_rate_bpm comes from Garmin's restingValue, documented as resting HR or absolute minimum when no resting value is available. The server cannot distinguish those cases and does not replace it with its own calculation.
  • A missing property stays absent and is listed in unavailable_fields; explicit null stays null; numeric zero stays zero. Absence means Garmin didn't provide that field, not proof of a device limitation. Numeric sleep score components preserve missing/null values too. Unsupported/unrecorded metrics are never inferred.
  • Every response includes retrieval time (UTC), presentation timezone, requested period where applicable, date semantics, records, missing dates and pagination/warnings. status=partial means some requested dates were absent; missing means no records. ok means coverage was returned, not that every optional metric exists. Malformed, duplicate or out-of-period wellness rows cause a schema error instead of misleading partial results. A 404 is an explicit not_found error, never a zero measurement.
  • Pages have at most 100 activities/split summaries; offsets are 0..10000. Activity search fetches one extra record to report has_more. Follow next_offset. A full page without another record has has_more=false. At the local offset cap, has_more=true with no next offset includes a warning to narrow the range.
  • Structured output is limited to 128 KiB, decoded HTTP bodies to 4 MiB. Oversize responses produce an error rather than silently dropping fields/rows. Split paging reduces returned output but fetches the same bounded activity summary.

Example tool inputs:

{"start_date":"2026-10-01","end_date":"2026-10-14","sport":"running","limit":20}
{"activity_id":"123456789","split_offset":0,"split_limit":25}

Reliability and privacy

Only documented Personal API operations are used. Remote data operations are GET; token creation is the required authentication POST. All five tools advertise read-only, nondestructive, idempotent, open-world MCP annotations. No arbitrary URL or endpoint tool exists; no email/password, scraping or unofficial fallback exists.

Requests have 5-second connect and 15-second read timeouts, plus a 20-second response-body deadline that starts after response headers arrive. A timer interrupts the socket even when a streaming read has not filled its chunk. GET attempts are limited to three, including retries for network failures and HTTP 429/500/502/503/504. Retry-After (seconds or HTTP date) is honored: wait up to five seconds per retry, otherwise return the delay without retrying. Without a header, delays are 0.5 then 1 second. Token POST is not retried. Stale-token recovery is limited to one renewal/replay; a slow pathological call can exceed an individual client timeout. Credential-lock acquisition waits at most five seconds.

Garmin calls run in worker threads, preserving MCP runtime responsiveness. Sessions close after each adapter call, including errors; the server exits when stdin closes or the client terminates it. Blocking in-flight requests finish or time out before thread cleanup. There is no long-lived HTTP session, background scheduler, database, or wellness cache. Data is fetched fresh each call; only bearer tokens are cached.

Source, configuration and credentials stay on your Mac. Internet is required for Garmin API requests, and package downloads are required during installation/update. Retrieved data can be sent to the AI provider by Claude Code or Codex. Running the server locally does not make the model's analysis offline or keep retrieved health information out of client history. Configure provider/client privacy settings to match your preferences. The server itself writes no health-data files or telemetry. Your macOS backups or sync tools may back up local files; the plaintext credential store is not Keychain and should be treated like other local secrets.

Stdout is exclusively MCP protocol during serving. Diagnostics go to stderr using fixed codes for ordinary tool failures. Upstream bodies and raw tool-validation errors are not returned in tool error responses. SDK diagnostic records are also redacted at the root logging handler, including protocol-validation warnings and tracebacks; see the security audit. Do not enable raw HTTP logging around this process. Returned activity names are untrusted user content, not instructions.

Verify and test

From the repository root, run the offline checks below. Tests use clearly synthetic fixtures and injected HTTP transport; no test requires your key or contacts Garmin:

uv run --locked pytest
uv run --locked ruff check .
uv run --locked ruff format --check .

The stdio tests launch actual subprocesses, initialize an MCP session, discover all five schemas/annotations, invoke all five tools with synthetic HTTP responses through the official SDK, check errors, and close the transport. They also start the production entry point without credentials to verify discovery and an actionable error. Fixture injection exists only in test files, not as a production mode.

Separate live verification, after you configure credentials locally: choose a date with synced activity, sleep and wellness data. From the repository root, replace YYYY-MM-DD with that date:

uvx --python 3.13 --from . garmin-mcp check --date YYYY-MM-DD

This command makes real Garmin calls and prints only availability counts/statuses, not raw health records or keys. It checks activity detail if search finds an activity. A missing date is a successful empty result, not proof of device support. Then:

  1. Make an actual tool invocation in each registered client; compare the returned metrics with Garmin Connect for a known workout/night/day.
  2. Verify sport-specific power/HR/distance, grouped split meaning, sleep score keys, HRV scale/baseline/status, and nested daily-summary fields for your account/devices.
  3. Compare sleep window timestamps to the associated calendar date, particularly across midnight/travel. Check actual sleep timestamp representation.
  4. Query an older short range to establish accessible history for each tool. No retention/backfill guarantee was found. Test pagination against enough activities.
  5. Restart both clients to exercise cache reuse. Check renewal on expiry and a deliberately revoked key only when ready to replace it through setup.

The five tools have passed a live account check, and activity/sleep queries have been exercised through Codex. These checks do not guarantee the same data is available for every account or device. Claude Code integration, historical coverage, every optional sport/device metric, and live expiry/revocation behavior still need verification for the deployment you use.

Troubleshooting

Symptom / error code Action
No tools/failed startup Run the absolute executable with --help; run locked sync; restart the client; check stderr. Running serve manually waits for MCP messages.
missing_credentials Check GARMIN_PERSONAL_API_KEY in the client's env block, or run setup if using a key file. An empty environment override is an error. For key files, check mode/ownership and use chmod 600 on files you intentionally trust.
authentication Key may be expired/deleted/revoked. Create a new Personal API key, update the client's environment setting and restart the server, or run setup if using a key file. No interactive sign-in occurs during calls.
configuration Check absolute config path, timezone and directory permissions; remove SDK base URL overrides.
busy Another process is accessing credentials/doing a request. Retry when it finishes. Do not delete the lock file while clients run.
invalid_input Check date format/order, <=31/366-day range, positive decimal-string ID, parent sport key, page size and offset. No credential arguments are accepted.
rate_limited Honor retry_after_seconds when provided, otherwise wait and retry conservatively.
timeout, network, upstream Check internet/Garmin availability, retry later or request a smaller range. Environment proxy settings are intentionally ignored.
not_found / missing Check account/date/sync state. Data may be unavailable; absence is not zero.
upstream_rejected Check supported parent sport, dates and resource availability; Garmin may impose undocumented limits.
upstream_schema Official API schema may have changed or an unexpected response arrived. Mapping is intentionally strict. Inspect upstream changes without logging credentials or full responses.
response_too_large Reduce date range/page size. A single summary over the upstream body cap cannot be retrieved by this version.
unsupported Samples/GPS require future FIT work; readiness/daily load are not available through the documented endpoints.

Update or remove

After editing dependencies intentionally, use uv lock to refresh uv.lock, review changes and run the full checks before keeping the update. The pinned official SDK versions do not automatically jump to breaking releases. For existing locked sources:

uv sync --locked

Run project commands from the repository root. For a local source directory, uv's cache normally checks the project metadata timestamp, so source-only changes may not trigger a rebuild. If using uvx, refresh the metadata timestamp and launch an isolated tool environment after source changes:

touch pyproject.toml
uvx --isolated --python 3.13 --from . garmin-mcp --help

The touch command updates only the timestamp, not the file's contents. See uv's local dependency caching behavior. Restart your MCP client after code/dependency changes. Remove registrations with:

claude mcp remove --scope user garmin-training
codex mcp remove garmin-training

To remove saved credentials, first stop both clients/server processes, then delete the garmin-training-mcp application-support directory using Finder. Revoke the key in Garmin Connect as well, and remove any GARMIN_PERSONAL_API_KEY value saved in client configuration or inherited from your environment. Removing client registration alone does not revoke the key or delete the separate credential store. Remove the checkout/virtual environment separately if no longer needed.

Architecture

config.py owns credentials/settings/locking; adapter.py wraps official SDK auth and transport; models.py defines contracts; normalize.py projects documented fields; service.py handles bounded data operations; server.py registers stdio MCP tools; __main__.py separates setup, serving and explicit live checks. The adapter accepts a session factory for synthetic HTTP tests. The pinned official SDK's private token writer is overridden only to add atomic replacement, with tests covering the cache and concurrency behavior.

See the capability matrix for the official API sources and the security audit for reviewed controls and the logging remediation.

License

This project is licensed under the MIT License. Dependencies retain their own licenses.

Metadata

Release files for garmin-training-mcp 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for garmin-training-mcp 0.1.0
File Size Uploaded Provenance
garmin_training_mcp-0.1.0.tar.gz 103.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for garmin-training-mcp 0.1.0
File Interpreter ABI Platform
garmin_training_mcp-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 131.2 kB

Release files / garmin_training_mcp-0.1.0.tar.gz

Download URL garmin_training_mcp-0.1.0.tar.gz
Size 103.3 kB
Tags Source
SHA-256 checksum
How to use checksums
c21d453f95b5ec7a4c16caefd16f4cb5a3c07d5c1bb087e27a791c9c6a940e6c
BLAKE2b-256 checksum
How to use checksums
87b690a2f0190a7dcd3bc6d2fb7321d3760b0cbbec5a4f7b403d70857ffe21eb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / garmin_training_mcp-0.1.0-py3-none-any.whl

Download URL garmin_training_mcp-0.1.0-py3-none-any.whl
Size 27.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
c51d4ed086a04fb17ba618de1cc31a6bfcca459ff29d7e7efe6e5a767fb75c67
BLAKE2b-256 checksum
How to use checksums
b00a053890b2f174f6be2acdcaadb2f1bf94313c19c3d05bd128db6b767fe912
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.5 {"installer":{"name":"uv","version":"0.13.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for garmin-training-mcp 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page