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
uvxcommand. - 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-mcpwith the repository directory printed bypwd -P./absolute/path/to/uvxwith the executable printed bycommand -v uvx.YOUR_GARMIN_PERSONAL_API_KEYwith 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
calendarDateis 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_bpmcomes from Garmin'srestingValue, 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; explicitnullstays 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=partialmeans some requested dates were absent;missingmeans no records.okmeans 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 explicitnot_founderror, 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. Follownext_offset. A full page without another record hashas_more=false. At the local offset cap,has_more=truewith 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:
- Make an actual tool invocation in each registered client; compare the returned metrics with Garmin Connect for a known workout/night/day.
- 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.
- Compare sleep window timestamps to the associated calendar date, particularly across midnight/travel. Check actual sleep timestamp representation.
- Query an older short range to establish accessible history for each tool. No retention/backfill guarantee was found. Test pagination against enough activities.
- 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)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| garmin_training_mcp-0.1.0.tar.gz | 103.3 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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.