garmin-personal-api-mcp 1.1.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PRIVACY.md +2 -2
- package/README.md +20 -2
- package/dist/keystore.js +9 -4
- package/dist/keystore.js.map +1 -1
- package/dist/server.js +3 -1
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/PRIVACY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Privacy Policy
|
|
2
2
|
|
|
3
|
-
_Last updated: 2026-10-
|
|
3
|
+
_Last updated: 2026-10-25_
|
|
4
4
|
|
|
5
5
|
This policy covers **garmin-personal-api-mcp**, also distributed as the Claude Desktop extension **Personal Data for Garmin Connect** ("the software"). It is an independent open-source project, not made or endorsed by Garmin.
|
|
6
6
|
|
|
@@ -31,7 +31,7 @@ Activity `.FIT` files used for split analysis are decoded in memory and not kept
|
|
|
31
31
|
|
|
32
32
|
## Third parties
|
|
33
33
|
|
|
34
|
-
- **Garmin** receives your API requests, as with any use of its Personal API. See Garmin's privacy policy.
|
|
34
|
+
- **Garmin** receives your API requests, as with any use of its Personal API. See Garmin's privacy policy. Your use of the API, including your key, is governed by Garmin's Personal API Supplemental Terms and Terms of Use: the key is for your own use only and must not be shared.
|
|
35
35
|
- **Your MCP client's provider** receives the data you ask it to retrieve, as described above.
|
|
36
36
|
|
|
37
37
|
No other party receives anything from the software.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Ask Claude about your own Garmin data: runs, VO2 max, sleep, HRV, Body Battery,
|
|
|
11
11
|
|
|
12
12
|

|
|
13
13
|
|
|
14
|
-
**Contents:** [Install](#install-in-claude-desktop) · [What you can ask](#what-you-can-ask) · [What it can't do](#what-it-cant-do) · [Your data](#your-data-and-your-key) · [Claude Code, Cursor and other apps](#claude-code-cursor-and-other-apps) · [Contributing](#contributing)
|
|
14
|
+
**Contents:** [Install](#install-in-claude-desktop) · [What you can ask](#what-you-can-ask) · [What it can't do](#what-it-cant-do) · [Your data](#your-data-and-your-key) · [Garmin's terms](#garmins-terms) · [Claude Code, Cursor and other apps](#claude-code-cursor-and-other-apps) · [Contributing](#contributing)
|
|
15
15
|
|
|
16
16
|
## Install in Claude Desktop
|
|
17
17
|
|
|
@@ -112,9 +112,27 @@ These limits are Garmin's, not this extension's:
|
|
|
112
112
|
|
|
113
113
|
This is an independent project, not made or endorsed by Garmin. "Garmin" and "Garmin Connect" are trademarks of Garmin Ltd.
|
|
114
114
|
|
|
115
|
+
## Garmin's terms
|
|
116
|
+
|
|
117
|
+
When you create a Personal API key, you accept Garmin's **Personal API Supplemental Terms** (Garmin Connect → Settings → [Personal API](https://connect.garmin.com/app/settings/personalApiToken) → Review Terms and Conditions), on top of the [Garmin Terms of Use](https://www.garmin.com/legal/terms-of-use/). This project is built to keep you within them:
|
|
118
|
+
|
|
119
|
+
- **Your key is for your own use only.** Garmin's terms allow a key to be used only by you, with tools you develop or operate for your own use, and forbid sharing it with anyone. So this tool runs on your computer, uses only your own key, and never sends the key anywhere except Garmin. **Never give your key or your Garmin login to anyone else**, including friends, support channels or reviewers. Anyone who wants to try the tool creates their own free Garmin account and key.
|
|
120
|
+
- **Only the official API.** Every request goes through the Personal API that Garmin provides for this purpose. There's no scraping and no unofficial login, which Garmin's Terms of Use forbid.
|
|
121
|
+
- **Personal, non-commercial and read-only.** The tool is free, never writes to your account, and has no hosted or shared version.
|
|
122
|
+
- **Rate limits.** Garmin may limit or block requests that are too frequent. Ask for the data you need rather than very long ranges over and over.
|
|
123
|
+
- **Your responsibility.** You're the one bound by Garmin's terms, and breaking them can get your key revoked. Read them when you create the key; Garmin can change them, and the current version is on that page.
|
|
124
|
+
|
|
115
125
|
## Claude Code, Cursor and other apps
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
**Claude Code and Cowork:** install the plugin, which adds the server plus a skill that tells Claude how to answer training and recovery questions:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
/plugin install garmin-personal-api-mcp --marketplace aaronhaddad/garmin-connect-mcp-claude-connector
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Claude Code asks for your key when you install it. Cowork doesn't ask, so run the setup below once and the plugin uses the saved key. See the [plugin README](plugin/README.md) for details.
|
|
134
|
+
|
|
135
|
+
**Any other MCP app**, or to save the key on your computer: run the setup wizard. You need Node.js 20.3 or later.
|
|
118
136
|
|
|
119
137
|
```bash
|
|
120
138
|
npx garmin-personal-api-mcp setup
|
package/dist/keystore.js
CHANGED
|
@@ -10,11 +10,15 @@ import { dirname } from "node:path";
|
|
|
10
10
|
import { API_KEY_ENV_VAR, CONFIG_DIR, DEFAULT_KEY_FILE, EXTENSION_DISPLAY_NAME, KEY_PAGE_URL, KEYCHAIN_ACCOUNT, KEYCHAIN_SERVICE, RUNTIME_ENV_VAR, SETTINGS_FILE, } from "./constants.js";
|
|
11
11
|
/** True when Claude Desktop runs us as a desktop extension, where the key comes from its settings, not `setup`. */
|
|
12
12
|
export const isExtension = () => process.env[RUNTIME_ENV_VAR] === "mcpb";
|
|
13
|
+
/** True when a Claude plugin (Claude Code, Cowork) runs us; the key comes from the plugin's userConfig or, failing that, from `setup`. */
|
|
14
|
+
export const isPlugin = () => process.env[RUNTIME_ENV_VAR] === "plugin";
|
|
13
15
|
/** How the user replaces a missing or rejected key, phrased for where we're running. Completes "Ask the user to …". */
|
|
14
16
|
export function keyFixHint() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
if (isExtension())
|
|
18
|
+
return `open Claude Desktop → Settings → Extensions → ${EXTENSION_DISPLAY_NAME}, and paste a new key created at ${KEY_PAGE_URL}`;
|
|
19
|
+
if (isPlugin())
|
|
20
|
+
return `create a new key at ${KEY_PAGE_URL}, then either paste it into the plugin's settings (in Claude Code: /plugin → Installed → ${EXTENSION_DISPLAY_NAME} → Configure) or run \`npx garmin-personal-api-mcp setup\` in a terminal to save it on this computer`;
|
|
21
|
+
return "run `npx garmin-personal-api-mcp setup` in a terminal to add a new key";
|
|
18
22
|
}
|
|
19
23
|
/** Appended to key errors: without it the model goes looking for the data in a browser instead of telling the user. */
|
|
20
24
|
export const KEY_STOP_NOTE = "Only the user can fix this: tell them, and don't try to get the data another way (such as a browser).";
|
|
@@ -68,7 +72,8 @@ export function deleteKeyFile(path = DEFAULT_KEY_FILE) {
|
|
|
68
72
|
}
|
|
69
73
|
export function resolveKey() {
|
|
70
74
|
const env = process.env[API_KEY_ENV_VAR]?.trim();
|
|
71
|
-
|
|
75
|
+
// An app that doesn't fill in plugin settings can pass the template through untouched; that's not a key.
|
|
76
|
+
if (env && !env.startsWith("${"))
|
|
72
77
|
return { key: env, source: "env", location: `$${API_KEY_ENV_VAR}` };
|
|
73
78
|
const kc = readKeychain();
|
|
74
79
|
if (kc)
|
package/dist/keystore.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keystore.js","sourceRoot":"","sources":["../src/keystore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where the Personal API key lives. Lookup order:\n * 1. GARMIN_PERSONAL_API_KEY environment variable\n * 2. macOS Keychain (written by `garmin-personal-api-mcp setup`)\n * 3. ~/.config/garmin/connect-personal-api-key (shared with Garmin's official Python client)\n */\nimport { spawnSync } from \"node:child_process\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport {\n API_KEY_ENV_VAR,\n CONFIG_DIR,\n DEFAULT_KEY_FILE,\n EXTENSION_DISPLAY_NAME,\n KEY_PAGE_URL,\n KEYCHAIN_ACCOUNT,\n KEYCHAIN_SERVICE,\n RUNTIME_ENV_VAR,\n SETTINGS_FILE,\n} from \"./constants.js\";\n\nexport type KeySource = \"env\" | \"keychain\" | \"file\";\n\nexport interface ResolvedKey {\n key: string;\n source: KeySource;\n location: string;\n}\n\nexport interface Settings {\n storage?: Exclude<KeySource, \"env\">;\n /** ISO date the user said the key expires, for reminders. */\n keyExpiresOn?: string;\n setupAt?: string;\n}\n\n/** True when Claude Desktop runs us as a desktop extension, where the key comes from its settings, not `setup`. */\nexport const isExtension = (): boolean => process.env[RUNTIME_ENV_VAR] === \"mcpb\";\n\n/** How the user replaces a missing or rejected key, phrased for where we're running. Completes \"Ask the user to …\". */\nexport function keyFixHint(): string {\n return isExtension()\n ? `open Claude Desktop → Settings → Extensions → ${EXTENSION_DISPLAY_NAME}, and paste a new key created at ${KEY_PAGE_URL}`\n : \"run `npx garmin-personal-api-mcp setup` in a terminal to add a new key\";\n}\n\n/** Appended to key errors: without it the model goes looking for the data in a browser instead of telling the user. */\nexport const KEY_STOP_NOTE = \"Only the user can fix this: tell them, and don't try to get the data another way (such as a browser).\";\n\nexport const keychainAvailable = (): boolean =>\n process.platform === \"darwin\" && existsSync(\"/usr/bin/security\");\n\nfunction runSecurity(args: string[], input?: string) {\n return spawnSync(\"/usr/bin/security\", args, { input, encoding: \"utf8\" });\n}\n\nexport function readKeychain(): string | undefined {\n if (!keychainAvailable()) return undefined;\n const res = runSecurity([\"find-generic-password\", \"-a\", KEYCHAIN_ACCOUNT, \"-s\", KEYCHAIN_SERVICE, \"-w\"]);\n const value = res.status === 0 ? res.stdout.trim() : \"\";\n return value || undefined;\n}\n\n/**\n * Writes via `security -i` (commands on stdin) so the key never appears in the\n * process list the way a `-w <key>` argument would.\n */\nexport function writeKeychain(key: string): void {\n const escaped = key.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n const cmd = `add-generic-password -U -a \"${KEYCHAIN_ACCOUNT}\" -s \"${KEYCHAIN_SERVICE}\" -l \"Garmin Connect Personal API key\" -w \"${escaped}\"\\n`;\n const res = runSecurity([\"-i\"], cmd);\n if (res.status !== 0 || /error/i.test(res.stderr)) {\n throw new Error(`Could not save the key to the macOS Keychain: ${res.stderr.trim() || `exit ${res.status}`}`);\n }\n}\n\nexport function deleteKeychain(): boolean {\n if (!keychainAvailable()) return false;\n return runSecurity([\"delete-generic-password\", \"-a\", KEYCHAIN_ACCOUNT, \"-s\", KEYCHAIN_SERVICE]).status === 0;\n}\n\nexport function readKeyFile(path = DEFAULT_KEY_FILE): string | undefined {\n try {\n const value = readFileSync(path, \"utf8\").trim();\n return value || undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function writeKeyFile(key: string, path = DEFAULT_KEY_FILE): void {\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n writeFileSync(path, `${key}\\n`, { mode: 0o600 });\n chmodSync(path, 0o600);\n}\n\nexport function deleteKeyFile(path = DEFAULT_KEY_FILE): boolean {\n if (!existsSync(path)) return false;\n rmSync(path);\n return true;\n}\n\nexport function resolveKey(): ResolvedKey | undefined {\n const env = process.env[API_KEY_ENV_VAR]?.trim();\n if (env) return { key: env, source: \"env\", location: `$${API_KEY_ENV_VAR}` };\n const kc = readKeychain();\n if (kc) return { key: kc, source: \"keychain\", location: `macOS Keychain (${KEYCHAIN_SERVICE})` };\n const file = readKeyFile();\n if (file) return { key: file, source: \"file\", location: DEFAULT_KEY_FILE };\n return undefined;\n}\n\nexport function readSettings(): Settings {\n try {\n return JSON.parse(readFileSync(SETTINGS_FILE, \"utf8\")) as Settings;\n } catch {\n return {};\n }\n}\n\nexport function writeSettings(settings: Settings): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { mode: 0o600 });\n}\n\n/** Days until the key expires, or undefined if unknown. Negative once expired. */\nexport function daysUntilKeyExpiry(settings = readSettings(), now = new Date()): number | undefined {\n if (!settings.keyExpiresOn) return undefined;\n const ms = new Date(`${settings.keyExpiresOn}T23:59:59`).getTime() - now.getTime();\n return Math.floor(ms / 86_400_000);\n}\n\n/** Masked preview such as `gk_a1…9f2c`, safe to print. */\nexport function maskKey(key: string): string {\n if (key.length <= 10) return \"•\".repeat(key.length);\n return `${key.slice(0, 4)}…${key.slice(-4)}`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"keystore.js","sourceRoot":"","sources":["../src/keystore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Where the Personal API key lives. Lookup order:\n * 1. GARMIN_PERSONAL_API_KEY environment variable\n * 2. macOS Keychain (written by `garmin-personal-api-mcp setup`)\n * 3. ~/.config/garmin/connect-personal-api-key (shared with Garmin's official Python client)\n */\nimport { spawnSync } from \"node:child_process\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport {\n API_KEY_ENV_VAR,\n CONFIG_DIR,\n DEFAULT_KEY_FILE,\n EXTENSION_DISPLAY_NAME,\n KEY_PAGE_URL,\n KEYCHAIN_ACCOUNT,\n KEYCHAIN_SERVICE,\n RUNTIME_ENV_VAR,\n SETTINGS_FILE,\n} from \"./constants.js\";\n\nexport type KeySource = \"env\" | \"keychain\" | \"file\";\n\nexport interface ResolvedKey {\n key: string;\n source: KeySource;\n location: string;\n}\n\nexport interface Settings {\n storage?: Exclude<KeySource, \"env\">;\n /** ISO date the user said the key expires, for reminders. */\n keyExpiresOn?: string;\n setupAt?: string;\n}\n\n/** True when Claude Desktop runs us as a desktop extension, where the key comes from its settings, not `setup`. */\nexport const isExtension = (): boolean => process.env[RUNTIME_ENV_VAR] === \"mcpb\";\n\n/** True when a Claude plugin (Claude Code, Cowork) runs us; the key comes from the plugin's userConfig or, failing that, from `setup`. */\nexport const isPlugin = (): boolean => process.env[RUNTIME_ENV_VAR] === \"plugin\";\n\n/** How the user replaces a missing or rejected key, phrased for where we're running. Completes \"Ask the user to …\". */\nexport function keyFixHint(): string {\n if (isExtension()) return `open Claude Desktop → Settings → Extensions → ${EXTENSION_DISPLAY_NAME}, and paste a new key created at ${KEY_PAGE_URL}`;\n if (isPlugin())\n return `create a new key at ${KEY_PAGE_URL}, then either paste it into the plugin's settings (in Claude Code: /plugin → Installed → ${EXTENSION_DISPLAY_NAME} → Configure) or run \\`npx garmin-personal-api-mcp setup\\` in a terminal to save it on this computer`;\n return \"run `npx garmin-personal-api-mcp setup` in a terminal to add a new key\";\n}\n\n/** Appended to key errors: without it the model goes looking for the data in a browser instead of telling the user. */\nexport const KEY_STOP_NOTE = \"Only the user can fix this: tell them, and don't try to get the data another way (such as a browser).\";\n\nexport const keychainAvailable = (): boolean =>\n process.platform === \"darwin\" && existsSync(\"/usr/bin/security\");\n\nfunction runSecurity(args: string[], input?: string) {\n return spawnSync(\"/usr/bin/security\", args, { input, encoding: \"utf8\" });\n}\n\nexport function readKeychain(): string | undefined {\n if (!keychainAvailable()) return undefined;\n const res = runSecurity([\"find-generic-password\", \"-a\", KEYCHAIN_ACCOUNT, \"-s\", KEYCHAIN_SERVICE, \"-w\"]);\n const value = res.status === 0 ? res.stdout.trim() : \"\";\n return value || undefined;\n}\n\n/**\n * Writes via `security -i` (commands on stdin) so the key never appears in the\n * process list the way a `-w <key>` argument would.\n */\nexport function writeKeychain(key: string): void {\n const escaped = key.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n const cmd = `add-generic-password -U -a \"${KEYCHAIN_ACCOUNT}\" -s \"${KEYCHAIN_SERVICE}\" -l \"Garmin Connect Personal API key\" -w \"${escaped}\"\\n`;\n const res = runSecurity([\"-i\"], cmd);\n if (res.status !== 0 || /error/i.test(res.stderr)) {\n throw new Error(`Could not save the key to the macOS Keychain: ${res.stderr.trim() || `exit ${res.status}`}`);\n }\n}\n\nexport function deleteKeychain(): boolean {\n if (!keychainAvailable()) return false;\n return runSecurity([\"delete-generic-password\", \"-a\", KEYCHAIN_ACCOUNT, \"-s\", KEYCHAIN_SERVICE]).status === 0;\n}\n\nexport function readKeyFile(path = DEFAULT_KEY_FILE): string | undefined {\n try {\n const value = readFileSync(path, \"utf8\").trim();\n return value || undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function writeKeyFile(key: string, path = DEFAULT_KEY_FILE): void {\n mkdirSync(dirname(path), { recursive: true, mode: 0o700 });\n writeFileSync(path, `${key}\\n`, { mode: 0o600 });\n chmodSync(path, 0o600);\n}\n\nexport function deleteKeyFile(path = DEFAULT_KEY_FILE): boolean {\n if (!existsSync(path)) return false;\n rmSync(path);\n return true;\n}\n\nexport function resolveKey(): ResolvedKey | undefined {\n const env = process.env[API_KEY_ENV_VAR]?.trim();\n // An app that doesn't fill in plugin settings can pass the template through untouched; that's not a key.\n if (env && !env.startsWith(\"${\")) return { key: env, source: \"env\", location: `$${API_KEY_ENV_VAR}` };\n const kc = readKeychain();\n if (kc) return { key: kc, source: \"keychain\", location: `macOS Keychain (${KEYCHAIN_SERVICE})` };\n const file = readKeyFile();\n if (file) return { key: file, source: \"file\", location: DEFAULT_KEY_FILE };\n return undefined;\n}\n\nexport function readSettings(): Settings {\n try {\n return JSON.parse(readFileSync(SETTINGS_FILE, \"utf8\")) as Settings;\n } catch {\n return {};\n }\n}\n\nexport function writeSettings(settings: Settings): void {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2), { mode: 0o600 });\n}\n\n/** Days until the key expires, or undefined if unknown. Negative once expired. */\nexport function daysUntilKeyExpiry(settings = readSettings(), now = new Date()): number | undefined {\n if (!settings.keyExpiresOn) return undefined;\n const ms = new Date(`${settings.keyExpiresOn}T23:59:59`).getTime() - now.getTime();\n return Math.floor(ms / 86_400_000);\n}\n\n/** Masked preview such as `gk_a1…9f2c`, safe to print. */\nexport function maskKey(key: string): string {\n if (key.length <= 10) return \"•\".repeat(key.length);\n return `${key.slice(0, 4)}…${key.slice(-4)}`;\n}\n"]}
|
package/dist/server.js
CHANGED
|
@@ -3,9 +3,11 @@ import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
|
3
3
|
import { GarminClient } from "./api.js";
|
|
4
4
|
import { TokenManager } from "./auth.js";
|
|
5
5
|
import { SERVER_NAME, VERSION } from "./constants.js";
|
|
6
|
-
import { daysUntilKeyExpiry, isExtension, KEY_STOP_NOTE, keyFixHint, resolveKey } from "./keystore.js";
|
|
6
|
+
import { daysUntilKeyExpiry, isExtension, isPlugin, KEY_STOP_NOTE, keyFixHint, resolveKey } from "./keystore.js";
|
|
7
7
|
import { registerTools } from "./tools.js";
|
|
8
8
|
function noKeyMessage() {
|
|
9
|
+
if (isPlugin())
|
|
10
|
+
return `No Garmin Personal API key is set for this plugin or on this computer. Ask the user to ${keyFixHint()}. ${KEY_STOP_NOTE}`;
|
|
9
11
|
return isExtension()
|
|
10
12
|
? `No Garmin Personal API key is set in this extension. Ask the user to ${keyFixHint()}. ${KEY_STOP_NOTE}`
|
|
11
13
|
: "No Garmin Personal API key is set up on this computer. Ask the user to run `npx garmin-personal-api-mcp setup` in a terminal (it walks them through creating and saving the key), then restart this app. " + KEY_STOP_NOTE;
|
package/dist/server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,aAAa,EAAE,UAAU,EAAkB,UAAU,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAChE,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAkB,UAAU,EAAE,MAAM,eAAe,CAAC;AACjI,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,SAAS,YAAY;IACnB,IAAI,QAAQ,EAAE;QAAE,OAAO,0FAA0F,UAAU,EAAE,KAAK,aAAa,EAAE,CAAC;IAClJ,OAAO,WAAW,EAAE;QAClB,CAAC,CAAC,wEAAwE,UAAU,EAAE,KAAK,aAAa,EAAE;QAC1G,CAAC,CAAC,2MAA2M,GAAG,aAAa,CAAC;AAClO,CAAC;AAED,6FAA6F;AAC7F,SAAS,MAAM;IACb,MAAM,KAAK,GAAG,UAAU,EAAE,CAAC;IAC3B,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,YAAY,EAAE,CAAC,CAAC;IAC5C,OAAO,KAAK,CAAC,GAAG,CAAC;AACnB,CAAC;AAED,iIAAiI;AACjI,MAAM,UAAU,aAAa,CAAC,MAA6B,EAAE,IAAwB;IACnF,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,KAAK,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,CAAC;IAC9E,IAAI,IAAI,GAAG,CAAC;QAAE,OAAO,gDAAgD,UAAU,EAAE,GAAG,CAAC;IACrF,IAAI,IAAI,IAAI,CAAC;QAAE,OAAO,uBAAuB,IAAI,yCAAyC,UAAU,EAAE,GAAG,CAAC;IAC1G,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,IAAI,YAAY,CAAC,MAAM,CAAC,CAAC,CAAC;AAE1D,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAC1B,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,OAAO,EAAE,EACvC;QACE,YAAY,EACV,+pBAA+pB;YAC/pB,aAAa,CAAC,UAAU,EAAE,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;KAC5D,CACF,CAAC;IACF,aAAa,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,UAAU,CAAC,YAAY,CAAC,CAAC;IACzB,MAAM,GAAG,GAAG,UAAU,EAAE,CAAC;IACzB,OAAO,CAAC,KAAK,CAAC,2BAA2B,OAAO,oBAAoB,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;AAC1H,CAAC","sourcesContent":["import { McpServer } from \"@modelcontextprotocol/server\";\nimport { serveStdio } from \"@modelcontextprotocol/server/stdio\";\nimport { GarminClient } from \"./api.js\";\nimport { TokenManager } from \"./auth.js\";\nimport { SERVER_NAME, VERSION } from \"./constants.js\";\nimport { daysUntilKeyExpiry, isExtension, isPlugin, KEY_STOP_NOTE, keyFixHint, type KeySource, resolveKey } from \"./keystore.js\";\nimport { registerTools } from \"./tools.js\";\n\nfunction noKeyMessage(): string {\n if (isPlugin()) return `No Garmin Personal API key is set for this plugin or on this computer. Ask the user to ${keyFixHint()}. ${KEY_STOP_NOTE}`;\n return isExtension()\n ? `No Garmin Personal API key is set in this extension. Ask the user to ${keyFixHint()}. ${KEY_STOP_NOTE}`\n : \"No Garmin Personal API key is set up on this computer. Ask the user to run `npx garmin-personal-api-mcp setup` in a terminal (it walks them through creating and saving the key), then restart this app. \" + KEY_STOP_NOTE;\n}\n\n/** Key is resolved per token refresh, so a key added by `setup` works without restarting. */\nfunction getKey(): string {\n const found = resolveKey();\n if (!found) throw new Error(noKeyMessage());\n return found.key;\n}\n\n/** The expiry date in settings belongs to the key `setup` saved; an env key (e.g. from the extension) may be a different one. */\nexport function keyExpiryNote(source: KeySource | undefined, days: number | undefined): string {\n if (source === undefined || source === \"env\" || days === undefined) return \"\";\n if (days < 0) return ` The saved key has expired; tell the user to ${keyFixHint()}.`;\n if (days <= 7) return ` The key expires in ${days} day(s); mention that the user should ${keyFixHint()}.`;\n return \"\";\n}\n\nconst client = new GarminClient(new TokenManager(getKey));\n\nexport function createServer(): McpServer {\n const server = new McpServer(\n { name: SERVER_NAME, version: VERSION },\n {\n instructions:\n \"Read-only access to the user's own Garmin Connect data through Garmin's official Personal API: activities (with exact splits from the .FIT file), sleep, HRV, Body Battery, stress, steps, weight and nutrition. Start with garmin_get_day for recovery questions and garmin_list_activities → garmin_get_activity_splits for training questions. Dates accept YYYY-MM-DD, 'today', 'yesterday' or 'N days ago'. For VO2 max (as Garmin Connect shows it), training effect and recovery time use garmin_get_performance_metrics, which decodes them from the activity files. Training readiness, training status and race predictions are computed on Garmin's servers and are not available.\" +\n keyExpiryNote(resolveKey()?.source, daysUntilKeyExpiry()),\n },\n );\n registerTools(server, client);\n return server;\n}\n\nexport function startStdio(): void {\n serveStdio(createServer);\n const key = resolveKey();\n console.error(`garmin-personal-api-mcp ${VERSION} on stdio · key: ${key ? key.location : `not set (${keyFixHint()})`}`);\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "garmin-personal-api-mcp",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "MCP server for your own Garmin Connect data (activities with FIT splits, sleep, HRV, Body Battery, weight) via Garmin's official Personal API, with an interactive key setup wizard.",
|
|
5
5
|
"main": "dist/server.js",
|
|
6
6
|
"scripts": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"prepare": "npm run build",
|
|
15
15
|
"check": "tsc --noEmit && vitest run && npm run smoke",
|
|
16
16
|
"pack:mcpb": "node scripts/pack-mcpb.mjs",
|
|
17
|
-
"version": "node scripts/sync-manifest-version.mjs && git add manifest.json"
|
|
17
|
+
"version": "node scripts/sync-manifest-version.mjs && git add manifest.json plugin/.claude-plugin/plugin.json plugin/.mcp.json"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"mcp",
|