garmin-personal-api-mcp 1.0.3 → 1.2.0

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/README.md CHANGED
@@ -46,7 +46,7 @@ Dates can be `YYYY-MM-DD`, `today`, `yesterday` or `N days ago`.
46
46
 
47
47
  | Tool | What it gives Claude |
48
48
  |---|---|
49
- | `garmin_list_activities` | Your activities with distance, time, pace and heart rate. Filter by date and type |
49
+ | `garmin_list_activities` | Your activities with distance, time, pace, heart rate, training effect and Garmin's training load. Filter by date and type |
50
50
  | `garmin_get_activity_splits` | Downloads the original `.FIT` file and computes **exact** km (or mile, or 400 m) splits, interpolated at each boundary with pauses removed, plus per-split HR, cadence and climb, device laps, and a pacing verdict (even, negative split, faded) |
51
51
  | `garmin_get_performance_metrics` | Your **VO2 max** as Garmin Connect shows it, every VO2 max reading in a window, and per-activity aerobic/anaerobic training effect, recovery time and performance condition, decoded from the activity files |
52
52
  | `garmin_get_activity` | Garmin's own summary for one activity |
@@ -70,7 +70,7 @@ Dates can be `YYYY-MM-DD`, `today`, `yesterday` or `N days ago`.
70
70
  | Tool | Try asking |
71
71
  |---|---|
72
72
  | `garmin_get_profile` | "What are my heart-rate zones?" |
73
- | `garmin_list_activities` | "List my runs from the last two weeks." |
73
+ | `garmin_list_activities` | "What was my training load this week, activity by activity?" |
74
74
  | `garmin_get_performance_metrics` | "What's my VO2 max, and how has it moved this month?" |
75
75
  | `garmin_get_activity` | "What was the training effect of my last ride?" |
76
76
  | `garmin_get_activity_splits` | "How was my pacing on yesterday's run? Did I fade?" |
@@ -114,7 +114,15 @@ This is an independent project, not made or endorsed by Garmin. "Garmin" and "Ga
114
114
 
115
115
  ## Claude Code, Cursor and other apps
116
116
 
117
- For any other MCP app, run the server from the command line. You need Node.js 20.3 or later.
117
+ **Claude Code and Cowork:** install the plugin, which adds the server plus a skill that tells Claude how to answer training and recovery questions:
118
+
119
+ ```
120
+ /plugin install garmin-personal-api-mcp --marketplace aaronhaddad/garmin-connect-mcp-claude-connector
121
+ ```
122
+
123
+ 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.
124
+
125
+ **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
126
 
119
127
  ```bash
120
128
  npx garmin-personal-api-mcp setup
package/dist/format.js CHANGED
@@ -54,6 +54,8 @@ export const textResult = (text) => ({ content: [{ type: "text", text }] });
54
54
  const g = (o, k) => o[k];
55
55
  const n = (v) => (typeof v === "number" && Number.isFinite(v) ? v : undefined);
56
56
  const str = (v) => (typeof v === "string" ? v : undefined);
57
+ const round1 = (v) => (v === undefined ? undefined : Math.round(v * 10) / 10);
58
+ const roundInt = (v) => (v === undefined ? undefined : Math.round(v));
57
59
  export function summarizeActivity(raw) {
58
60
  if (!isObj(raw))
59
61
  return undefined;
@@ -81,16 +83,21 @@ export function summarizeActivity(raw) {
81
83
  elevation_gain_m: n(g(summary, "elevationGain")),
82
84
  calories: n(g(summary, "calories")),
83
85
  avg_cadence: n(g(summary, "averageRunningCadenceInStepsPerMinute")) ?? n(g(summary, "averageRunCadence")),
84
- training_effect: n(g(summary, "aerobicTrainingEffect")) ?? n(g(summary, "trainingEffect")),
86
+ training_effect: round1(n(g(summary, "aerobicTrainingEffect")) ?? n(g(summary, "trainingEffect"))),
87
+ anaerobic_training_effect: round1(n(g(summary, "anaerobicTrainingEffect"))),
88
+ training_effect_label: str(g(summary, "trainingEffectLabel"))?.toLowerCase().replace(/_/g, " "),
89
+ training_load: roundInt(n(g(summary, "activityTrainingLoad"))),
85
90
  };
86
91
  }
87
92
  export function activityLine(a) {
88
93
  const bits = [
89
- a.distance_km !== undefined ? `${a.distance_km} km` : undefined,
94
+ a.distance_km ? `${a.distance_km} km` : undefined,
90
95
  a.duration,
91
96
  a.avg_pace,
92
97
  a.avg_hr ? `avg HR ${a.avg_hr}` : undefined,
93
98
  a.elevation_gain_m ? `+${Math.round(a.elevation_gain_m)} m` : undefined,
99
+ a.training_effect !== undefined ? `TE ${a.training_effect}${a.training_effect_label ? ` (${a.training_effect_label})` : ""}` : undefined,
100
+ a.training_load !== undefined ? `load ${a.training_load}` : undefined,
94
101
  ].filter(Boolean);
95
102
  return `- **${a.name ?? "Untitled"}** (${a.type ?? "activity"}, ${a.start_local ?? "?"}) · id ${a.activity_id}\n ${bits.join(" · ")}`;
96
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHARACTER_LIMIT } from \"./constants.js\";\nimport type { Json } from \"./api.js\";\nimport { fmtDuration, fmtPace } from \"./fit.js\";\n\ntype Obj = { [k: string]: Json };\nexport const isObj = (v: unknown): v is Obj => !!v && typeof v === \"object\" && !Array.isArray(v);\n\n/** Drops null/undefined/empty-string fields recursively; keeps zeros and false. */\nexport function compact(v: Json): Json {\n if (Array.isArray(v)) return v.map(compact);\n if (isObj(v)) {\n const out: Obj = {};\n for (const [k, x] of Object.entries(v)) {\n if (x === null || x === undefined || x === \"\") continue;\n out[k] = compact(x);\n }\n return out;\n }\n return v;\n}\n\n/**\n * Keeps a JSON payload under CHARACTER_LIMIT without cutting text mid-structure:\n * long arrays (usually per-minute samples) are evenly downsampled, halving until it fits.\n */\nexport function fitJson(value: Json, limit = CHARACTER_LIMIT): { value: Json; text: string; downsampled: boolean } {\n let v = compact(value);\n let text = JSON.stringify(v);\n if (text.length <= limit) return { value: v, text, downsampled: false };\n for (let cap = 512; cap >= 4; cap = Math.floor(cap / 2)) {\n v = downsample(compact(value), cap);\n text = JSON.stringify(v);\n if (text.length <= limit) return { value: v, text, downsampled: true };\n }\n throw new Error(\n \"The response is too large even after downsampling. Ask for a shorter date range, or use a summary tool such as garmin_get_daily_summaries.\",\n );\n}\n\nfunction downsample(v: Json, cap: number): Json {\n if (Array.isArray(v)) {\n const arr = v.length > cap ? Array.from({ length: cap }, (_, i) => v[Math.floor((i * v.length) / cap)]) : v;\n return arr.map((x) => downsample(x, cap));\n }\n if (isObj(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, downsample(x, cap)]));\n return v;\n}\n\nexport function jsonResult(value: Json, note?: string) {\n const { text, downsampled } = fitJson(value);\n const notes = [note, downsampled ? \"Long sample arrays were evenly downsampled to fit; ask for a single day for full resolution.\" : undefined]\n .filter(Boolean)\n .join(\" \");\n return { content: [{ type: \"text\" as const, text: notes ? `${notes}\\n\\n${text}` : text }] };\n}\n\nexport const textResult = (text: string) => ({ content: [{ type: \"text\" as const, text }] });\n\n// ---- Activity summaries (field names as returned by Garmin Connect's activity list) ----\n\nconst g = (o: Obj, k: string) => o[k];\nconst n = (v: Json | undefined) => (typeof v === \"number\" && Number.isFinite(v) ? v : undefined);\nconst str = (v: Json | undefined) => (typeof v === \"string\" ? v : undefined);\n\nexport interface ActivitySummary {\n activity_id: number | string;\n name?: string;\n type?: string;\n start_local?: string;\n distance_km?: number;\n duration?: string;\n moving_time?: string;\n avg_pace?: string;\n avg_hr?: number;\n max_hr?: number;\n elevation_gain_m?: number;\n calories?: number;\n avg_cadence?: number;\n training_effect?: number;\n}\n\nexport function summarizeActivity(raw: Json): ActivitySummary | undefined {\n if (!isObj(raw)) return undefined;\n const id = g(raw, \"activityId\");\n if (typeof id !== \"number\" && typeof id !== \"string\") return undefined;\n const summary = isObj(raw.summaryDTO) ? raw.summaryDTO : raw; // detail endpoint nests stats\n const type = isObj(raw.activityType) ? str(raw.activityType.typeKey) : isObj(raw.activityTypeDTO) ? str(raw.activityTypeDTO.typeKey) : undefined;\n const dist = n(g(summary, \"distance\"));\n const dur = n(g(summary, \"duration\"));\n const moving = n(g(summary, \"movingDuration\"));\n const speed = n(g(summary, \"averageSpeed\"));\n const isFoot = !!type && /run|walk|hik|trail|treadmill/.test(type);\n return {\n activity_id: id,\n name: str(raw.activityName),\n type,\n start_local: str(g(summary, \"startTimeLocal\")) ?? str(raw.startTimeLocal),\n distance_km: dist !== undefined ? Math.round(dist / 10) / 100 : undefined,\n duration: dur !== undefined ? fmtDuration(dur) : undefined,\n moving_time: moving !== undefined ? fmtDuration(moving) : undefined,\n avg_pace: isFoot && speed ? fmtPace(1000 / speed) : undefined,\n avg_hr: n(g(summary, \"averageHR\")),\n max_hr: n(g(summary, \"maxHR\")),\n elevation_gain_m: n(g(summary, \"elevationGain\")),\n calories: n(g(summary, \"calories\")),\n avg_cadence: n(g(summary, \"averageRunningCadenceInStepsPerMinute\")) ?? n(g(summary, \"averageRunCadence\")),\n training_effect: n(g(summary, \"aerobicTrainingEffect\")) ?? n(g(summary, \"trainingEffect\")),\n };\n}\n\nexport function activityLine(a: ActivitySummary): string {\n const bits = [\n a.distance_km !== undefined ? `${a.distance_km} km` : undefined,\n a.duration,\n a.avg_pace,\n a.avg_hr ? `avg HR ${a.avg_hr}` : undefined,\n a.elevation_gain_m ? `+${Math.round(a.elevation_gain_m)} m` : undefined,\n ].filter(Boolean);\n return `- **${a.name ?? \"Untitled\"}** (${a.type ?? \"activity\"}, ${a.start_local ?? \"?\"}) · id ${a.activity_id}\\n ${bits.join(\" · \")}`;\n}\n"]}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHARACTER_LIMIT } from \"./constants.js\";\nimport type { Json } from \"./api.js\";\nimport { fmtDuration, fmtPace } from \"./fit.js\";\n\ntype Obj = { [k: string]: Json };\nexport const isObj = (v: unknown): v is Obj => !!v && typeof v === \"object\" && !Array.isArray(v);\n\n/** Drops null/undefined/empty-string fields recursively; keeps zeros and false. */\nexport function compact(v: Json): Json {\n if (Array.isArray(v)) return v.map(compact);\n if (isObj(v)) {\n const out: Obj = {};\n for (const [k, x] of Object.entries(v)) {\n if (x === null || x === undefined || x === \"\") continue;\n out[k] = compact(x);\n }\n return out;\n }\n return v;\n}\n\n/**\n * Keeps a JSON payload under CHARACTER_LIMIT without cutting text mid-structure:\n * long arrays (usually per-minute samples) are evenly downsampled, halving until it fits.\n */\nexport function fitJson(value: Json, limit = CHARACTER_LIMIT): { value: Json; text: string; downsampled: boolean } {\n let v = compact(value);\n let text = JSON.stringify(v);\n if (text.length <= limit) return { value: v, text, downsampled: false };\n for (let cap = 512; cap >= 4; cap = Math.floor(cap / 2)) {\n v = downsample(compact(value), cap);\n text = JSON.stringify(v);\n if (text.length <= limit) return { value: v, text, downsampled: true };\n }\n throw new Error(\n \"The response is too large even after downsampling. Ask for a shorter date range, or use a summary tool such as garmin_get_daily_summaries.\",\n );\n}\n\nfunction downsample(v: Json, cap: number): Json {\n if (Array.isArray(v)) {\n const arr = v.length > cap ? Array.from({ length: cap }, (_, i) => v[Math.floor((i * v.length) / cap)]) : v;\n return arr.map((x) => downsample(x, cap));\n }\n if (isObj(v)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, downsample(x, cap)]));\n return v;\n}\n\nexport function jsonResult(value: Json, note?: string) {\n const { text, downsampled } = fitJson(value);\n const notes = [note, downsampled ? \"Long sample arrays were evenly downsampled to fit; ask for a single day for full resolution.\" : undefined]\n .filter(Boolean)\n .join(\" \");\n return { content: [{ type: \"text\" as const, text: notes ? `${notes}\\n\\n${text}` : text }] };\n}\n\nexport const textResult = (text: string) => ({ content: [{ type: \"text\" as const, text }] });\n\n// ---- Activity summaries (field names as returned by Garmin Connect's activity list) ----\n\nconst g = (o: Obj, k: string) => o[k];\nconst n = (v: Json | undefined) => (typeof v === \"number\" && Number.isFinite(v) ? v : undefined);\nconst str = (v: Json | undefined) => (typeof v === \"string\" ? v : undefined);\nconst round1 = (v: number | undefined) => (v === undefined ? undefined : Math.round(v * 10) / 10);\nconst roundInt = (v: number | undefined) => (v === undefined ? undefined : Math.round(v));\n\nexport interface ActivitySummary {\n activity_id: number | string;\n name?: string;\n type?: string;\n start_local?: string;\n distance_km?: number;\n duration?: string;\n moving_time?: string;\n avg_pace?: string;\n avg_hr?: number;\n max_hr?: number;\n elevation_gain_m?: number;\n calories?: number;\n avg_cadence?: number;\n training_effect?: number;\n anaerobic_training_effect?: number;\n /** Garmin's label for the main benefit, e.g. \"lactate threshold\". */\n training_effect_label?: string;\n /** Garmin's per-activity training load (EPOC-based), as Garmin Connect shows it. */\n training_load?: number;\n}\n\nexport function summarizeActivity(raw: Json): ActivitySummary | undefined {\n if (!isObj(raw)) return undefined;\n const id = g(raw, \"activityId\");\n if (typeof id !== \"number\" && typeof id !== \"string\") return undefined;\n const summary = isObj(raw.summaryDTO) ? raw.summaryDTO : raw; // detail endpoint nests stats\n const type = isObj(raw.activityType) ? str(raw.activityType.typeKey) : isObj(raw.activityTypeDTO) ? str(raw.activityTypeDTO.typeKey) : undefined;\n const dist = n(g(summary, \"distance\"));\n const dur = n(g(summary, \"duration\"));\n const moving = n(g(summary, \"movingDuration\"));\n const speed = n(g(summary, \"averageSpeed\"));\n const isFoot = !!type && /run|walk|hik|trail|treadmill/.test(type);\n return {\n activity_id: id,\n name: str(raw.activityName),\n type,\n start_local: str(g(summary, \"startTimeLocal\")) ?? str(raw.startTimeLocal),\n distance_km: dist !== undefined ? Math.round(dist / 10) / 100 : undefined,\n duration: dur !== undefined ? fmtDuration(dur) : undefined,\n moving_time: moving !== undefined ? fmtDuration(moving) : undefined,\n avg_pace: isFoot && speed ? fmtPace(1000 / speed) : undefined,\n avg_hr: n(g(summary, \"averageHR\")),\n max_hr: n(g(summary, \"maxHR\")),\n elevation_gain_m: n(g(summary, \"elevationGain\")),\n calories: n(g(summary, \"calories\")),\n avg_cadence: n(g(summary, \"averageRunningCadenceInStepsPerMinute\")) ?? n(g(summary, \"averageRunCadence\")),\n training_effect: round1(n(g(summary, \"aerobicTrainingEffect\")) ?? n(g(summary, \"trainingEffect\"))),\n anaerobic_training_effect: round1(n(g(summary, \"anaerobicTrainingEffect\"))),\n training_effect_label: str(g(summary, \"trainingEffectLabel\"))?.toLowerCase().replace(/_/g, \" \"),\n training_load: roundInt(n(g(summary, \"activityTrainingLoad\"))),\n };\n}\n\nexport function activityLine(a: ActivitySummary): string {\n const bits = [\n a.distance_km ? `${a.distance_km} km` : undefined,\n a.duration,\n a.avg_pace,\n a.avg_hr ? `avg HR ${a.avg_hr}` : undefined,\n a.elevation_gain_m ? `+${Math.round(a.elevation_gain_m)} m` : undefined,\n a.training_effect !== undefined ? `TE ${a.training_effect}${a.training_effect_label ? ` (${a.training_effect_label})` : \"\"}` : undefined,\n a.training_load !== undefined ? `load ${a.training_load}` : undefined,\n ].filter(Boolean);\n return `- **${a.name ?? \"Untitled\"}** (${a.type ?? \"activity\"}, ${a.start_local ?? \"?\"}) · id ${a.activity_id}\\n ${bits.join(\" · \")}`;\n}\n"]}
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
- return isExtension()
16
- ? `open Claude Desktop → Settings → Extensions → ${EXTENSION_DISPLAY_NAME}, and paste a new key created at ${KEY_PAGE_URL}`
17
- : "run `npx garmin-personal-api-mcp setup` in a terminal to add a new key";
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
- if (env)
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)
@@ -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;
@@ -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;AACvH,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAE3C,SAAS,YAAY;IACnB,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, KEY_STOP_NOTE, keyFixHint, type KeySource, resolveKey } from \"./keystore.js\";\nimport { registerTools } from \"./tools.js\";\n\nfunction noKeyMessage(): string {\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"]}
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/dist/tools.js CHANGED
@@ -32,7 +32,7 @@ export function registerTools(server, client) {
32
32
  // ---------------- Activities ----------------
33
33
  server.registerTool("garmin_list_activities", {
34
34
  title: "List activities",
35
- description: `List recorded activities (runs, gym sessions, rides…), newest first, with distance, time, pace and heart rate.
35
+ description: `List recorded activities (runs, gym sessions, rides…), newest first, with distance, time, pace, heart rate, training effect and Garmin's training load.
36
36
 
37
37
  Examples:
38
38
  - "my runs this month" -> start_date="2026-10-01", activity_type="running"
package/dist/tools.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport type { McpServer } from \"@modelcontextprotocol/server\";\nimport * as z from \"zod/v4\";\nimport {\n ALLOWED_PATH_PREFIXES,\n fetchRange,\n type GarminClient,\n type Json,\n type RangeEndpoint,\n resolveDate,\n resolveRange,\n} from \"./api.js\";\nimport { decodeFit, extractFit, fmtDuration, fmtPace, type PerformanceMetrics, performanceMetrics, summarizeFit } from \"./fit.js\";\nimport { activityLine, isObj, jsonResult, summarizeActivity, textResult } from \"./format.js\";\n\nconst READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } as const;\n\nconst DateArg = z\n .string()\n .min(1)\n .describe('Date as YYYY-MM-DD, or \"today\", \"yesterday\", \"N days ago\" (local time)');\nconst StartArg = DateArg.describe('First day, inclusive: YYYY-MM-DD, \"today\", \"yesterday\" or \"N days ago\"');\nconst EndArg = z.string().optional().describe('Last day, inclusive. Defaults to today. Same formats as start_date');\nconst ActivityId = z\n .union([z.string().regex(/^\\d+$/), z.number().int().positive()])\n .transform(String)\n .describe(\"Numeric Garmin activity ID, from garmin_list_activities\");\nconst Format = z.enum([\"markdown\", \"json\"]).default(\"markdown\").describe(\"'markdown' for a readable summary, 'json' for every field\");\n\nexport function registerTools(server: McpServer, client: GarminClient): void {\n // ---------------- Profile ----------------\n server.registerTool(\n \"garmin_get_profile\",\n {\n title: \"Profile & heart-rate zones\",\n description:\n \"The user's Garmin profile settings (units, weight, height, birth year, lactate threshold where set) plus their heart-rate zones. Call this first when you need zones or units to interpret other data.\",\n inputSchema: z.strictObject({}),\n annotations: READ_ONLY,\n },\n async (_args, ctx) => {\n const s = ctx.mcpReq.signal;\n const [profile, zones] = await Promise.all([client.profileSettings(s), client.heartRateZones(s).catch((e: Error) => ({ error: e.message }))]);\n return jsonResult({ profile, heart_rate_zones: zones as Json });\n },\n );\n\n // ---------------- Activities ----------------\n server.registerTool(\n \"garmin_list_activities\",\n {\n title: \"List activities\",\n description: `List recorded activities (runs, gym sessions, rides…), newest first, with distance, time, pace and heart rate.\n\nExamples:\n - \"my runs this month\" -> start_date=\"2026-10-01\", activity_type=\"running\"\n - \"last 5 activities\" -> limit=5\nUse garmin_get_activity_splits on an activity_id for per-km splits and pacing.`,\n inputSchema: z.strictObject({\n start_date: z.string().optional().describe(\"Only activities on/after this day (YYYY-MM-DD, 'today', 'N days ago')\"),\n end_date: z.string().optional().describe(\"Only activities on/before this day\"),\n activity_type: z\n .string()\n .optional()\n .describe(\"Garmin type key, e.g. running, treadmill_running, trail_running, cycling, indoor_cycling, strength_training, walking\"),\n limit: z.number().int().min(1).max(100).default(20).describe(\"Max activities to return\"),\n offset: z.number().int().min(0).default(0).describe(\"Skip this many, for paging\"),\n response_format: Format,\n }),\n annotations: READ_ONLY,\n },\n async ({ start_date, end_date, activity_type, limit, offset, response_format }, ctx) => {\n const raw = await client.searchActivities(\n {\n start: offset,\n limit,\n startDate: start_date ? resolveDate(start_date) : undefined,\n endDate: end_date ? resolveDate(end_date) : undefined,\n activityType: activity_type,\n },\n ctx.mcpReq.signal,\n );\n const list = Array.isArray(raw) ? raw : isObj(raw) && Array.isArray(raw.activityList) ? raw.activityList : [];\n if (response_format === \"json\") return jsonResult({ offset, count: list.length, has_more: list.length === limit, activities: list });\n const items = list.map(summarizeActivity).filter((a) => a !== undefined);\n if (!items.length) return textResult(\"No activities match. Widen the date range or drop activity_type.\");\n const lines = [`# ${items.length} activities`, \"\", ...items.map(activityLine)];\n if (list.length === limit) lines.push(\"\", `More may exist: call again with offset=${offset + limit}.`);\n return textResult(lines.join(\"\\n\"));\n },\n );\n\n server.registerTool(\n \"garmin_get_activity\",\n {\n title: \"Activity details\",\n description:\n \"Garmin's computed summary for one activity: distance, durations, pace/speed, HR, cadence, elevation, training effect, calories. For per-km splits use garmin_get_activity_splits.\",\n inputSchema: z.strictObject({ activity_id: ActivityId, response_format: Format }),\n annotations: READ_ONLY,\n },\n async ({ activity_id, response_format }, ctx) => {\n const raw = await client.activity(activity_id, ctx.mcpReq.signal);\n if (response_format === \"json\") return jsonResult(raw);\n const a = summarizeActivity(raw);\n if (!a) return jsonResult(raw, \"Could not summarise this activity; raw data follows.\");\n return textResult(\n [\n `# ${a.name ?? \"Activity\"} (${a.type ?? \"?\"})`,\n ...Object.entries(a)\n .filter(([k, v]) => v !== undefined && k !== \"name\" && k !== \"type\")\n .map(([k, v]) => `- ${k.replace(/_/g, \" \")}: ${v}`),\n \"\",\n \"Call again with response_format='json' for every field Garmin returns.\",\n ].join(\"\\n\"),\n );\n },\n );\n\n server.registerTool(\n \"garmin_get_activity_splits\",\n {\n title: \"Splits & pacing analysis\",\n description: `Downloads the original .FIT recording and computes exact distance splits (default 1 km, interpolated at each boundary, pauses excluded), per-split HR, cadence and climb, the device laps, and a pacing verdict (even, negative split, faded).\n\nUse this for \"how was my pacing\", \"did I go out too fast\", \"compare km 1 to km 5\". Works for any GPS or footpod activity recorded on a Garmin device.`,\n inputSchema: z.strictObject({\n activity_id: ActivityId,\n split_meters: z.number().int().min(100).max(10_000).default(1000).describe(\"Split length in metres: 1000 for km, 1609 for miles, 400 for track laps\"),\n response_format: Format,\n }),\n annotations: READ_ONLY,\n },\n async ({ activity_id, split_meters, response_format }, ctx) => {\n const bytes = await client.downloadActivity(activity_id, ctx.mcpReq.signal);\n const summary = summarizeFit(extractFit(bytes), split_meters);\n if (response_format === \"json\") return jsonResult(summary as unknown as Json);\n const s = summary.session;\n const unit = split_meters === 1000 ? \"km\" : `${split_meters} m`;\n const lines = [`# Activity ${activity_id}${s?.sport ? ` (${s.sport})` : \"\"}`];\n if (s) {\n lines.push(\n [\n s.distance_m !== undefined ? `${(s.distance_m / 1000).toFixed(2)} km` : undefined,\n `moving ${fmtDuration(s.timer_s)}`,\n s.distance_m && s.timer_s ? `avg ${fmtPace((s.timer_s / s.distance_m) * 1000)}` : undefined,\n s.avg_hr ? `HR ${s.avg_hr} avg / ${s.max_hr} max` : undefined,\n s.avg_cadence ? `cadence ${Math.round(s.avg_cadence)}` : undefined,\n s.total_ascent_m ? `+${s.total_ascent_m} m` : undefined,\n ]\n .filter(Boolean)\n .join(\" · \"),\n );\n }\n if (summary.paused_s > 5) lines.push(`Paused ${fmtDuration(summary.paused_s)} (excluded from split times).`);\n lines.push(\"\", `| ${unit} | dist | time | pace | avg HR | max HR | cadence | climb |`, \"|---|---|---|---|---|---|---|---|\");\n for (const sp of summary.splits) {\n lines.push(\n `| ${sp.index}${sp.partial ? \"*\" : \"\"} | ${(sp.distance_m / 1000).toFixed(2)} | ${fmtDuration(sp.moving_s)} | ${fmtPace(sp.pace_s_per_km)} | ${sp.avg_hr ?? \"–\"} | ${sp.max_hr ?? \"–\"} | ${sp.avg_cadence ?? \"–\"} | ${sp.elev_gain_m !== undefined ? `+${sp.elev_gain_m}` : \"–\"} |`,\n );\n }\n if (summary.splits.some((x) => x.partial)) lines.push(\"\", \"\\\\* partial split\");\n const p = summary.pacing;\n if (p) {\n lines.push(\n \"\",\n \"## Pacing\",\n `- ${p.verdict}`,\n `- First ${fmtPace(p.first_split_pace_s_per_km)}, last ${fmtPace(p.last_split_pace_s_per_km)}; fastest split ${p.fastest_split}, slowest ${p.slowest_split}`,\n `- Second half vs first half: ${p.half_split_delta_s_per_km > 0 ? \"+\" : \"\"}${p.half_split_delta_s_per_km} s/km`,\n `- Split-to-split variability: ${p.pace_variability_pct}% (under 3% is very even)`,\n );\n }\n if (summary.laps.length > 1) {\n lines.push(\"\", \"## Device laps\", \"| lap | dist | time | pace | avg HR |\", \"|---|---|---|---|---|\");\n for (const l of summary.laps) {\n lines.push(`| ${l.index} | ${(l.distance_m / 1000).toFixed(2)} | ${fmtDuration(l.timer_s)} | ${fmtPace(l.pace_s_per_km)} | ${l.avg_hr ?? \"–\"} |`);\n }\n }\n if (!summary.splits.length) lines.push(\"\", \"No distance data in this recording (e.g. a gym session), so there are no splits.\");\n return textResult(lines.join(\"\\n\"));\n },\n );\n\n server.registerTool(\n \"garmin_get_performance_metrics\",\n {\n title: \"VO2 max, training effect & recovery\",\n description: `Garmin's own VO2 max, read the way Garmin Connect shows it, plus per-activity aerobic/anaerobic training effect, recovery time and performance condition. Decoded from the original .FIT files, since the Personal API has no VO2 max endpoint.\n\nReturns the current VO2 max (whole number, as Garmin's apps round it, plus the precise value), every new VO2 max reading in the window (Garmin only updates it on qualifying runs and power-meter rides), and a row per activity.\n\nExamples:\n - \"what's my VO2 max?\" / \"how is my VO2 max trending?\" -> last_n=20\n - \"how hard was today's workout, how long to recover?\" -> last_n=1\nEach activity costs one file download, so keep last_n small unless a trend is asked for.`,\n inputSchema: z.strictObject({\n activity_id: ActivityId.optional().describe(\"One activity, from garmin_list_activities. Omit to scan recent activities\"),\n last_n: z.number().int().min(1).max(30).default(10).describe(\"How many recent activities to scan (all types)\"),\n activity_type: z.string().optional().describe(\"Only this Garmin type key, e.g. running, cycling\"),\n response_format: Format,\n }),\n annotations: READ_ONLY,\n },\n async ({ activity_id, last_n, activity_type, response_format }, ctx) => {\n const signal = ctx.mcpReq.signal;\n const targets: { id: string; name?: string; type?: string; start?: string }[] = [];\n if (activity_id) {\n const a = summarizeActivity(await client.activity(activity_id, signal));\n targets.push({ id: activity_id, name: a?.name, type: a?.type, start: a?.start_local });\n } else {\n const raw = await client.searchActivities({ limit: last_n, activityType: activity_type }, signal);\n const list = Array.isArray(raw) ? raw : isObj(raw) && Array.isArray(raw.activityList) ? raw.activityList : [];\n for (const a of list.map(summarizeActivity)) if (a) targets.push({ id: String(a.activity_id), name: a.name, type: a.type, start: a.start_local });\n }\n const rows = await mapLimit(targets, 3, async (t) => {\n try {\n return { ...t, m: performanceMetrics(decodeFit(extractFit(await client.downloadActivity(t.id, signal)))) };\n } catch (e) {\n return { ...t, skipped: (e as Error).message };\n }\n });\n const ok = rows.filter((r): r is typeof r & { m: PerformanceMetrics } => \"m\" in r && !!r.m);\n const readings = ok.filter((r) => r.m.new_vo2max_reading && r.m.vo2max_after);\n const newest = ok[0];\n const current = newest?.m.new_vo2max_reading ? newest.m.vo2max_after : (newest?.m.vo2max_before ?? readings[0]?.m.vo2max_after);\n const result = {\n vo2max: current === undefined ? undefined : { value: Math.round(current), precise: current, as_of: newest?.start?.slice(0, 10), last_reading: readings[0]?.start?.slice(0, 10) },\n vo2max_readings: readings.map((r) => ({\n date: r.start?.slice(0, 10),\n activity_id: r.id,\n type: r.type,\n vo2max: Math.round(r.m.vo2max_after!),\n precise: r.m.vo2max_after,\n change: r.m.vo2max_before !== undefined ? Math.round((r.m.vo2max_after! - r.m.vo2max_before) * 100) / 100 : undefined,\n })),\n activities: ok.map((r) => ({ date: r.start, activity_id: r.id, name: r.name, type: r.type, ...r.m })),\n skipped: rows.filter((r) => \"skipped\" in r).map((r) => ({ activity_id: r.id, type: r.type, reason: (r as { skipped: string }).skipped })),\n note: \"VO2 max and training effect match Garmin Connect. Recovery time and performance condition come from undocumented FIT fields; treat them as approximate.\",\n };\n if (response_format === \"json\") return jsonResult(result as unknown as Json);\n\n const lines: string[] = [];\n if (result.vo2max) {\n const v = result.vo2max;\n lines.push(`# VO2 max: ${v.value} ml/kg/min`, `Precise ${v.precise.toFixed(2)} as of ${v.as_of}${v.last_reading ? `; last updated by the ${v.last_reading} activity` : \"; no new reading in this window\"}.`);\n } else {\n lines.push(\"# VO2 max: not available\", \"None of these activities carry a VO2 max. Garmin needs a qualifying outdoor run (or a ride with a power meter) to estimate one.\");\n }\n if (result.vo2max_readings.length) {\n lines.push(\"\", \"## VO2 max readings\", \"| date | activity | VO2 max | change |\", \"|---|---|---|---|\");\n for (const r of result.vo2max_readings) lines.push(`| ${r.date} | ${r.type ?? \"?\"} | ${r.vo2max} (${r.precise?.toFixed(2)}) | ${r.change === undefined ? \"–\" : `${r.change > 0 ? \"+\" : \"\"}${r.change.toFixed(2)}`} |`);\n }\n if (ok.length) {\n lines.push(\"\", \"## Per activity\", \"| date | activity | aerobic TE | anaerobic TE | recovery | perf. condition |\", \"|---|---|---|---|---|---|\");\n for (const r of ok) {\n const m = r.m;\n lines.push(`| ${r.start?.slice(0, 16) ?? \"?\"} | ${r.name ?? r.type ?? r.id} | ${m.training_effect_aerobic ?? \"–\"} | ${m.training_effect_anaerobic ?? \"–\"} | ${m.recovery_time_h !== undefined ? `${m.recovery_time_h} h` : \"–\"} | ${m.performance_condition !== undefined ? `${m.performance_condition > 0 ? \"+\" : \"\"}${m.performance_condition}` : \"–\"} |`);\n }\n }\n if (result.skipped.length) lines.push(\"\", `Skipped ${result.skipped.length} without a FIT file (e.g. manual entries): ${result.skipped.map((s) => s.type ?? s.activity_id).join(\", \")}.`);\n lines.push(\"\", `_${result.note}_`);\n return textResult(lines.join(\"\\n\"));\n },\n );\n\n server.registerTool(\n \"garmin_download_activity_fit\",\n {\n title: \"Save original activity file\",\n description:\n \"Saves an activity's original file (Garmin's ZIP containing the .fit) to a folder on this computer, for import into other tools such as Runalyze or Golden Cheetah. Returns the saved path.\",\n inputSchema: z.strictObject({\n activity_id: ActivityId,\n directory: z.string().optional().describe(\"Folder to save into. Defaults to ~/Downloads/garmin\"),\n }),\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async ({ activity_id, directory }, ctx) => {\n const bytes = await client.downloadActivity(activity_id, ctx.mcpReq.signal);\n const dir = resolve((directory ?? join(homedir(), \"Downloads\", \"garmin\")).replace(/^~(?=$|\\/)/, homedir()));\n mkdirSync(dir, { recursive: true });\n const isZip = bytes[0] === 0x50 && bytes[1] === 0x4b;\n const file = join(dir, `${activity_id}.${isZip ? \"zip\" : \"fit\"}`);\n writeFileSync(file, bytes);\n return textResult(`Saved ${(bytes.length / 1024).toFixed(0)} KB to ${file}`);\n },\n );\n\n // ---------------- Day overview ----------------\n server.registerTool(\n \"garmin_get_day\",\n {\n title: \"One day at a glance\",\n description:\n \"Everything for one day in one call: daily summary (steps, calories, resting HR, stress, intensity minutes), sleep, overnight HRV, Body Battery and activities. Best first call for 'how recovered am I today' or 'how was yesterday'. Parts Garmin has no data for are omitted.\",\n inputSchema: z.strictObject({ date: DateArg.default(\"today\") }),\n annotations: READ_ONLY,\n },\n async ({ date }, ctx) => {\n const d = resolveDate(date);\n const s = ctx.mcpReq.signal;\n const settle = async (p: Promise<Json>) => p.catch((e: Error) => ({ unavailable: e.message }) as Json);\n const [summary, sleep, hrv, bodyBattery, activities] = await Promise.all([\n settle(client.range(\"dailySummaries\", d, d, s)),\n settle(client.dailySleep(d, s)),\n settle(client.dailyHrv(d, s)),\n settle(client.range(\"bodyBattery\", d, d, s)),\n settle(client.searchActivities({ startDate: d, endDate: d, limit: 20 }, s)),\n ]);\n const acts = (Array.isArray(activities) ? activities : []).map(summarizeActivity).filter(Boolean) as unknown as Json[];\n return jsonResult({ date: d, daily_summary: summary, sleep, hrv, body_battery: bodyBattery, activities: acts.length ? acts : activities });\n },\n );\n\n // ---------------- Ranges ----------------\n const rangeTool = (name: string, endpoint: RangeEndpoint, title: string, description: string) =>\n server.registerTool(\n name,\n {\n title,\n description: `${description}\\n\\nLong ranges are fetched in chunks automatically (up to 366 days).`,\n inputSchema: z.strictObject({ start_date: StartArg, end_date: EndArg }),\n annotations: READ_ONLY,\n },\n async ({ start_date, end_date }, ctx) => {\n const [a, b] = resolveRange(start_date, end_date);\n return jsonResult(await fetchRange(client, endpoint, a, b, ctx.mcpReq.signal), `${title}, ${a} to ${b}.`);\n },\n );\n\n rangeTool(\n \"garmin_get_daily_summaries\",\n \"dailySummaries\",\n \"Daily summaries\",\n \"Per-day totals: steps, distance, active and total calories, resting/min/max heart rate, average and max stress, intensity minutes, floors, Body Battery high/low. Best tool for trends across weeks.\",\n );\n rangeTool(\"garmin_get_sleep\", \"sleep\", \"Sleep\", \"Per-night sleep: duration, deep/light/REM/awake time, sleep score, and timing.\");\n rangeTool(\"garmin_get_hrv\", \"hrv\", \"HRV\", \"Per-night heart-rate variability: last-night average, weekly average, baseline range and status (balanced, unbalanced, low).\");\n rangeTool(\"garmin_get_body_battery\", \"bodyBattery\", \"Body Battery\", \"Per-day Body Battery: charged, drained, highest and lowest values.\");\n rangeTool(\"garmin_get_stress\", \"stress\", \"Stress\", \"Per-day all-day stress levels.\");\n rangeTool(\"garmin_get_steps\", \"steps\", \"Steps\", \"Per-day step totals and goals.\");\n rangeTool(\"garmin_get_hydration\", \"hydration\", \"Hydration\", \"Per-day logged water intake and goal.\");\n rangeTool(\n \"garmin_get_weight\",\n \"weight\",\n \"Weight & body composition\",\n \"Every weigh-in in the range (Garmin usually reports weight in grams: divide by 1000 for kg), with BMI, body fat, muscle mass and water where a smart scale recorded them. Use for bulk/cut progress.\",\n );\n rangeTool(\"garmin_get_weight_goals\", \"weightGoals\", \"Weight goals\", \"Weight goals in effect during the range.\");\n rangeTool(\n \"garmin_get_wellness_activities\",\n \"wellnessActivities\",\n \"Wellness activities\",\n \"Wellness activity summaries (e.g. breathwork and relaxation sessions) for the range.\",\n );\n\n // ---------------- Single-day detail ----------------\n const dayTool = (name: string, title: string, description: string, fn: (d: string, s: AbortSignal) => Promise<Json>) =>\n server.registerTool(\n name,\n { title, description, inputSchema: z.strictObject({ date: DateArg.default(\"today\") }), annotations: READ_ONLY },\n async ({ date }, ctx) => {\n const d = resolveDate(date);\n return jsonResult(await fn(d, ctx.mcpReq.signal), `${title}, ${d}.`);\n },\n );\n\n dayTool(\"garmin_get_sleep_detail\", \"Sleep detail\", \"One night's sleep in full: stages timeline, sleep score breakdown, respiration, restless moments, SpO2 and HR during sleep.\", client.dailySleep);\n dayTool(\"garmin_get_heart_rate\", \"Heart rate (all day)\", \"One day's resting HR and the all-day heart-rate samples.\", client.dailyHeartRate);\n dayTool(\"garmin_get_spo2\", \"Pulse Ox\", \"One day's SpO2 readings and acclimation.\", client.dailySpo2);\n dayTool(\"garmin_get_fitness_age\", \"Fitness age\", \"Garmin's fitness age estimate and its inputs for a date.\", client.fitnessAge);\n dayTool(\"garmin_get_nutrition\", \"Food log\", \"Foods and macros logged in Garmin Connect's nutrition log for one day.\", client.foodLogs);\n\n // ---------------- Escape hatch ----------------\n server.registerTool(\n \"garmin_api_get\",\n {\n title: \"Raw Personal API GET\",\n description: `Call any documented Personal API endpoint directly, for data the other tools don't cover (e.g. menstrual cycle summaries, activity workouts).\nPath must start with one of: ${ALLOWED_PATH_PREFIXES.join(\", \")}.\nExample: path=\"/activity-service/activity/123456/workouts\"`,\n inputSchema: z.strictObject({\n path: z.string().startsWith(\"/\").describe(\"API path beginning with /\"),\n params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe(\"Query parameters\"),\n }),\n annotations: READ_ONLY,\n },\n async ({ path, params }, ctx) => {\n if (path.includes(\"..\") || !ALLOWED_PATH_PREFIXES.some((p) => path.startsWith(p))) {\n throw new Error(`Path not allowed. It must start with one of ${ALLOWED_PATH_PREFIXES.join(\", \")}.`);\n }\n return jsonResult(await client.get(path, params, ctx.mcpReq.signal));\n },\n );\n}\n\n/** Runs `fn` over `items` with at most `limit` in flight, keeping input order. */\nasync function mapLimit<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const out: R[] = new Array(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const i = next++;\n out[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return out;\n}\n"]}
1
+ {"version":3,"file":"tools.js","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport type { McpServer } from \"@modelcontextprotocol/server\";\nimport * as z from \"zod/v4\";\nimport {\n ALLOWED_PATH_PREFIXES,\n fetchRange,\n type GarminClient,\n type Json,\n type RangeEndpoint,\n resolveDate,\n resolveRange,\n} from \"./api.js\";\nimport { decodeFit, extractFit, fmtDuration, fmtPace, type PerformanceMetrics, performanceMetrics, summarizeFit } from \"./fit.js\";\nimport { activityLine, isObj, jsonResult, summarizeActivity, textResult } from \"./format.js\";\n\nconst READ_ONLY = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true } as const;\n\nconst DateArg = z\n .string()\n .min(1)\n .describe('Date as YYYY-MM-DD, or \"today\", \"yesterday\", \"N days ago\" (local time)');\nconst StartArg = DateArg.describe('First day, inclusive: YYYY-MM-DD, \"today\", \"yesterday\" or \"N days ago\"');\nconst EndArg = z.string().optional().describe('Last day, inclusive. Defaults to today. Same formats as start_date');\nconst ActivityId = z\n .union([z.string().regex(/^\\d+$/), z.number().int().positive()])\n .transform(String)\n .describe(\"Numeric Garmin activity ID, from garmin_list_activities\");\nconst Format = z.enum([\"markdown\", \"json\"]).default(\"markdown\").describe(\"'markdown' for a readable summary, 'json' for every field\");\n\nexport function registerTools(server: McpServer, client: GarminClient): void {\n // ---------------- Profile ----------------\n server.registerTool(\n \"garmin_get_profile\",\n {\n title: \"Profile & heart-rate zones\",\n description:\n \"The user's Garmin profile settings (units, weight, height, birth year, lactate threshold where set) plus their heart-rate zones. Call this first when you need zones or units to interpret other data.\",\n inputSchema: z.strictObject({}),\n annotations: READ_ONLY,\n },\n async (_args, ctx) => {\n const s = ctx.mcpReq.signal;\n const [profile, zones] = await Promise.all([client.profileSettings(s), client.heartRateZones(s).catch((e: Error) => ({ error: e.message }))]);\n return jsonResult({ profile, heart_rate_zones: zones as Json });\n },\n );\n\n // ---------------- Activities ----------------\n server.registerTool(\n \"garmin_list_activities\",\n {\n title: \"List activities\",\n description: `List recorded activities (runs, gym sessions, rides…), newest first, with distance, time, pace, heart rate, training effect and Garmin's training load.\n\nExamples:\n - \"my runs this month\" -> start_date=\"2026-10-01\", activity_type=\"running\"\n - \"last 5 activities\" -> limit=5\nUse garmin_get_activity_splits on an activity_id for per-km splits and pacing.`,\n inputSchema: z.strictObject({\n start_date: z.string().optional().describe(\"Only activities on/after this day (YYYY-MM-DD, 'today', 'N days ago')\"),\n end_date: z.string().optional().describe(\"Only activities on/before this day\"),\n activity_type: z\n .string()\n .optional()\n .describe(\"Garmin type key, e.g. running, treadmill_running, trail_running, cycling, indoor_cycling, strength_training, walking\"),\n limit: z.number().int().min(1).max(100).default(20).describe(\"Max activities to return\"),\n offset: z.number().int().min(0).default(0).describe(\"Skip this many, for paging\"),\n response_format: Format,\n }),\n annotations: READ_ONLY,\n },\n async ({ start_date, end_date, activity_type, limit, offset, response_format }, ctx) => {\n const raw = await client.searchActivities(\n {\n start: offset,\n limit,\n startDate: start_date ? resolveDate(start_date) : undefined,\n endDate: end_date ? resolveDate(end_date) : undefined,\n activityType: activity_type,\n },\n ctx.mcpReq.signal,\n );\n const list = Array.isArray(raw) ? raw : isObj(raw) && Array.isArray(raw.activityList) ? raw.activityList : [];\n if (response_format === \"json\") return jsonResult({ offset, count: list.length, has_more: list.length === limit, activities: list });\n const items = list.map(summarizeActivity).filter((a) => a !== undefined);\n if (!items.length) return textResult(\"No activities match. Widen the date range or drop activity_type.\");\n const lines = [`# ${items.length} activities`, \"\", ...items.map(activityLine)];\n if (list.length === limit) lines.push(\"\", `More may exist: call again with offset=${offset + limit}.`);\n return textResult(lines.join(\"\\n\"));\n },\n );\n\n server.registerTool(\n \"garmin_get_activity\",\n {\n title: \"Activity details\",\n description:\n \"Garmin's computed summary for one activity: distance, durations, pace/speed, HR, cadence, elevation, training effect, calories. For per-km splits use garmin_get_activity_splits.\",\n inputSchema: z.strictObject({ activity_id: ActivityId, response_format: Format }),\n annotations: READ_ONLY,\n },\n async ({ activity_id, response_format }, ctx) => {\n const raw = await client.activity(activity_id, ctx.mcpReq.signal);\n if (response_format === \"json\") return jsonResult(raw);\n const a = summarizeActivity(raw);\n if (!a) return jsonResult(raw, \"Could not summarise this activity; raw data follows.\");\n return textResult(\n [\n `# ${a.name ?? \"Activity\"} (${a.type ?? \"?\"})`,\n ...Object.entries(a)\n .filter(([k, v]) => v !== undefined && k !== \"name\" && k !== \"type\")\n .map(([k, v]) => `- ${k.replace(/_/g, \" \")}: ${v}`),\n \"\",\n \"Call again with response_format='json' for every field Garmin returns.\",\n ].join(\"\\n\"),\n );\n },\n );\n\n server.registerTool(\n \"garmin_get_activity_splits\",\n {\n title: \"Splits & pacing analysis\",\n description: `Downloads the original .FIT recording and computes exact distance splits (default 1 km, interpolated at each boundary, pauses excluded), per-split HR, cadence and climb, the device laps, and a pacing verdict (even, negative split, faded).\n\nUse this for \"how was my pacing\", \"did I go out too fast\", \"compare km 1 to km 5\". Works for any GPS or footpod activity recorded on a Garmin device.`,\n inputSchema: z.strictObject({\n activity_id: ActivityId,\n split_meters: z.number().int().min(100).max(10_000).default(1000).describe(\"Split length in metres: 1000 for km, 1609 for miles, 400 for track laps\"),\n response_format: Format,\n }),\n annotations: READ_ONLY,\n },\n async ({ activity_id, split_meters, response_format }, ctx) => {\n const bytes = await client.downloadActivity(activity_id, ctx.mcpReq.signal);\n const summary = summarizeFit(extractFit(bytes), split_meters);\n if (response_format === \"json\") return jsonResult(summary as unknown as Json);\n const s = summary.session;\n const unit = split_meters === 1000 ? \"km\" : `${split_meters} m`;\n const lines = [`# Activity ${activity_id}${s?.sport ? ` (${s.sport})` : \"\"}`];\n if (s) {\n lines.push(\n [\n s.distance_m !== undefined ? `${(s.distance_m / 1000).toFixed(2)} km` : undefined,\n `moving ${fmtDuration(s.timer_s)}`,\n s.distance_m && s.timer_s ? `avg ${fmtPace((s.timer_s / s.distance_m) * 1000)}` : undefined,\n s.avg_hr ? `HR ${s.avg_hr} avg / ${s.max_hr} max` : undefined,\n s.avg_cadence ? `cadence ${Math.round(s.avg_cadence)}` : undefined,\n s.total_ascent_m ? `+${s.total_ascent_m} m` : undefined,\n ]\n .filter(Boolean)\n .join(\" · \"),\n );\n }\n if (summary.paused_s > 5) lines.push(`Paused ${fmtDuration(summary.paused_s)} (excluded from split times).`);\n lines.push(\"\", `| ${unit} | dist | time | pace | avg HR | max HR | cadence | climb |`, \"|---|---|---|---|---|---|---|---|\");\n for (const sp of summary.splits) {\n lines.push(\n `| ${sp.index}${sp.partial ? \"*\" : \"\"} | ${(sp.distance_m / 1000).toFixed(2)} | ${fmtDuration(sp.moving_s)} | ${fmtPace(sp.pace_s_per_km)} | ${sp.avg_hr ?? \"–\"} | ${sp.max_hr ?? \"–\"} | ${sp.avg_cadence ?? \"–\"} | ${sp.elev_gain_m !== undefined ? `+${sp.elev_gain_m}` : \"–\"} |`,\n );\n }\n if (summary.splits.some((x) => x.partial)) lines.push(\"\", \"\\\\* partial split\");\n const p = summary.pacing;\n if (p) {\n lines.push(\n \"\",\n \"## Pacing\",\n `- ${p.verdict}`,\n `- First ${fmtPace(p.first_split_pace_s_per_km)}, last ${fmtPace(p.last_split_pace_s_per_km)}; fastest split ${p.fastest_split}, slowest ${p.slowest_split}`,\n `- Second half vs first half: ${p.half_split_delta_s_per_km > 0 ? \"+\" : \"\"}${p.half_split_delta_s_per_km} s/km`,\n `- Split-to-split variability: ${p.pace_variability_pct}% (under 3% is very even)`,\n );\n }\n if (summary.laps.length > 1) {\n lines.push(\"\", \"## Device laps\", \"| lap | dist | time | pace | avg HR |\", \"|---|---|---|---|---|\");\n for (const l of summary.laps) {\n lines.push(`| ${l.index} | ${(l.distance_m / 1000).toFixed(2)} | ${fmtDuration(l.timer_s)} | ${fmtPace(l.pace_s_per_km)} | ${l.avg_hr ?? \"–\"} |`);\n }\n }\n if (!summary.splits.length) lines.push(\"\", \"No distance data in this recording (e.g. a gym session), so there are no splits.\");\n return textResult(lines.join(\"\\n\"));\n },\n );\n\n server.registerTool(\n \"garmin_get_performance_metrics\",\n {\n title: \"VO2 max, training effect & recovery\",\n description: `Garmin's own VO2 max, read the way Garmin Connect shows it, plus per-activity aerobic/anaerobic training effect, recovery time and performance condition. Decoded from the original .FIT files, since the Personal API has no VO2 max endpoint.\n\nReturns the current VO2 max (whole number, as Garmin's apps round it, plus the precise value), every new VO2 max reading in the window (Garmin only updates it on qualifying runs and power-meter rides), and a row per activity.\n\nExamples:\n - \"what's my VO2 max?\" / \"how is my VO2 max trending?\" -> last_n=20\n - \"how hard was today's workout, how long to recover?\" -> last_n=1\nEach activity costs one file download, so keep last_n small unless a trend is asked for.`,\n inputSchema: z.strictObject({\n activity_id: ActivityId.optional().describe(\"One activity, from garmin_list_activities. Omit to scan recent activities\"),\n last_n: z.number().int().min(1).max(30).default(10).describe(\"How many recent activities to scan (all types)\"),\n activity_type: z.string().optional().describe(\"Only this Garmin type key, e.g. running, cycling\"),\n response_format: Format,\n }),\n annotations: READ_ONLY,\n },\n async ({ activity_id, last_n, activity_type, response_format }, ctx) => {\n const signal = ctx.mcpReq.signal;\n const targets: { id: string; name?: string; type?: string; start?: string }[] = [];\n if (activity_id) {\n const a = summarizeActivity(await client.activity(activity_id, signal));\n targets.push({ id: activity_id, name: a?.name, type: a?.type, start: a?.start_local });\n } else {\n const raw = await client.searchActivities({ limit: last_n, activityType: activity_type }, signal);\n const list = Array.isArray(raw) ? raw : isObj(raw) && Array.isArray(raw.activityList) ? raw.activityList : [];\n for (const a of list.map(summarizeActivity)) if (a) targets.push({ id: String(a.activity_id), name: a.name, type: a.type, start: a.start_local });\n }\n const rows = await mapLimit(targets, 3, async (t) => {\n try {\n return { ...t, m: performanceMetrics(decodeFit(extractFit(await client.downloadActivity(t.id, signal)))) };\n } catch (e) {\n return { ...t, skipped: (e as Error).message };\n }\n });\n const ok = rows.filter((r): r is typeof r & { m: PerformanceMetrics } => \"m\" in r && !!r.m);\n const readings = ok.filter((r) => r.m.new_vo2max_reading && r.m.vo2max_after);\n const newest = ok[0];\n const current = newest?.m.new_vo2max_reading ? newest.m.vo2max_after : (newest?.m.vo2max_before ?? readings[0]?.m.vo2max_after);\n const result = {\n vo2max: current === undefined ? undefined : { value: Math.round(current), precise: current, as_of: newest?.start?.slice(0, 10), last_reading: readings[0]?.start?.slice(0, 10) },\n vo2max_readings: readings.map((r) => ({\n date: r.start?.slice(0, 10),\n activity_id: r.id,\n type: r.type,\n vo2max: Math.round(r.m.vo2max_after!),\n precise: r.m.vo2max_after,\n change: r.m.vo2max_before !== undefined ? Math.round((r.m.vo2max_after! - r.m.vo2max_before) * 100) / 100 : undefined,\n })),\n activities: ok.map((r) => ({ date: r.start, activity_id: r.id, name: r.name, type: r.type, ...r.m })),\n skipped: rows.filter((r) => \"skipped\" in r).map((r) => ({ activity_id: r.id, type: r.type, reason: (r as { skipped: string }).skipped })),\n note: \"VO2 max and training effect match Garmin Connect. Recovery time and performance condition come from undocumented FIT fields; treat them as approximate.\",\n };\n if (response_format === \"json\") return jsonResult(result as unknown as Json);\n\n const lines: string[] = [];\n if (result.vo2max) {\n const v = result.vo2max;\n lines.push(`# VO2 max: ${v.value} ml/kg/min`, `Precise ${v.precise.toFixed(2)} as of ${v.as_of}${v.last_reading ? `; last updated by the ${v.last_reading} activity` : \"; no new reading in this window\"}.`);\n } else {\n lines.push(\"# VO2 max: not available\", \"None of these activities carry a VO2 max. Garmin needs a qualifying outdoor run (or a ride with a power meter) to estimate one.\");\n }\n if (result.vo2max_readings.length) {\n lines.push(\"\", \"## VO2 max readings\", \"| date | activity | VO2 max | change |\", \"|---|---|---|---|\");\n for (const r of result.vo2max_readings) lines.push(`| ${r.date} | ${r.type ?? \"?\"} | ${r.vo2max} (${r.precise?.toFixed(2)}) | ${r.change === undefined ? \"–\" : `${r.change > 0 ? \"+\" : \"\"}${r.change.toFixed(2)}`} |`);\n }\n if (ok.length) {\n lines.push(\"\", \"## Per activity\", \"| date | activity | aerobic TE | anaerobic TE | recovery | perf. condition |\", \"|---|---|---|---|---|---|\");\n for (const r of ok) {\n const m = r.m;\n lines.push(`| ${r.start?.slice(0, 16) ?? \"?\"} | ${r.name ?? r.type ?? r.id} | ${m.training_effect_aerobic ?? \"–\"} | ${m.training_effect_anaerobic ?? \"–\"} | ${m.recovery_time_h !== undefined ? `${m.recovery_time_h} h` : \"–\"} | ${m.performance_condition !== undefined ? `${m.performance_condition > 0 ? \"+\" : \"\"}${m.performance_condition}` : \"–\"} |`);\n }\n }\n if (result.skipped.length) lines.push(\"\", `Skipped ${result.skipped.length} without a FIT file (e.g. manual entries): ${result.skipped.map((s) => s.type ?? s.activity_id).join(\", \")}.`);\n lines.push(\"\", `_${result.note}_`);\n return textResult(lines.join(\"\\n\"));\n },\n );\n\n server.registerTool(\n \"garmin_download_activity_fit\",\n {\n title: \"Save original activity file\",\n description:\n \"Saves an activity's original file (Garmin's ZIP containing the .fit) to a folder on this computer, for import into other tools such as Runalyze or Golden Cheetah. Returns the saved path.\",\n inputSchema: z.strictObject({\n activity_id: ActivityId,\n directory: z.string().optional().describe(\"Folder to save into. Defaults to ~/Downloads/garmin\"),\n }),\n annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },\n },\n async ({ activity_id, directory }, ctx) => {\n const bytes = await client.downloadActivity(activity_id, ctx.mcpReq.signal);\n const dir = resolve((directory ?? join(homedir(), \"Downloads\", \"garmin\")).replace(/^~(?=$|\\/)/, homedir()));\n mkdirSync(dir, { recursive: true });\n const isZip = bytes[0] === 0x50 && bytes[1] === 0x4b;\n const file = join(dir, `${activity_id}.${isZip ? \"zip\" : \"fit\"}`);\n writeFileSync(file, bytes);\n return textResult(`Saved ${(bytes.length / 1024).toFixed(0)} KB to ${file}`);\n },\n );\n\n // ---------------- Day overview ----------------\n server.registerTool(\n \"garmin_get_day\",\n {\n title: \"One day at a glance\",\n description:\n \"Everything for one day in one call: daily summary (steps, calories, resting HR, stress, intensity minutes), sleep, overnight HRV, Body Battery and activities. Best first call for 'how recovered am I today' or 'how was yesterday'. Parts Garmin has no data for are omitted.\",\n inputSchema: z.strictObject({ date: DateArg.default(\"today\") }),\n annotations: READ_ONLY,\n },\n async ({ date }, ctx) => {\n const d = resolveDate(date);\n const s = ctx.mcpReq.signal;\n const settle = async (p: Promise<Json>) => p.catch((e: Error) => ({ unavailable: e.message }) as Json);\n const [summary, sleep, hrv, bodyBattery, activities] = await Promise.all([\n settle(client.range(\"dailySummaries\", d, d, s)),\n settle(client.dailySleep(d, s)),\n settle(client.dailyHrv(d, s)),\n settle(client.range(\"bodyBattery\", d, d, s)),\n settle(client.searchActivities({ startDate: d, endDate: d, limit: 20 }, s)),\n ]);\n const acts = (Array.isArray(activities) ? activities : []).map(summarizeActivity).filter(Boolean) as unknown as Json[];\n return jsonResult({ date: d, daily_summary: summary, sleep, hrv, body_battery: bodyBattery, activities: acts.length ? acts : activities });\n },\n );\n\n // ---------------- Ranges ----------------\n const rangeTool = (name: string, endpoint: RangeEndpoint, title: string, description: string) =>\n server.registerTool(\n name,\n {\n title,\n description: `${description}\\n\\nLong ranges are fetched in chunks automatically (up to 366 days).`,\n inputSchema: z.strictObject({ start_date: StartArg, end_date: EndArg }),\n annotations: READ_ONLY,\n },\n async ({ start_date, end_date }, ctx) => {\n const [a, b] = resolveRange(start_date, end_date);\n return jsonResult(await fetchRange(client, endpoint, a, b, ctx.mcpReq.signal), `${title}, ${a} to ${b}.`);\n },\n );\n\n rangeTool(\n \"garmin_get_daily_summaries\",\n \"dailySummaries\",\n \"Daily summaries\",\n \"Per-day totals: steps, distance, active and total calories, resting/min/max heart rate, average and max stress, intensity minutes, floors, Body Battery high/low. Best tool for trends across weeks.\",\n );\n rangeTool(\"garmin_get_sleep\", \"sleep\", \"Sleep\", \"Per-night sleep: duration, deep/light/REM/awake time, sleep score, and timing.\");\n rangeTool(\"garmin_get_hrv\", \"hrv\", \"HRV\", \"Per-night heart-rate variability: last-night average, weekly average, baseline range and status (balanced, unbalanced, low).\");\n rangeTool(\"garmin_get_body_battery\", \"bodyBattery\", \"Body Battery\", \"Per-day Body Battery: charged, drained, highest and lowest values.\");\n rangeTool(\"garmin_get_stress\", \"stress\", \"Stress\", \"Per-day all-day stress levels.\");\n rangeTool(\"garmin_get_steps\", \"steps\", \"Steps\", \"Per-day step totals and goals.\");\n rangeTool(\"garmin_get_hydration\", \"hydration\", \"Hydration\", \"Per-day logged water intake and goal.\");\n rangeTool(\n \"garmin_get_weight\",\n \"weight\",\n \"Weight & body composition\",\n \"Every weigh-in in the range (Garmin usually reports weight in grams: divide by 1000 for kg), with BMI, body fat, muscle mass and water where a smart scale recorded them. Use for bulk/cut progress.\",\n );\n rangeTool(\"garmin_get_weight_goals\", \"weightGoals\", \"Weight goals\", \"Weight goals in effect during the range.\");\n rangeTool(\n \"garmin_get_wellness_activities\",\n \"wellnessActivities\",\n \"Wellness activities\",\n \"Wellness activity summaries (e.g. breathwork and relaxation sessions) for the range.\",\n );\n\n // ---------------- Single-day detail ----------------\n const dayTool = (name: string, title: string, description: string, fn: (d: string, s: AbortSignal) => Promise<Json>) =>\n server.registerTool(\n name,\n { title, description, inputSchema: z.strictObject({ date: DateArg.default(\"today\") }), annotations: READ_ONLY },\n async ({ date }, ctx) => {\n const d = resolveDate(date);\n return jsonResult(await fn(d, ctx.mcpReq.signal), `${title}, ${d}.`);\n },\n );\n\n dayTool(\"garmin_get_sleep_detail\", \"Sleep detail\", \"One night's sleep in full: stages timeline, sleep score breakdown, respiration, restless moments, SpO2 and HR during sleep.\", client.dailySleep);\n dayTool(\"garmin_get_heart_rate\", \"Heart rate (all day)\", \"One day's resting HR and the all-day heart-rate samples.\", client.dailyHeartRate);\n dayTool(\"garmin_get_spo2\", \"Pulse Ox\", \"One day's SpO2 readings and acclimation.\", client.dailySpo2);\n dayTool(\"garmin_get_fitness_age\", \"Fitness age\", \"Garmin's fitness age estimate and its inputs for a date.\", client.fitnessAge);\n dayTool(\"garmin_get_nutrition\", \"Food log\", \"Foods and macros logged in Garmin Connect's nutrition log for one day.\", client.foodLogs);\n\n // ---------------- Escape hatch ----------------\n server.registerTool(\n \"garmin_api_get\",\n {\n title: \"Raw Personal API GET\",\n description: `Call any documented Personal API endpoint directly, for data the other tools don't cover (e.g. menstrual cycle summaries, activity workouts).\nPath must start with one of: ${ALLOWED_PATH_PREFIXES.join(\", \")}.\nExample: path=\"/activity-service/activity/123456/workouts\"`,\n inputSchema: z.strictObject({\n path: z.string().startsWith(\"/\").describe(\"API path beginning with /\"),\n params: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional().describe(\"Query parameters\"),\n }),\n annotations: READ_ONLY,\n },\n async ({ path, params }, ctx) => {\n if (path.includes(\"..\") || !ALLOWED_PATH_PREFIXES.some((p) => path.startsWith(p))) {\n throw new Error(`Path not allowed. It must start with one of ${ALLOWED_PATH_PREFIXES.join(\", \")}.`);\n }\n return jsonResult(await client.get(path, params, ctx.mcpReq.signal));\n },\n );\n}\n\n/** Runs `fn` over `items` with at most `limit` in flight, keeping input order. */\nasync function mapLimit<T, R>(items: T[], limit: number, fn: (item: T) => Promise<R>): Promise<R[]> {\n const out: R[] = new Array(items.length);\n let next = 0;\n const worker = async () => {\n while (next < items.length) {\n const i = next++;\n out[i] = await fn(items[i]);\n }\n };\n await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));\n return out;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "garmin-personal-api-mcp",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
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",