hevy-mcp 1.28.1-beta.1 → 3.0.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
@@ -15,6 +15,7 @@ A Model Context Protocol (MCP) server implementation that interfaces with the [H
15
15
  - [Quick Start](#quick-start)
16
16
  - [Prerequisites](#prerequisites)
17
17
  - [Installation](#installation)
18
+ - [Run with Docker](#run-with-docker)
18
19
  - [Claude Desktop Configuration](#claude-desktop-configuration)
19
20
  - [Cursor Configuration](#cursor-configuration)
20
21
  - [Other MCP Clients (via add-mcp)](#other-mcp-clients-via-add-mcp)
@@ -39,10 +40,11 @@ A Model Context Protocol (MCP) server implementation that interfaces with the [H
39
40
 
40
41
  Pick the workflow that fits your setup:
41
42
 
42
- | Scenario | Command | Requirements |
43
- | :-------------------- | :------------------------------------------------------------------------------------------ | :------------------------- |
44
- | **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 ≥ 20, Hevy API key |
45
- | **Local development** | `npm install && npm run build && npm start` | `.env` with `HEVY_API_KEY` |
43
+ | Scenario | Command | Requirements |
44
+ | :-------------------- | :-------------------------------------------------------------------------------------- | :------------------------- |
45
+ | **One-off stdio run** | `HEVY_API_KEY=your_key npx -y hevy-mcp` or `HEVY_API_KEY=your_key bunx hevy-mcp@latest` | Node.js ≥ 20, Hevy API key |
46
+ | **Docker stdio run** | `docker run -i --rm -e HEVY_API_KEY ghcr.io/chrisdoc/hevy-mcp:latest` | Docker, Hevy API key |
47
+ | **Local development** | `npm install && npm run build && npm start` | `.env` with `HEVY_API_KEY` |
46
48
 
47
49
  ---
48
50
 
@@ -51,6 +53,7 @@ Pick the workflow that fits your setup:
51
53
  - **Node.js**: v20 or higher (strongly recommended to use the exact version pinned in `.nvmrc`).
52
54
  - **npm**: v10 or higher.
53
55
  - **Bun** (optional): If you want to launch with `bunx`.
56
+ - **Docker** (optional): If you want an isolated container-based stdio setup.
54
57
  - **Hevy API key**: Required for all operations (available with Hevy PRO).
55
58
 
56
59
  ---
@@ -70,25 +73,6 @@ HEVY_API_KEY=your_hevy_api_key_here npx -y hevy-mcp
70
73
  HEVY_API_KEY=your_hevy_api_key_here bunx hevy-mcp@latest
71
74
  ```
72
75
 
73
- ### Prerelease channel
74
-
75
- Install `hevy-mcp@beta` to opt into changes merged to `main` that have not yet
76
- reached a stable release:
77
-
78
- ```bash
79
- npm install hevy-mcp@beta
80
-
81
- # npx launcher
82
- npx -y hevy-mcp@beta
83
-
84
- # bun launcher
85
- bunx hevy-mcp@beta
86
- ```
87
-
88
- Beta builds may be less stable and can change before their stable release.
89
- `npm install hevy-mcp@latest` remains the stable channel. To switch back from
90
- beta, install `hevy-mcp@latest` again.
91
-
92
76
  ### Manual Installation
93
77
 
94
78
  ```bash
@@ -104,6 +88,32 @@ cp .env.sample .env
104
88
  # Edit .env and add your HEVY_API_KEY
105
89
  ```
106
90
 
91
+ ### Run with Docker
92
+
93
+ Official multi-platform images are published to GitHub Container Registry for
94
+ `linux/amd64` and `linux/arm64`:
95
+
96
+ ```bash
97
+ export HEVY_API_KEY=your_hevy_api_key_here
98
+ docker run -i --rm -e HEVY_API_KEY ghcr.io/chrisdoc/hevy-mcp:latest
99
+ ```
100
+
101
+ The server uses stdio, so `-i` keeps standard input open for the MCP client.
102
+ `--rm` removes the stopped container automatically. The `-e HEVY_API_KEY`
103
+ form forwards the variable from the host environment without putting the key
104
+ in the command arguments.
105
+
106
+ The image uses the official Node.js LTS Alpine base, runs as the non-root
107
+ `node` user, and ships the application and its third-party runtime dependencies
108
+ as a standalone bundle. It does not include an application `/app/node_modules`
109
+ directory; the official Node base image may still contain its own globally
110
+ packaged npm or Corepack files.
111
+
112
+ Use `latest` to follow the newest stable release. For reproducible deployments,
113
+ pin the exact version shown on the release, using a tag such as
114
+ `ghcr.io/chrisdoc/hevy-mcp:X.Y.Z`. Major (`:X`) and major.minor (`:X.Y`) tags
115
+ are also published for controlled automatic updates.
116
+
107
117
  ---
108
118
 
109
119
  ## 🔗 Integration
@@ -138,6 +148,41 @@ If you prefer Bun, swap the launcher fields:
138
148
  }
139
149
  ```
140
150
 
151
+ To run Claude Desktop through Docker instead, first create an environment file
152
+ outside the repository containing your real key:
153
+
154
+ ```dotenv
155
+ HEVY_API_KEY=replace_with_your_real_key
156
+ ```
157
+
158
+ Restrict access to that file where supported (for example,
159
+ `chmod 600 /absolute/path/to/hevy-mcp.env`), then use its absolute path in the
160
+ Claude Desktop configuration:
161
+
162
+ ```json
163
+ {
164
+ "mcpServers": {
165
+ "hevy-mcp": {
166
+ "command": "docker",
167
+ "args": [
168
+ "run",
169
+ "-i",
170
+ "--rm",
171
+ "--env-file",
172
+ "/absolute/path/to/hevy-mcp.env",
173
+ "ghcr.io/chrisdoc/hevy-mcp:latest"
174
+ ]
175
+ }
176
+ }
177
+ }
178
+ ```
179
+
180
+ This configuration runs the same stdio server inside the container; it does
181
+ not expose an HTTP port or start a detached service. Docker reads the key from
182
+ the environment file, so the Claude configuration does not replace an
183
+ inherited key with a placeholder. Replace `latest` with an exact version tag if
184
+ you want Claude Desktop to stay on a pinned release.
185
+
141
186
  ### Cursor Configuration
142
187
 
143
188
  Add this server under `"mcpServers"` in `~/.cursor/mcp.json`:
@@ -191,10 +236,6 @@ This bootstraps the `hevy-mcp` entry in your client config without manual JSON e
191
236
  Supply your Hevy API key via the `HEVY_API_KEY` environment variable (in
192
237
  `.env` or system environment).
193
238
 
194
- > ⚠️ CLI API key arguments (`--hevy-api-key=...`, `--hevyApiKey=...`,
195
- > `hevy-api-key=...`) are still accepted for backward compatibility, but are
196
- > deprecated and insecure. Use `HEVY_API_KEY` instead.
197
-
198
239
  Set `HEVY_MCP_API_TIMEOUT` to override the default 30-second Hevy API request
199
240
  timeout. Its value is in milliseconds.
200
241
 
@@ -241,10 +282,11 @@ paging behavior explicit and avoid cross-page invalidation complexity.
241
282
  <details>
242
283
  <summary><strong>⚠️ Migration Note (v1.18.0)</strong></summary>
243
284
 
244
- As of **v1.18.0**, `hevy-mcp` removed both HTTP/SSE transport and Docker
245
- support.
285
+ As of **v1.18.0**, `hevy-mcp` removed HTTP/SSE transport and its previous
286
+ Docker packaging. Docker support is now available again for the stdio server.
246
287
 
247
- The supported path is stdio via `npx hevy-mcp`.
288
+ Both `npx hevy-mcp` and the official container image use stdio; HTTP ports and
289
+ detached-container deployment are not supported.
248
290
 
249
291
  </details>
250
292
 
@@ -297,9 +339,11 @@ Compatibility note: with MCP SDK v1.29.0, clients using the default must send
297
339
  - **Build**: `npm run build`
298
340
  - **Lint/Format**: `npm run check` (uses oxlint/oxfmt)
299
341
  - **Type Check**: `npm run check:types`
300
- - **Unit Tests**: `npx vitest run --exclude 'tests/integration/**'`
301
- - **Full Test Suite**: `npm test` (requires `HEVY_API_KEY`)
342
+ - **Unit Tests**: `npm run test:unit`
343
+ - **Full Vitest Discovery**: `npm test` (builds first; live tests skip when
344
+ `HEVY_API_KEY` is absent)
302
345
  - **Changeset Check**: `npm run check:changeset`
346
+ - **Tool Token Cost**: `npm run measure:tokens` ([measurement guide](./docs/token-cost-tracking.md))
303
347
 
304
348
  For a detailed senior engineer guide, please refer to [AGENTS.md](./AGENTS.md).
305
349
 
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.28.1-beta.1" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.0.0" };
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-CHT_Meh8.mjs";
12
+ import { r as runServer } from "./src-BBzEvkOX.mjs";
13
13
  //#region src/cli.ts
14
14
  runServer().catch((error) => {
15
15
  console.error("Fatal error in main():", error);
package/dist/index.d.mts CHANGED
@@ -14,7 +14,7 @@ declare function createServer({
14
14
  config
15
15
  }: {
16
16
  config: ServerConfig;
17
- }): McpServer;
17
+ }): Promise<McpServer>;
18
18
  declare function runServer(): Promise<void>;
19
19
  //#endregion
20
20
  export { configSchema, createServer, createServer as default, runServer };
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.28.1-beta.1" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.0.0" };
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-CHT_Meh8.mjs";
12
+ import { n as createServer, r as runServer, t as configSchema } from "./src-BBzEvkOX.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.28.1-beta.1" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.0.0" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "aa9ed1a5-ba18-4930-8061-5891221f6944", e._sentryDebugIdIdentifier = "sentry-dbid-aa9ed1a5-ba18-4930-8061-5891221f6944");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "06533ca8-0b74-40dd-a418-fc98c764276c", e._sentryDebugIdIdentifier = "sentry-dbid-06533ca8-0b74-40dd-a418-fc98c764276c");
10
10
  } catch (e) {}
11
11
  })();
12
12
  import * as Sentry from "@sentry/node";
@@ -41,10 +41,10 @@ import semver from "semver";
41
41
  * OTel Collector → Honeycomb: performance traces, metrics
42
42
  */
43
43
  const name$1 = "hevy-mcp";
44
- const version$1 = "1.28.1-beta.1";
44
+ const version$1 = "3.0.0";
45
45
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
46
46
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
47
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@1.28.1-beta.1`;
47
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@3.0.0`;
48
48
  const resource = resourceFromAttributes({
49
49
  "service.name": name$1,
50
50
  "service.version": version$1
@@ -2110,43 +2110,20 @@ function registerWorkoutTools(server, hevyClient) {
2110
2110
  }, "update-workout-operation"));
2111
2111
  }
2112
2112
  //#endregion
2113
+ //#region src/tools/register.ts
2114
+ /** Register every Hevy tool in its production ordering. */
2115
+ function registerHevyTools(server, hevyClient, options = {}) {
2116
+ registerWorkoutTools(server, hevyClient);
2117
+ registerRoutineTools(server, hevyClient);
2118
+ registerTemplateTools(server, hevyClient, { logger: options.logger });
2119
+ registerFolderTools(server, hevyClient);
2120
+ registerBodyMeasurementTools(server, hevyClient);
2121
+ registerUserTools(server, hevyClient);
2122
+ }
2123
+ //#endregion
2113
2124
  //#region src/utils/config.ts
2114
- const DEPRECATED_CLI_ARGUMENT_WARNING = [
2115
- "DEPRECATION WARNING: Passing the Hevy API key via CLI arguments",
2116
- "(--hevy-api-key=..., --hevyApiKey=..., hevy-api-key=...) is",
2117
- "deprecated and insecure. Use the HEVY_API_KEY environment",
2118
- "variable instead."
2119
- ].join(" ");
2120
- /**
2121
- * Parse CLI arguments and environment to derive configuration.
2122
- * Priority order for API key: deprecated CLI flag forms > environment variable.
2123
- * Supported deprecated CLI arg forms:
2124
- * --hevy-api-key=KEY
2125
- * --hevyApiKey=KEY
2126
- * hevy-api-key=KEY (bare, e.g. when passed after npm start -- )
2127
- */
2128
- function parseConfig(argv, env) {
2129
- let apiKey = "";
2130
- let usedDeprecatedApiKeyArg = false;
2131
- const apiKeyArgPatterns = [
2132
- /^--hevy-api-key=(.+)$/i,
2133
- /^--hevyApiKey=(.+)$/i,
2134
- /^hevy-api-key=(.+)$/i
2135
- ];
2136
- for (const raw of argv) {
2137
- for (const pattern of apiKeyArgPatterns) {
2138
- const m = raw.match(pattern);
2139
- if (m) {
2140
- apiKey = m[1];
2141
- usedDeprecatedApiKeyArg = true;
2142
- break;
2143
- }
2144
- }
2145
- if (apiKey) break;
2146
- }
2147
- if (usedDeprecatedApiKeyArg) console.error(DEPRECATED_CLI_ARGUMENT_WARNING);
2148
- if (!apiKey) apiKey = env.HEVY_API_KEY || "";
2149
- return { apiKey };
2125
+ function parseConfig(env) {
2126
+ return { apiKey: env.HEVY_API_KEY || "" };
2150
2127
  }
2151
2128
  function assertApiKey(apiKey) {
2152
2129
  if (!apiKey) {
@@ -2871,6 +2848,15 @@ function getApiTimeoutMs() {
2871
2848
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
2872
2849
  return Math.trunc(parsed);
2873
2850
  }
2851
+ function normalizeMaxGetRetries(value) {
2852
+ if (value === void 0 || !Number.isFinite(value) || value < 0) return 3;
2853
+ return Math.floor(value);
2854
+ }
2855
+ function normalizeTimeoutMs(value) {
2856
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return getApiTimeoutMs();
2857
+ const normalizedValue = Math.floor(value);
2858
+ return normalizedValue > 0 ? normalizedValue : getApiTimeoutMs();
2859
+ }
2874
2860
  function sleep(ms) {
2875
2861
  return new Promise((resolve) => {
2876
2862
  setTimeout(resolve, ms);
@@ -2958,7 +2944,7 @@ function markRetryExhausted(error, retryCount) {
2958
2944
  if (retryError.code) retryError.hevyRetryOriginalCode = retryError.code;
2959
2945
  retryError.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
2960
2946
  }
2961
- async function requestWithRetries(axiosInstance, config, logger) {
2947
+ async function requestWithRetries(axiosInstance, config, maxGetRetries, logger) {
2962
2948
  let retryCount = 0;
2963
2949
  while (true) try {
2964
2950
  return await axiosInstance.request(config);
@@ -2980,7 +2966,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2980
2966
  });
2981
2967
  throw error;
2982
2968
  }
2983
- if (retryCount >= 3) {
2969
+ if (retryCount >= maxGetRetries) {
2984
2970
  emitClientLog(logger, {
2985
2971
  level: status === 429 ? "warning" : "error",
2986
2972
  logger: "hevy-api",
@@ -2988,7 +2974,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2988
2974
  message: "Hevy API request failed after retries",
2989
2975
  status,
2990
2976
  attempt: retryCount + 1,
2991
- maxAttempts: 4,
2977
+ maxAttempts: maxGetRetries + 1,
2992
2978
  method,
2993
2979
  endpoint
2994
2980
  }
@@ -3006,7 +2992,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
3006
2992
  message: status === 429 ? "Hevy API rate limit; retrying request" : "Retrying Hevy API request",
3007
2993
  status,
3008
2994
  attempt: retryCount + 1,
3009
- maxAttempts: 4,
2995
+ maxAttempts: maxGetRetries + 1,
3010
2996
  delayMs: retryDelayMs,
3011
2997
  ...status === 429 ? { retryAfterMs: retryAfterMs ?? null } : {},
3012
2998
  method,
@@ -3016,10 +3002,10 @@ async function requestWithRetries(axiosInstance, config, logger) {
3016
3002
  await sleep(retryDelayMs);
3017
3003
  }
3018
3004
  }
3019
- function createResilientClient(axiosInstance, logger) {
3005
+ function createResilientClient(axiosInstance, maxGetRetries, logger) {
3020
3006
  let clientConfig = { baseURL: axiosInstance.defaults.baseURL };
3021
3007
  const resilientClient = (async (config) => {
3022
- return requestWithRetries(axiosInstance, config, logger);
3008
+ return requestWithRetries(axiosInstance, config, maxGetRetries, logger);
3023
3009
  });
3024
3010
  resilientClient.getConfig = () => ({ ...clientConfig });
3025
3011
  resilientClient.setConfig = (config) => {
@@ -3075,9 +3061,11 @@ function finalizeRequestTrace(opts) {
3075
3061
  }
3076
3062
  function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {}) {
3077
3063
  const { logger } = options;
3064
+ const maxGetRetries = normalizeMaxGetRetries(options.maxGetRetries);
3065
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
3078
3066
  const axiosInstance = axios.create({
3079
3067
  baseURL: baseUrl,
3080
- timeout: getApiTimeoutMs(),
3068
+ timeout: timeoutMs,
3081
3069
  headers: { "api-key": apiKey }
3082
3070
  });
3083
3071
  axiosInstance.interceptors.request.use((config) => {
@@ -3085,11 +3073,13 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
3085
3073
  const method = (config.method ?? "get").toUpperCase();
3086
3074
  const url = config.url ?? "";
3087
3075
  const endpoint = url.split("?")[0] ?? url;
3076
+ const userId = getCurrentUserId();
3088
3077
  tracedConfig._span = tracer.startSpan(`hevy.api.${method}`, { attributes: {
3089
3078
  "http.method": method,
3090
3079
  "http.url": url,
3091
3080
  "http.base_url": config.baseURL ?? "",
3092
- "hevy.api.endpoint": endpoint
3081
+ "hevy.api.endpoint": endpoint,
3082
+ ...userId ? { "user.id": userId } : {}
3093
3083
  } });
3094
3084
  tracedConfig._startTime = Date.now();
3095
3085
  return config;
@@ -3141,7 +3131,7 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
3141
3131
  throw error;
3142
3132
  });
3143
3133
  const headers = { "api-key": apiKey };
3144
- const client = createResilientClient(axiosInstance, logger);
3134
+ const client = createResilientClient(axiosInstance, maxGetRetries, logger);
3145
3135
  return {
3146
3136
  getWorkouts: (params) => wrapApi(getV1Workouts)(headers, params, { client }),
3147
3137
  getWorkout: (workoutId) => wrapApi(getV1WorkoutsWorkoutid)(workoutId, headers, { client }),
@@ -3542,16 +3532,13 @@ const HELP_TEXT = [
3542
3532
  "Options:",
3543
3533
  " -h, --help Show this help message and exit",
3544
3534
  " -v, --version Show version and exit",
3545
- " --hevy-api-key=<api-key> (deprecated, use HEVY_API_KEY env var)",
3546
3535
  "",
3547
3536
  "Environment:",
3548
3537
  " HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
3549
3538
  " HEVY_MCP_DEBUG=1 Enable verbose diagnostics on stderr",
3550
3539
  "",
3551
3540
  "Examples:",
3552
- " HEVY_API_KEY=your-key npx hevy-mcp",
3553
- " npx hevy-mcp --hevy-api-key=your-key",
3554
- " npm start -- --hevy-api-key=your-key"
3541
+ " HEVY_API_KEY=your-key npx hevy-mcp"
3555
3542
  ].join("\n");
3556
3543
  function getCliAction(args) {
3557
3544
  for (const arg of args) {
@@ -3561,6 +3548,21 @@ function getCliAction(args) {
3561
3548
  return "start";
3562
3549
  }
3563
3550
  const HEVY_API_BASEURL = "https://api.hevyapp.com";
3551
+ const STARTUP_PROBE_TIMEOUT_MS = 5e3;
3552
+ const INVALID_API_KEY_MESSAGE = "HEVY_API_KEY is invalid or expired. Please check your API key in the Hevy app under Settings > API Key.";
3553
+ const API_KEY_VALIDATION_WARNING = "Warning: HEVY_API_KEY could not be validated during startup. Startup will continue; check your network connection and Hevy API availability.";
3554
+ const SAFE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
3555
+ "EAI_AGAIN",
3556
+ "ECONNABORTED",
3557
+ "ECONNREFUSED",
3558
+ "ECONNRESET",
3559
+ "ENETUNREACH",
3560
+ "ENOTFOUND",
3561
+ "ERR_NETWORK",
3562
+ "ERR_SOCKET_TIMEOUT",
3563
+ "ETIMEDOUT",
3564
+ "HEVY_RETRY_EXHAUSTED"
3565
+ ]);
3564
3566
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
3565
3567
  function fingerprintApiKey(apiKey) {
3566
3568
  return createHmac("sha256", apiKey).update(SENTRY_USER_ID_CONTEXT).digest("hex").slice(0, 10);
@@ -3589,6 +3591,33 @@ function createToolCountingServer(server) {
3589
3591
  getCount: () => count
3590
3592
  };
3591
3593
  }
3594
+ function getHttpStatus(error) {
3595
+ if (!error || typeof error !== "object" || !("response" in error)) return;
3596
+ const response = error.response;
3597
+ if (!response || typeof response !== "object" || !("status" in response)) return;
3598
+ return typeof response.status === "number" && Number.isInteger(response.status) && response.status >= 100 && response.status <= 599 ? response.status : void 0;
3599
+ }
3600
+ function getSafeValidationDiagnostic(error) {
3601
+ const status = getHttpStatus(error);
3602
+ if (status !== void 0) return `HTTP ${status}`;
3603
+ if (!error || typeof error !== "object" || !("code" in error)) return;
3604
+ const code = error.code;
3605
+ return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
3606
+ }
3607
+ async function validateApiKey(apiKey) {
3608
+ const startupProbeClient = createClient(apiKey, HEVY_API_BASEURL, {
3609
+ maxGetRetries: 0,
3610
+ timeoutMs: STARTUP_PROBE_TIMEOUT_MS
3611
+ });
3612
+ try {
3613
+ await startupProbeClient.getUserInfo();
3614
+ } catch (error) {
3615
+ const status = getHttpStatus(error);
3616
+ if (status === 401 || status === 403) throw new Error(INVALID_API_KEY_MESSAGE);
3617
+ const diagnostic = getSafeValidationDiagnostic(error);
3618
+ console.error(diagnostic ? `${API_KEY_VALIDATION_WARNING} Diagnostic: ${diagnostic}.` : API_KEY_VALIDATION_WARNING);
3619
+ }
3620
+ }
3592
3621
  function buildServer(apiKey) {
3593
3622
  const userId = fingerprintApiKey(apiKey);
3594
3623
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
@@ -3620,12 +3649,7 @@ function buildServer(apiKey) {
3620
3649
  tracer.startActiveSpan("mcp.tools.register", (toolsSpan) => {
3621
3650
  try {
3622
3651
  const counting = createToolCountingServer(server);
3623
- registerWorkoutTools(counting.server, hevyClient);
3624
- registerRoutineTools(counting.server, hevyClient);
3625
- registerTemplateTools(counting.server, hevyClient, { logger: clientLogger });
3626
- registerFolderTools(counting.server, hevyClient);
3627
- registerBodyMeasurementTools(counting.server, hevyClient);
3628
- registerUserTools(counting.server, hevyClient);
3652
+ registerHevyTools(counting.server, hevyClient, { logger: clientLogger });
3629
3653
  toolsSpan.setAttribute("mcp.tools.count", counting.getCount());
3630
3654
  } finally {
3631
3655
  toolsSpan.end();
@@ -3649,13 +3673,13 @@ function buildServer(apiKey) {
3649
3673
  }
3650
3674
  });
3651
3675
  }
3652
- function createServer({ config }) {
3676
+ async function createServer({ config }) {
3653
3677
  const { apiKey } = serverConfigSchema.parse(config);
3678
+ await validateApiKey(apiKey);
3654
3679
  return buildServer(apiKey);
3655
3680
  }
3656
3681
  async function runServer() {
3657
- const args = process.argv.slice(2);
3658
- const cliAction = getCliAction(args);
3682
+ const cliAction = getCliAction(process.argv.slice(2));
3659
3683
  if (cliAction === "version") {
3660
3684
  console.error(`${name} v${version}`);
3661
3685
  return;
@@ -3667,9 +3691,9 @@ async function runServer() {
3667
3691
  serverStartups.add(1, { version });
3668
3692
  await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
3669
3693
  try {
3670
- const apiKey = parseConfig(args, process.env).apiKey;
3694
+ const apiKey = parseConfig(process.env).apiKey;
3671
3695
  assertApiKey(apiKey);
3672
- const server = buildServer(apiKey);
3696
+ const server = await createServer({ config: { apiKey } });
3673
3697
  console.error("Starting MCP server in stdio mode");
3674
3698
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
3675
3699
  await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
@@ -3700,4 +3724,4 @@ async function runServer() {
3700
3724
  //#endregion
3701
3725
  export { createServer as n, runServer as r, configSchema as t };
3702
3726
 
3703
- //# sourceMappingURL=src-CHT_Meh8.mjs.map
3727
+ //# sourceMappingURL=src-BBzEvkOX.mjs.map