hevy-mcp 1.28.1-beta.1 → 2.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,26 @@ 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
+ Use `latest` to follow the newest stable release. For reproducible deployments,
107
+ pin the exact version shown on the release, using a tag such as
108
+ `ghcr.io/chrisdoc/hevy-mcp:X.Y.Z`. Major (`:X`) and major.minor (`:X.Y`) tags
109
+ are also published for controlled automatic updates.
110
+
107
111
  ---
108
112
 
109
113
  ## 🔗 Integration
@@ -138,6 +142,41 @@ If you prefer Bun, swap the launcher fields:
138
142
  }
139
143
  ```
140
144
 
145
+ To run Claude Desktop through Docker instead, first create an environment file
146
+ outside the repository containing your real key:
147
+
148
+ ```dotenv
149
+ HEVY_API_KEY=replace_with_your_real_key
150
+ ```
151
+
152
+ Restrict access to that file where supported (for example,
153
+ `chmod 600 /absolute/path/to/hevy-mcp.env`), then use its absolute path in the
154
+ Claude Desktop configuration:
155
+
156
+ ```json
157
+ {
158
+ "mcpServers": {
159
+ "hevy-mcp": {
160
+ "command": "docker",
161
+ "args": [
162
+ "run",
163
+ "-i",
164
+ "--rm",
165
+ "--env-file",
166
+ "/absolute/path/to/hevy-mcp.env",
167
+ "ghcr.io/chrisdoc/hevy-mcp:latest"
168
+ ]
169
+ }
170
+ }
171
+ }
172
+ ```
173
+
174
+ This configuration runs the same stdio server inside the container; it does
175
+ not expose an HTTP port or start a detached service. Docker reads the key from
176
+ the environment file, so the Claude configuration does not replace an
177
+ inherited key with a placeholder. Replace `latest` with an exact version tag if
178
+ you want Claude Desktop to stay on a pinned release.
179
+
141
180
  ### Cursor Configuration
142
181
 
143
182
  Add this server under `"mcpServers"` in `~/.cursor/mcp.json`:
@@ -241,10 +280,11 @@ paging behavior explicit and avoid cross-page invalidation complexity.
241
280
  <details>
242
281
  <summary><strong>⚠️ Migration Note (v1.18.0)</strong></summary>
243
282
 
244
- As of **v1.18.0**, `hevy-mcp` removed both HTTP/SSE transport and Docker
245
- support.
283
+ As of **v1.18.0**, `hevy-mcp` removed HTTP/SSE transport and its previous
284
+ Docker packaging. Docker support is now available again for the stdio server.
246
285
 
247
- The supported path is stdio via `npx hevy-mcp`.
286
+ Both `npx hevy-mcp` and the official container image use stdio; HTTP ports and
287
+ detached-container deployment are not supported.
248
288
 
249
289
  </details>
250
290
 
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@2.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-c3mB-R0m.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@2.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-c3mB-R0m.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@2.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] = "17d5086b-9850-447d-9372-27bb2ce5dcc5", e._sentryDebugIdIdentifier = "sentry-dbid-17d5086b-9850-447d-9372-27bb2ce5dcc5");
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 = "2.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@2.0.0`;
48
48
  const resource = resourceFromAttributes({
49
49
  "service.name": name$1,
50
50
  "service.version": version$1
@@ -2871,6 +2871,15 @@ function getApiTimeoutMs() {
2871
2871
  if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
2872
2872
  return Math.trunc(parsed);
2873
2873
  }
2874
+ function normalizeMaxGetRetries(value) {
2875
+ if (value === void 0 || !Number.isFinite(value) || value < 0) return 3;
2876
+ return Math.floor(value);
2877
+ }
2878
+ function normalizeTimeoutMs(value) {
2879
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) return getApiTimeoutMs();
2880
+ const normalizedValue = Math.floor(value);
2881
+ return normalizedValue > 0 ? normalizedValue : getApiTimeoutMs();
2882
+ }
2874
2883
  function sleep(ms) {
2875
2884
  return new Promise((resolve) => {
2876
2885
  setTimeout(resolve, ms);
@@ -2958,7 +2967,7 @@ function markRetryExhausted(error, retryCount) {
2958
2967
  if (retryError.code) retryError.hevyRetryOriginalCode = retryError.code;
2959
2968
  retryError.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
2960
2969
  }
2961
- async function requestWithRetries(axiosInstance, config, logger) {
2970
+ async function requestWithRetries(axiosInstance, config, maxGetRetries, logger) {
2962
2971
  let retryCount = 0;
2963
2972
  while (true) try {
2964
2973
  return await axiosInstance.request(config);
@@ -2980,7 +2989,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2980
2989
  });
2981
2990
  throw error;
2982
2991
  }
2983
- if (retryCount >= 3) {
2992
+ if (retryCount >= maxGetRetries) {
2984
2993
  emitClientLog(logger, {
2985
2994
  level: status === 429 ? "warning" : "error",
2986
2995
  logger: "hevy-api",
@@ -2988,7 +2997,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
2988
2997
  message: "Hevy API request failed after retries",
2989
2998
  status,
2990
2999
  attempt: retryCount + 1,
2991
- maxAttempts: 4,
3000
+ maxAttempts: maxGetRetries + 1,
2992
3001
  method,
2993
3002
  endpoint
2994
3003
  }
@@ -3006,7 +3015,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
3006
3015
  message: status === 429 ? "Hevy API rate limit; retrying request" : "Retrying Hevy API request",
3007
3016
  status,
3008
3017
  attempt: retryCount + 1,
3009
- maxAttempts: 4,
3018
+ maxAttempts: maxGetRetries + 1,
3010
3019
  delayMs: retryDelayMs,
3011
3020
  ...status === 429 ? { retryAfterMs: retryAfterMs ?? null } : {},
3012
3021
  method,
@@ -3016,10 +3025,10 @@ async function requestWithRetries(axiosInstance, config, logger) {
3016
3025
  await sleep(retryDelayMs);
3017
3026
  }
3018
3027
  }
3019
- function createResilientClient(axiosInstance, logger) {
3028
+ function createResilientClient(axiosInstance, maxGetRetries, logger) {
3020
3029
  let clientConfig = { baseURL: axiosInstance.defaults.baseURL };
3021
3030
  const resilientClient = (async (config) => {
3022
- return requestWithRetries(axiosInstance, config, logger);
3031
+ return requestWithRetries(axiosInstance, config, maxGetRetries, logger);
3023
3032
  });
3024
3033
  resilientClient.getConfig = () => ({ ...clientConfig });
3025
3034
  resilientClient.setConfig = (config) => {
@@ -3075,9 +3084,11 @@ function finalizeRequestTrace(opts) {
3075
3084
  }
3076
3085
  function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {}) {
3077
3086
  const { logger } = options;
3087
+ const maxGetRetries = normalizeMaxGetRetries(options.maxGetRetries);
3088
+ const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
3078
3089
  const axiosInstance = axios.create({
3079
3090
  baseURL: baseUrl,
3080
- timeout: getApiTimeoutMs(),
3091
+ timeout: timeoutMs,
3081
3092
  headers: { "api-key": apiKey }
3082
3093
  });
3083
3094
  axiosInstance.interceptors.request.use((config) => {
@@ -3085,11 +3096,13 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
3085
3096
  const method = (config.method ?? "get").toUpperCase();
3086
3097
  const url = config.url ?? "";
3087
3098
  const endpoint = url.split("?")[0] ?? url;
3099
+ const userId = getCurrentUserId();
3088
3100
  tracedConfig._span = tracer.startSpan(`hevy.api.${method}`, { attributes: {
3089
3101
  "http.method": method,
3090
3102
  "http.url": url,
3091
3103
  "http.base_url": config.baseURL ?? "",
3092
- "hevy.api.endpoint": endpoint
3104
+ "hevy.api.endpoint": endpoint,
3105
+ ...userId ? { "user.id": userId } : {}
3093
3106
  } });
3094
3107
  tracedConfig._startTime = Date.now();
3095
3108
  return config;
@@ -3141,7 +3154,7 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
3141
3154
  throw error;
3142
3155
  });
3143
3156
  const headers = { "api-key": apiKey };
3144
- const client = createResilientClient(axiosInstance, logger);
3157
+ const client = createResilientClient(axiosInstance, maxGetRetries, logger);
3145
3158
  return {
3146
3159
  getWorkouts: (params) => wrapApi(getV1Workouts)(headers, params, { client }),
3147
3160
  getWorkout: (workoutId) => wrapApi(getV1WorkoutsWorkoutid)(workoutId, headers, { client }),
@@ -3561,6 +3574,21 @@ function getCliAction(args) {
3561
3574
  return "start";
3562
3575
  }
3563
3576
  const HEVY_API_BASEURL = "https://api.hevyapp.com";
3577
+ const STARTUP_PROBE_TIMEOUT_MS = 5e3;
3578
+ 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.";
3579
+ 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.";
3580
+ const SAFE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
3581
+ "EAI_AGAIN",
3582
+ "ECONNABORTED",
3583
+ "ECONNREFUSED",
3584
+ "ECONNRESET",
3585
+ "ENETUNREACH",
3586
+ "ENOTFOUND",
3587
+ "ERR_NETWORK",
3588
+ "ERR_SOCKET_TIMEOUT",
3589
+ "ETIMEDOUT",
3590
+ "HEVY_RETRY_EXHAUSTED"
3591
+ ]);
3564
3592
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
3565
3593
  function fingerprintApiKey(apiKey) {
3566
3594
  return createHmac("sha256", apiKey).update(SENTRY_USER_ID_CONTEXT).digest("hex").slice(0, 10);
@@ -3589,6 +3617,33 @@ function createToolCountingServer(server) {
3589
3617
  getCount: () => count
3590
3618
  };
3591
3619
  }
3620
+ function getHttpStatus(error) {
3621
+ if (!error || typeof error !== "object" || !("response" in error)) return;
3622
+ const response = error.response;
3623
+ if (!response || typeof response !== "object" || !("status" in response)) return;
3624
+ return typeof response.status === "number" && Number.isInteger(response.status) && response.status >= 100 && response.status <= 599 ? response.status : void 0;
3625
+ }
3626
+ function getSafeValidationDiagnostic(error) {
3627
+ const status = getHttpStatus(error);
3628
+ if (status !== void 0) return `HTTP ${status}`;
3629
+ if (!error || typeof error !== "object" || !("code" in error)) return;
3630
+ const code = error.code;
3631
+ return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
3632
+ }
3633
+ async function validateApiKey(apiKey) {
3634
+ const startupProbeClient = createClient(apiKey, HEVY_API_BASEURL, {
3635
+ maxGetRetries: 0,
3636
+ timeoutMs: STARTUP_PROBE_TIMEOUT_MS
3637
+ });
3638
+ try {
3639
+ await startupProbeClient.getUserInfo();
3640
+ } catch (error) {
3641
+ const status = getHttpStatus(error);
3642
+ if (status === 401 || status === 403) throw new Error(INVALID_API_KEY_MESSAGE);
3643
+ const diagnostic = getSafeValidationDiagnostic(error);
3644
+ console.error(diagnostic ? `${API_KEY_VALIDATION_WARNING} Diagnostic: ${diagnostic}.` : API_KEY_VALIDATION_WARNING);
3645
+ }
3646
+ }
3592
3647
  function buildServer(apiKey) {
3593
3648
  const userId = fingerprintApiKey(apiKey);
3594
3649
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
@@ -3649,8 +3704,9 @@ function buildServer(apiKey) {
3649
3704
  }
3650
3705
  });
3651
3706
  }
3652
- function createServer({ config }) {
3707
+ async function createServer({ config }) {
3653
3708
  const { apiKey } = serverConfigSchema.parse(config);
3709
+ await validateApiKey(apiKey);
3654
3710
  return buildServer(apiKey);
3655
3711
  }
3656
3712
  async function runServer() {
@@ -3669,7 +3725,7 @@ async function runServer() {
3669
3725
  try {
3670
3726
  const apiKey = parseConfig(args, process.env).apiKey;
3671
3727
  assertApiKey(apiKey);
3672
- const server = buildServer(apiKey);
3728
+ const server = await createServer({ config: { apiKey } });
3673
3729
  console.error("Starting MCP server in stdio mode");
3674
3730
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
3675
3731
  await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
@@ -3700,4 +3756,4 @@ async function runServer() {
3700
3756
  //#endregion
3701
3757
  export { createServer as n, runServer as r, configSchema as t };
3702
3758
 
3703
- //# sourceMappingURL=src-CHT_Meh8.mjs.map
3759
+ //# sourceMappingURL=src-c3mB-R0m.mjs.map