hevy-mcp 1.26.0 → 1.26.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/README.md CHANGED
@@ -38,10 +38,10 @@ A Model Context Protocol (MCP) server implementation that interfaces with the [H
38
38
 
39
39
  Pick the workflow that fits your setup:
40
40
 
41
- | Scenario | Command | Requirements |
42
- | :-------------------- | :------------------------------------------ | :------------------------- |
43
- | **One-off stdio run** | `HEVY_API_KEY=sk_live... npx -y hevy-mcp` | Node.js ≥ 26, Hevy API key |
44
- | **Local development** | `npm install && npm run build && npm start` | `.env` with `HEVY_API_KEY` |
41
+ | Scenario | Command | Requirements |
42
+ | :-------------------- | :------------------------------------------------------------------------------------------ | :------------------------- |
43
+ | **One-off stdio run** | `HEVY_API_KEY=sk_live... npx -y hevy-mcp` or `HEVY_API_KEY=sk_live... bunx hevy-mcp@latest` | Node.js ≥ 26, Hevy API key |
44
+ | **Local development** | `npm install && npm run build && npm start` | `.env` with `HEVY_API_KEY` |
45
45
 
46
46
  ---
47
47
 
@@ -49,18 +49,24 @@ Pick the workflow that fits your setup:
49
49
 
50
50
  - **Node.js**: v26 or higher (strongly recommended to use the exact version pinned in `.nvmrc`).
51
51
  - **npm**: v10 or higher.
52
+ - **Bun** (optional): If you want to launch with `bunx`.
52
53
  - **Hevy API key**: Required for all operations (available with Hevy PRO).
53
54
 
54
55
  ---
55
56
 
56
57
  ## 📦 Installation
57
58
 
58
- ### Run via npx (Recommended)
59
+ ### Run via npx or bunx
59
60
 
60
- You can launch the server directly without cloning:
61
+ You can launch the server directly without cloning. Both launchers are covered
62
+ by nightly smoke tests:
61
63
 
62
64
  ```bash
65
+ # npm launcher
63
66
  HEVY_API_KEY=your_hevy_api_key_here npx -y hevy-mcp
67
+
68
+ # bun launcher
69
+ HEVY_API_KEY=your_hevy_api_key_here bunx hevy-mcp@latest
64
70
  ```
65
71
 
66
72
  ### Manual Installation
@@ -103,6 +109,15 @@ To use this server with Claude Desktop, add the following to your `claude_deskto
103
109
  }
104
110
  ```
105
111
 
112
+ If you prefer Bun, swap the launcher fields:
113
+
114
+ ```json
115
+ {
116
+ "command": "bunx",
117
+ "args": ["hevy-mcp@latest"]
118
+ }
119
+ ```
120
+
106
121
  ### Cursor Configuration
107
122
 
108
123
  Add this server under `"mcpServers"` in `~/.cursor/mcp.json`:
@@ -121,6 +136,15 @@ Add this server under `"mcpServers"` in `~/.cursor/mcp.json`:
121
136
  }
122
137
  ```
123
138
 
139
+ If you prefer Bun, swap the launcher fields:
140
+
141
+ ```json
142
+ {
143
+ "command": "bunx",
144
+ "args": ["hevy-mcp@latest"]
145
+ }
146
+ ```
147
+
124
148
  ### Other MCP Clients (via add-mcp)
125
149
 
126
150
  For a generic setup flow across MCP clients, use [`add-mcp`](https://github.com/neon-solutions/add-mcp):
@@ -144,16 +168,34 @@ This bootstraps the `hevy-mcp` entry in your client config without manual JSON e
144
168
 
145
169
  ## ⚙️ Configuration
146
170
 
147
- Supply your Hevy API key via:
171
+ Supply your Hevy API key via the `HEVY_API_KEY` environment variable (in
172
+ `.env` or system environment).
148
173
 
149
- 1. **Environment Variable**: `HEVY_API_KEY` (in `.env` or system environment).
150
- 2. **CLI Argument**: `--hevy-api-key=your_key` (after `--` in npm scripts).
174
+ > ⚠️ CLI API key arguments (`--hevy-api-key=...`, `--hevyApiKey=...`,
175
+ > `hevy-api-key=...`) are still accepted for backward compatibility, but are
176
+ > deprecated and insecure. Use `HEVY_API_KEY` instead.
151
177
 
152
178
  ```env
153
179
  # Example .env
154
180
  HEVY_API_KEY=your_hevy_api_key_here
155
181
  ```
156
182
 
183
+ ### 🧠 Exercise Template Cache Behavior
184
+
185
+ `search-exercise-templates` now uses a shared in-memory async cache for the
186
+ full exercise template catalog:
187
+
188
+ - **TTL**: 5 minutes per cached catalog entry.
189
+ - **Memory bound**: max 1 catalog entry (LRU bounded cache).
190
+ - **In-flight de-duplication**: concurrent requests share the same active
191
+ fetch when possible.
192
+ - **Manual refresh**: set `refresh: true` in the tool input to invalidate the
193
+ cached catalog and force a re-fetch from the Hevy API.
194
+
195
+ This cache currently applies to `search-exercise-templates` only. Paginated
196
+ `get-exercise-templates` requests still call the API directly to keep paging
197
+ behavior explicit and avoid cross-page invalidation complexity.
198
+
157
199
  ### 📡 Sentry Monitoring
158
200
 
159
201
  `hevy-mcp` includes Sentry monitoring to observe errors and usage in production. It initializes `@sentry/node` with tracing enabled and PII collection disabled by default. Recent observability changes also add:
@@ -166,15 +208,12 @@ HEVY_API_KEY=your_hevy_api_key_here
166
208
  ---
167
209
 
168
210
  <details>
169
- <summary><strong>⚠️ Deprecation Notices (HTTP/SSE & Docker)</strong></summary>
170
-
171
- ### Stdio Only
211
+ <summary><strong>⚠️ Migration Note (v1.18.0)</strong></summary>
172
212
 
173
- As of version **1.18.0**, `hevy-mcp` only supports **stdio** transport. HTTP/SSE transport has been completely removed to simplify the codebase and focus on the native MCP experience.
213
+ As of **v1.18.0**, `hevy-mcp` removed both HTTP/SSE transport and Docker
214
+ support.
174
215
 
175
- ### Docker
176
-
177
- Docker-based workflows are retired. The provided `Dockerfile` now exits with a message pointing to the stdio-native experience. Legacy GHCR images are no longer updated.
216
+ The supported path is stdio via `npx hevy-mcp`.
178
217
 
179
218
  </details>
180
219
 
@@ -191,6 +230,11 @@ Docker-based workflows are retired. The provided `Dockerfile` now exits with a m
191
230
  | **Body Measurements** | `get-body-measurements`, `get-body-measurement`, `create-body-measurement`, `update-body-measurement` |
192
231
  | **User** | `get-user-info` |
193
232
 
233
+ > **Delete operations are currently unsupported:** The upstream Hevy OpenAPI
234
+ > spec does not expose `DELETE` endpoints for workouts, routines, routine
235
+ > folders, exercise templates, or body measurements, so `hevy-mcp` does not
236
+ > provide delete tools for these resources.
237
+
194
238
  ---
195
239
 
196
240
  ## 👨‍💻 Development & Contributing
@@ -199,6 +243,7 @@ Docker-based workflows are retired. The provided `Dockerfile` now exits with a m
199
243
 
200
244
  - **Build**: `npm run build`
201
245
  - **Lint/Format**: `npm run check` (uses oxlint/oxfmt)
246
+ - **Type Check**: `npm run check:types`
202
247
  - **Unit Tests**: `npx vitest run --exclude tests/integration/**`
203
248
  - **Full Test Suite**: `npm test` (requires `HEVY_API_KEY`)
204
249
  - **Changeset Check**: `npm run check:changeset`
@@ -210,6 +255,8 @@ For a detailed senior engineer guide, please refer to [AGENTS.md](./AGENTS.md).
210
255
  - **Conventional Commits**: CI lints commit messages on pull requests, so use
211
256
  prefixes such as `feat:`, `fix:`, `docs:`, `ci:`, `chore:`, `refactor:`,
212
257
  `test:`, or `style:`.
258
+ - **Type Checking**: CI runs `npm run check:types` on pull requests and pushes
259
+ to `main`; run this locally before opening a PR.
213
260
  - **Changesets**: Contributor pull requests targeting `main` must include a
214
261
  changeset. Dependabot PRs and automated `changeset-release/main` release PRs
215
262
  are handled by automation and skip this check.
package/dist/cli.mjs CHANGED
@@ -4,12 +4,12 @@
4
4
  (function() {
5
5
  try {
6
6
  var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
7
- e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.1" };
8
8
  var n = new e.Error().stack;
9
9
  n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "03cf150f-6466-4ca3-9cf1-e423cc0e3cd2", e._sentryDebugIdIdentifier = "sentry-dbid-03cf150f-6466-4ca3-9cf1-e423cc0e3cd2");
10
10
  } catch (e) {}
11
11
  })();
12
- import { r as runServer } from "./src-Di7keTQA.mjs";
12
+ import { r as runServer } from "./src-_FHG1OUO.mjs";
13
13
  //#region src/cli.ts
14
14
  runServer().catch((error) => {
15
15
  console.error("Fatal error in main():", error);
package/dist/index.mjs CHANGED
@@ -4,10 +4,10 @@
4
4
  (function() {
5
5
  try {
6
6
  var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
7
- e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.1" };
8
8
  var n = new e.Error().stack;
9
9
  n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "5b077b94-9ef8-4f24-a0b0-8dcfbf491be1", e._sentryDebugIdIdentifier = "sentry-dbid-5b077b94-9ef8-4f24-a0b0-8dcfbf491be1");
10
10
  } catch (e) {}
11
11
  })();
12
- import { n as createServer, r as runServer, t as configSchema } from "./src-Di7keTQA.mjs";
12
+ import { n as createServer, r as runServer, t as configSchema } from "./src-_FHG1OUO.mjs";
13
13
  export { configSchema, createServer, createServer as default, runServer };
@@ -4,9 +4,9 @@
4
4
  (function() {
5
5
  try {
6
6
  var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
7
- e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@1.26.1" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "b6fef1cd-aaf1-497b-af9e-7d50fa405061", e._sentryDebugIdIdentifier = "sentry-dbid-b6fef1cd-aaf1-497b-af9e-7d50fa405061");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "d8a16a1c-7e19-4663-9699-a6c28dfa8b53", e._sentryDebugIdIdentifier = "sentry-dbid-d8a16a1c-7e19-4663-9699-a6c28dfa8b53");
10
10
  } catch (e) {}
11
11
  })();
12
12
  import * as Sentry from "@sentry/node";
@@ -36,10 +36,10 @@ import { deserializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
36
36
  * OTel Collector → Honeycomb: performance traces, metrics
37
37
  */
38
38
  const name$1 = "hevy-mcp";
39
- const version$1 = "1.26.0";
39
+ const version$1 = "1.26.1";
40
40
  const collectorToken = process.env.OTEL_COLLECTOR_TOKEN ?? "";
41
41
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
42
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@1.26.0`;
42
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@1.26.1`;
43
43
  const resource = resourceFromAttributes({
44
44
  "service.name": name$1,
45
45
  "service.version": version$1
@@ -135,18 +135,21 @@ const serverStartups = meter.createCounter("mcp.server.startups", { description:
135
135
  * @returns A formatted MCP tool response with error information
136
136
  */
137
137
  function createErrorResponse(error, context) {
138
- let errorMessage = error instanceof Error ? error.message : String(error);
139
- if (isAxiosError(error) && error.response?.data) {
140
- const { data } = error.response;
141
- if (typeof data === "string") errorMessage = data;
142
- else if (data && typeof data === "object") try {
143
- errorMessage = JSON.stringify(data);
144
- } catch (_e) {
145
- errorMessage = String(data);
146
- }
147
- }
138
+ const originalErrorMessage = extractErrorMessage(error);
139
+ let errorMessage = originalErrorMessage;
140
+ const axiosErrorContext = extractAxiosErrorContext(error);
141
+ const mappedHevyErrorMessage = mapHevyErrorMessageByStatus(axiosErrorContext?.status);
142
+ if (mappedHevyErrorMessage) errorMessage = mappedHevyErrorMessage;
143
+ if (!mappedHevyErrorMessage && axiosErrorContext?.data) errorMessage = stringifyErrorData(axiosErrorContext.data);
148
144
  const errorCode = error instanceof Error && "code" in error ? error.code : void 0;
149
145
  const errorType = determineErrorType(error, errorMessage);
146
+ const errorContext = {
147
+ sourceContext: context,
148
+ originalErrorMessage,
149
+ errorCode,
150
+ errorType,
151
+ axios: axiosErrorContext ?? void 0
152
+ };
150
153
  const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${errorMessage}`;
151
154
  const errorCodeSuffix = errorCode ? `, Code: ${errorCode}` : "";
152
155
  console.error(`${formattedMessage} (Type: ${errorType}${errorCodeSuffix})`, error);
@@ -155,9 +158,49 @@ function createErrorResponse(error, context) {
155
158
  type: "text",
156
159
  text: formattedMessage
157
160
  }],
158
- isError: true
161
+ isError: true,
162
+ errorContext
159
163
  };
160
164
  }
165
+ function extractErrorMessage(error) {
166
+ if (error instanceof Error) return error.message;
167
+ if (typeof error === "string") return error;
168
+ if (error && typeof error === "object" && "message" in error && typeof error.message === "string") return error.message;
169
+ if (error && typeof error === "object") try {
170
+ return JSON.stringify(error);
171
+ } catch (_e) {
172
+ return "Unknown error object";
173
+ }
174
+ return String(error);
175
+ }
176
+ function extractAxiosErrorContext(error) {
177
+ if (!isAxiosError(error)) return null;
178
+ return {
179
+ status: error.response?.status,
180
+ statusText: error.response?.statusText,
181
+ data: error.response?.data,
182
+ method: error.config?.method,
183
+ url: error.config?.url
184
+ };
185
+ }
186
+ function mapHevyErrorMessageByStatus(status) {
187
+ if (status === 401 || status === 403) return "The Hevy API key is invalid or has expired. Check HEVY_API_KEY.";
188
+ if (status === 404) return "The requested resource was not found in Hevy.";
189
+ if (status === 409) return "A conflict occurred (e.g., a body measurement already exists for this date). Use the update tool instead.";
190
+ if (status === 422) return "The request failed Hevy validation. Check the field values and try again.";
191
+ if (status === 429) return "Rate limited by Hevy. Please wait and retry.";
192
+ if (status && status >= 500 && status <= 599) return "Hevy API experienced an error. Please retry later.";
193
+ return null;
194
+ }
195
+ function stringifyErrorData(data) {
196
+ if (typeof data === "string") return data;
197
+ if (data && typeof data === "object") try {
198
+ return JSON.stringify(data);
199
+ } catch (_e) {
200
+ return "Unable to serialize error response data";
201
+ }
202
+ return String(data);
203
+ }
161
204
  /**
162
205
  * Determine the type of error based on error characteristics
163
206
  */
@@ -405,7 +448,7 @@ function createJsonResponse(data, options = {
405
448
  }) {
406
449
  return { content: [{
407
450
  type: "text",
408
- text: options.pretty ? JSON.stringify(data, null, options.indent) : JSON.stringify(data)
451
+ text: (options.pretty ? JSON.stringify(data, null, options.indent) : JSON.stringify(data)) ?? "null"
409
452
  }] };
410
453
  }
411
454
  /**
@@ -427,10 +470,7 @@ function createTextResponse(message) {
427
470
  * @returns A formatted MCP tool response for empty results
428
471
  */
429
472
  function createEmptyResponse(message = "No data found") {
430
- return { content: [{
431
- type: "text",
432
- text: message
433
- }] };
473
+ return createTextResponse(message);
434
474
  }
435
475
  //#endregion
436
476
  //#region src/utils/tool-annotations.ts
@@ -836,6 +876,92 @@ function registerRoutineTools(server, hevyClient) {
836
876
  }, "update-routine"));
837
877
  }
838
878
  //#endregion
879
+ //#region src/utils/cache.ts
880
+ /**
881
+ * Shared in-memory cache for async fetches with TTL, LRU eviction,
882
+ * in-flight de-duplication, and explicit refresh/invalidation support.
883
+ */
884
+ var AsyncTtlCache = class {
885
+ ttlMs;
886
+ maxSize;
887
+ now;
888
+ entries = /* @__PURE__ */ new Map();
889
+ inFlight = /* @__PURE__ */ new Map();
890
+ requestCounter = 0;
891
+ constructor(options, now = Date.now) {
892
+ const { ttlMs, maxSize } = options;
893
+ if (ttlMs <= 0) throw new Error("Cache ttlMs must be greater than 0.");
894
+ if (maxSize <= 0) throw new Error("Cache maxSize must be greater than 0.");
895
+ this.ttlMs = ttlMs;
896
+ this.maxSize = maxSize;
897
+ this.now = now;
898
+ }
899
+ async getOrFetch(key, fetcher, options = {}) {
900
+ const { refresh = false } = options;
901
+ if (refresh) this.invalidate(key);
902
+ else {
903
+ const cachedEntry = this.entries.get(key);
904
+ if (cachedEntry !== void 0) {
905
+ if (cachedEntry.expiresAt > this.now()) {
906
+ this.markAsRecentlyUsed(key, cachedEntry);
907
+ return cachedEntry.value;
908
+ }
909
+ this.entries.delete(key);
910
+ }
911
+ const inFlightEntry = this.inFlight.get(key);
912
+ if (inFlightEntry !== void 0) return inFlightEntry.promise;
913
+ }
914
+ const requestId = ++this.requestCounter;
915
+ const request = (async () => {
916
+ try {
917
+ const value = await fetcher();
918
+ if (this.isCurrentRequest(key, requestId)) this.setValue(key, value);
919
+ return value;
920
+ } finally {
921
+ if (this.inFlight.get(key)?.requestId === requestId) this.inFlight.delete(key);
922
+ }
923
+ })();
924
+ this.inFlight.set(key, {
925
+ promise: request,
926
+ requestId
927
+ });
928
+ return request;
929
+ }
930
+ invalidate(key) {
931
+ this.entries.delete(key);
932
+ this.inFlight.delete(key);
933
+ }
934
+ clear() {
935
+ this.entries.clear();
936
+ this.inFlight.clear();
937
+ }
938
+ get size() {
939
+ return this.entries.size;
940
+ }
941
+ isCurrentRequest(key, requestId) {
942
+ return this.inFlight.get(key)?.requestId === requestId;
943
+ }
944
+ markAsRecentlyUsed(key, entry) {
945
+ this.entries.delete(key);
946
+ this.entries.set(key, entry);
947
+ }
948
+ setValue(key, value) {
949
+ this.entries.delete(key);
950
+ this.entries.set(key, {
951
+ value,
952
+ expiresAt: this.now() + this.ttlMs
953
+ });
954
+ this.evictLeastRecentlyUsed();
955
+ }
956
+ evictLeastRecentlyUsed() {
957
+ while (this.entries.size > this.maxSize) {
958
+ const oldestKey = this.entries.keys().next().value;
959
+ if (oldestKey === void 0) return;
960
+ this.entries.delete(oldestKey);
961
+ }
962
+ }
963
+ };
964
+ //#endregion
839
965
  //#region src/tools/templates.ts
840
966
  const MUSCLE_GROUPS = [
841
967
  "abdominals",
@@ -859,8 +985,27 @@ const MUSCLE_GROUPS = [
859
985
  "full_body",
860
986
  "other"
861
987
  ];
862
- let exerciseTemplateCache = null;
863
- let exerciseTemplateFetch = null;
988
+ const EXERCISE_TEMPLATE_CATALOG_CACHE_KEY = "exercise-template-catalog";
989
+ const exerciseTemplateCatalogCache = new AsyncTtlCache({
990
+ ttlMs: 300 * 1e3,
991
+ maxSize: 1
992
+ });
993
+ async function fetchExerciseTemplateCatalog(hevyClient) {
994
+ const allTemplates = [];
995
+ let page = 1;
996
+ let pageCount = 1;
997
+ do {
998
+ const data = await hevyClient.getExerciseTemplates({
999
+ page,
1000
+ pageSize: 100
1001
+ });
1002
+ const templates = data?.exercise_templates ?? [];
1003
+ allTemplates.push(...templates);
1004
+ pageCount = data?.page_count ?? 1;
1005
+ page++;
1006
+ } while (page <= pageCount);
1007
+ return allTemplates;
1008
+ }
864
1009
  /**
865
1010
  * Register all exercise template-related tools with the MCP server
866
1011
  */
@@ -945,39 +1090,14 @@ function registerTemplateTools(server, hevyClient) {
945
1090
  const searchExerciseTemplatesSchema = {
946
1091
  query: z.string().min(1).describe("Case-insensitive substring to match against exercise template titles"),
947
1092
  primaryMuscleGroup: z.enum(MUSCLE_GROUPS).optional().describe("Optional filter to restrict results to a specific primary muscle group"),
948
- refresh: z.boolean().optional().default(false).describe("Set to true to bust the in-memory cache and re-fetch all templates from the API")
1093
+ refresh: z.boolean().optional().default(false).describe("Set to true to invalidate the catalog cache and re-fetch all templates from the API")
949
1094
  };
950
- server.tool("search-exercise-templates", "Search exercise templates by name with optional muscle group filter. Fetches all templates from the Hevy API on first call and caches them in memory for subsequent searches. Use refresh:true to force a re-fetch.", searchExerciseTemplatesSchema, readOnlyAnnotations("Search Exercise Templates"), withErrorHandling(async (args) => {
1095
+ server.tool("search-exercise-templates", "Search exercise templates by name with optional muscle group filter. Fetches all templates from the Hevy API on first call, caches the catalog in memory with a bounded TTL cache, and reuses it for subsequent searches. Use refresh:true to force a re-fetch.", searchExerciseTemplatesSchema, readOnlyAnnotations("Search Exercise Templates"), withErrorHandling(async (args) => {
951
1096
  if (!hevyClient) throw new Error("API client not initialized. Please provide HEVY_API_KEY.");
952
1097
  const { query, primaryMuscleGroup, refresh } = args;
953
- if (exerciseTemplateCache === null || refresh) {
954
- if (refresh) exerciseTemplateFetch = null;
955
- if (exerciseTemplateFetch === null) exerciseTemplateFetch = (async () => {
956
- try {
957
- const allTemplates = [];
958
- let page = 1;
959
- let pageCount = 1;
960
- do {
961
- const data = await hevyClient.getExerciseTemplates({
962
- page,
963
- pageSize: 100
964
- });
965
- const templates = data?.exercise_templates ?? [];
966
- allTemplates.push(...templates);
967
- pageCount = data?.page_count ?? 1;
968
- page++;
969
- } while (page <= pageCount);
970
- exerciseTemplateCache = allTemplates;
971
- return allTemplates;
972
- } finally {
973
- exerciseTemplateFetch = null;
974
- }
975
- })();
976
- await exerciseTemplateFetch;
977
- }
1098
+ const catalog = await exerciseTemplateCatalogCache.getOrFetch(EXERCISE_TEMPLATE_CATALOG_CACHE_KEY, () => fetchExerciseTemplateCatalog(hevyClient), { refresh });
978
1099
  const queryLower = query.toLowerCase();
979
- if (exerciseTemplateCache === null) throw new Error("Failed to populate exercise template cache.");
980
- let results = exerciseTemplateCache.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower));
1100
+ let results = catalog.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower));
981
1101
  if (primaryMuscleGroup !== void 0) results = results.filter((t) => t.primary_muscle_group === primaryMuscleGroup);
982
1102
  if (results.length === 0) return createEmptyResponse(`No exercise templates found matching "${query}"${primaryMuscleGroup ? ` with primary muscle group "${primaryMuscleGroup}"` : ""}`);
983
1103
  return createJsonResponse(results.map(formatExerciseTemplate));
@@ -1165,16 +1285,23 @@ function registerWorkoutTools(server, hevyClient) {
1165
1285
  }
1166
1286
  //#endregion
1167
1287
  //#region src/utils/config.ts
1288
+ const DEPRECATED_CLI_ARGUMENT_WARNING = [
1289
+ "DEPRECATION WARNING: Passing the Hevy API key via CLI arguments",
1290
+ "(--hevy-api-key=..., --hevyApiKey=..., hevy-api-key=...) is",
1291
+ "deprecated and insecure. Use the HEVY_API_KEY environment",
1292
+ "variable instead."
1293
+ ].join(" ");
1168
1294
  /**
1169
1295
  * Parse CLI arguments and environment to derive configuration.
1170
- * Priority order for API key: CLI flag forms > environment variable.
1171
- * Supported CLI arg forms:
1296
+ * Priority order for API key: deprecated CLI flag forms > environment variable.
1297
+ * Supported deprecated CLI arg forms:
1172
1298
  * --hevy-api-key=KEY
1173
1299
  * --hevyApiKey=KEY
1174
1300
  * hevy-api-key=KEY (bare, e.g. when passed after npm start -- )
1175
1301
  */
1176
1302
  function parseConfig(argv, env) {
1177
1303
  let apiKey = "";
1304
+ let usedDeprecatedApiKeyArg = false;
1178
1305
  const apiKeyArgPatterns = [
1179
1306
  /^--hevy-api-key=(.+)$/i,
1180
1307
  /^--hevyApiKey=(.+)$/i,
@@ -1185,17 +1312,19 @@ function parseConfig(argv, env) {
1185
1312
  const m = raw.match(pattern);
1186
1313
  if (m) {
1187
1314
  apiKey = m[1];
1315
+ usedDeprecatedApiKeyArg = true;
1188
1316
  break;
1189
1317
  }
1190
1318
  }
1191
1319
  if (apiKey) break;
1192
1320
  }
1321
+ if (usedDeprecatedApiKeyArg) console.error(DEPRECATED_CLI_ARGUMENT_WARNING);
1193
1322
  if (!apiKey) apiKey = env.HEVY_API_KEY || "";
1194
1323
  return { apiKey };
1195
1324
  }
1196
1325
  function assertApiKey(apiKey) {
1197
1326
  if (!apiKey) {
1198
- console.error("Hevy API key is required. Provide it via the HEVY_API_KEY environment variable or the --hevy-api-key=YOUR_KEY command argument.");
1327
+ console.error("Hevy API key is required. Provide it via the HEVY_API_KEY environment variable.");
1199
1328
  process.exit(1);
1200
1329
  }
1201
1330
  }
@@ -1921,6 +2050,44 @@ function createClient(apiKey, baseUrl) {
1921
2050
  //#endregion
1922
2051
  //#region src/utils/stdio-observability.ts
1923
2052
  const UTF8_BOM = "";
2053
+ /**
2054
+ * Adapter boundary around MCP SDK stdio internals.
2055
+ *
2056
+ * MCP SDK v1.29.0 exposes public message-level hooks but does not expose a
2057
+ * public raw-chunk hook on `StdioServerTransport`. To capture chunk metadata,
2058
+ * we currently rely on private internals (`_ondata`, `_readBuffer`, `_buffer`)
2059
+ * in this one place.
2060
+ *
2061
+ * If those internals change in a future SDK release, this adapter should fail
2062
+ * closed and preserve default transport behavior (no instrumentation patching).
2063
+ */
2064
+ function createSdkPrivateStdioAdapter(transport) {
2065
+ const mutableTransport = transport;
2066
+ return {
2067
+ wrapOnData(onChunk) {
2068
+ const originalOnData = mutableTransport._ondata;
2069
+ if (typeof originalOnData !== "function") return;
2070
+ mutableTransport._ondata = (chunk) => {
2071
+ onChunk(chunk);
2072
+ originalOnData(chunk);
2073
+ };
2074
+ },
2075
+ installReadMessageHook(onReadLine) {
2076
+ const readBuffer = mutableTransport._readBuffer;
2077
+ if (!readBuffer || typeof readBuffer.readMessage !== "function") return false;
2078
+ readBuffer.readMessage = () => {
2079
+ const buffer = readBuffer._buffer;
2080
+ if (!buffer) return null;
2081
+ const index = buffer.indexOf("\n");
2082
+ if (index === -1) return null;
2083
+ const lineBuffer = buffer.subarray(0, index);
2084
+ readBuffer._buffer = buffer.subarray(index + 1);
2085
+ return onReadLine(lineBuffer.toString("utf8").replace(/\r$/, ""));
2086
+ };
2087
+ return true;
2088
+ }
2089
+ };
2090
+ }
1924
2091
  function hasUtf8BomPrefix(chunk) {
1925
2092
  return chunk.length >= 3 && chunk[0] === 239 && chunk[1] === 187 && chunk[2] === 191;
1926
2093
  }
@@ -1987,36 +2154,48 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
1987
2154
  });
1988
2155
  }
1989
2156
  function createInstrumentedStdioTransport(transport) {
1990
- const mutableTransport = transport;
2157
+ const privateAdapter = createSdkPrivateStdioAdapter(transport);
1991
2158
  let lastChunkSnapshot = {
1992
2159
  lastChunkByteLength: 0,
1993
2160
  lastChunkStartsWithUtf8Bom: false
1994
2161
  };
1995
- const originalOnData = mutableTransport._ondata;
1996
- if (typeof originalOnData === "function") mutableTransport._ondata = (chunk) => {
2162
+ privateAdapter.wrapOnData((chunk) => {
1997
2163
  lastChunkSnapshot = {
1998
2164
  lastChunkByteLength: chunk.byteLength,
1999
2165
  lastChunkStartsWithUtf8Bom: hasUtf8BomPrefix(chunk)
2000
2166
  };
2001
- originalOnData(chunk);
2002
- };
2003
- const readBuffer = mutableTransport._readBuffer;
2004
- if (!readBuffer || typeof readBuffer.readMessage !== "function") return transport;
2005
- readBuffer.readMessage = () => {
2006
- const buffer = readBuffer._buffer;
2007
- if (!buffer) return null;
2008
- const index = buffer.indexOf("\n");
2009
- if (index === -1) return null;
2010
- const lineBuffer = buffer.subarray(0, index);
2011
- readBuffer._buffer = buffer.subarray(index + 1);
2012
- return deserializeMessageWithObservability(lineBuffer.toString("utf8").replace(/\r$/, ""), lastChunkSnapshot);
2013
- };
2167
+ });
2168
+ if (!privateAdapter.installReadMessageHook((line) => deserializeMessageWithObservability(line, lastChunkSnapshot))) return transport;
2014
2169
  return transport;
2015
2170
  }
2016
2171
  //#endregion
2017
2172
  //#region src/index.ts
2018
2173
  const name = name$1;
2019
2174
  const version = version$1;
2175
+ const HELP_TEXT = [
2176
+ "Usage:",
2177
+ " hevy-mcp [options]",
2178
+ "",
2179
+ "Options:",
2180
+ " -h, --help Show this help message and exit",
2181
+ " -v, --version Show version and exit",
2182
+ " --hevy-api-key=<api-key> (deprecated, use HEVY_API_KEY env var)",
2183
+ "",
2184
+ "Environment:",
2185
+ " HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
2186
+ "",
2187
+ "Examples:",
2188
+ " HEVY_API_KEY=your-key npx hevy-mcp",
2189
+ " npx hevy-mcp --hevy-api-key=your-key",
2190
+ " npm start -- --hevy-api-key=your-key"
2191
+ ].join("\n");
2192
+ function getCliAction(args) {
2193
+ for (const arg of args) {
2194
+ if (arg === "--version" || arg === "-v") return "version";
2195
+ if (arg === "--help" || arg === "-h") return "help";
2196
+ }
2197
+ return "start";
2198
+ }
2020
2199
  const HEVY_API_BASEURL = "https://api.hevyapp.com";
2021
2200
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
2022
2201
  function fingerprintApiKey(apiKey) {
@@ -2075,10 +2254,20 @@ function createServer({ config }) {
2075
2254
  return buildServer(apiKey);
2076
2255
  }
2077
2256
  async function runServer() {
2257
+ const args = process.argv.slice(2);
2258
+ const cliAction = getCliAction(args);
2259
+ if (cliAction === "version") {
2260
+ console.log(version);
2261
+ return;
2262
+ }
2263
+ if (cliAction === "help") {
2264
+ console.log(HELP_TEXT);
2265
+ return;
2266
+ }
2078
2267
  serverStartups.add(1, { version });
2079
2268
  await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
2080
2269
  try {
2081
- const apiKey = parseConfig(process.argv.slice(2), process.env).apiKey;
2270
+ const apiKey = parseConfig(args, process.env).apiKey;
2082
2271
  assertApiKey(apiKey);
2083
2272
  const server = buildServer(apiKey);
2084
2273
  console.error("Starting MCP server in stdio mode");
@@ -2106,4 +2295,4 @@ async function runServer() {
2106
2295
  //#endregion
2107
2296
  export { createServer as n, runServer as r, configSchema as t };
2108
2297
 
2109
- //# sourceMappingURL=src-Di7keTQA.mjs.map
2298
+ //# sourceMappingURL=src-_FHG1OUO.mjs.map