hevy-mcp 6.1.0 → 6.1.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
@@ -80,7 +80,7 @@ so your client does not need Node.js, Bun, Docker, or a local server process.
80
80
  Production URL:
81
81
 
82
82
  ```text
83
- https://hevy.chrisdoc.dev/mcp
83
+ https://mcp.hevy-mcp.dev/mcp
84
84
  ```
85
85
 
86
86
  The endpoint uses Streamable HTTP. Send your Hevy API key as a bearer token on
@@ -95,7 +95,7 @@ Codex, then add the hosted server:
95
95
  ```bash
96
96
  export HEVY_API_KEY=your-hevy-api-key
97
97
  codex mcp add hevy \
98
- --url https://hevy.chrisdoc.dev/mcp \
98
+ --url https://mcp.hevy-mcp.dev/mcp \
99
99
  --bearer-token-env-var HEVY_API_KEY
100
100
  ```
101
101
 
@@ -111,7 +111,7 @@ Clients that accept a remote MCP URL and fixed headers commonly use this shape:
111
111
  {
112
112
  "mcpServers": {
113
113
  "hevy": {
114
- "url": "https://hevy.chrisdoc.dev/mcp",
114
+ "url": "https://mcp.hevy-mcp.dev/mcp",
115
115
  "headers": {
116
116
  "Authorization": "Bearer your-hevy-api-key"
117
117
  }
@@ -363,7 +363,7 @@ corresponding delete tools.
363
363
  The production MCP server is live at:
364
364
 
365
365
  ```text
366
- https://hevy.chrisdoc.dev/mcp
366
+ https://mcp.hevy-mcp.dev/mcp
367
367
  ```
368
368
 
369
369
  It is the quickest way to use `hevy-mcp`: there is nothing to install or keep
@@ -377,7 +377,7 @@ Clients must send their Hevy API key as a fixed authorization header:
377
377
  {
378
378
  "mcpServers": {
379
379
  "hevy": {
380
- "url": "https://hevy.chrisdoc.dev/mcp",
380
+ "url": "https://mcp.hevy-mcp.dev/mcp",
381
381
  "headers": {
382
382
  "Authorization": "Bearer your-hevy-api-key"
383
383
  }
package/dist/cli.mjs CHANGED
@@ -8,8 +8,8 @@
8
8
  n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "d805d34c-9cce-4622-8b67-728aa0cacb5d", e._sentryDebugIdIdentifier = "sentry-dbid-d805d34c-9cce-4622-8b67-728aa0cacb5d");
9
9
  } catch (e) {}
10
10
  })();
11
- import { i as flushTelemetry, r as MissingHevyApiKeyError, t as runServer } from "./runtime-D_9Jylpe.mjs";
12
- import { a as createSafeErrorDiagnostic } from "./src-tzdOTpis.mjs";
11
+ import { i as flushTelemetry, r as MissingHevyApiKeyError, t as runServer } from "./runtime-CQ5qda5F.mjs";
12
+ import { a as createSafeErrorDiagnostic } from "./src-c7i9L8Mm.mjs";
13
13
  //#region src/cli.ts
14
14
  runServer().catch(async (error) => {
15
15
  if (error instanceof MissingHevyApiKeyError) console.error(error.message);
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // Generated with tsdown
3
3
  // https://tsdown.dev
4
- import { o as createHevyClient, t as createHevyMcpServer } from "./src-tzdOTpis.mjs";
4
+ import { o as createHevyClient, t as createHevyMcpServer } from "./src-c7i9L8Mm.mjs";
5
5
  import { z } from "zod";
6
6
  //#region src/index.ts
7
7
  const serverConfigSchema = z.object({ apiKey: z.string().min(1, "Hevy API key is required").describe("Your Hevy API key (available in the Hevy app settings).") });
@@ -32,7 +32,7 @@ function createNodeMcpServer({ apiKey }, _transport = "stdio", lifecycleSignal)
32
32
  * does not evaluate the runtime bootstrap; it is loaded only when invoked.
33
33
  */
34
34
  async function runStdioServer() {
35
- const { runStdioServer: run } = await import("./runtime-D_9Jylpe.mjs").then((n) => n.n);
35
+ const { runStdioServer: run } = await import("./runtime-CQ5qda5F.mjs").then((n) => n.n);
36
36
  return run();
37
37
  }
38
38
  /**
@@ -40,7 +40,7 @@ async function runStdioServer() {
40
40
  * does not evaluate the runtime bootstrap; it is loaded only when invoked.
41
41
  */
42
42
  async function runServer() {
43
- const { runServer: run } = await import("./runtime-D_9Jylpe.mjs").then((n) => n.n);
43
+ const { runServer: run } = await import("./runtime-CQ5qda5F.mjs").then((n) => n.n);
44
44
  return run();
45
45
  }
46
46
  //#endregion
@@ -5,10 +5,10 @@
5
5
  try {
6
6
  var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
7
7
  var n = new e.Error().stack;
8
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "b2b3cdcb-1211-4999-8f1f-facca1d41d6b", e._sentryDebugIdIdentifier = "sentry-dbid-b2b3cdcb-1211-4999-8f1f-facca1d41d6b");
8
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "c292bf26-255c-46b3-9cb2-3343afe7b7ce", e._sentryDebugIdIdentifier = "sentry-dbid-c292bf26-255c-46b3-9cb2-3343afe7b7ce");
9
9
  } catch (e) {}
10
10
  })();
11
- import { a as createSafeErrorDiagnostic, c as diagnosticEndpointIdentity, i as bucketCount, l as metricEndpointIdentity, n as createExecutionProjection, o as createHevyClient, r as mergeAbortSignals, s as SAFE_OBSERVATION_CODES, t as createHevyMcpServer, u as isHevyHttpError } from "./src-tzdOTpis.mjs";
11
+ import { a as createSafeErrorDiagnostic, c as diagnosticEndpointIdentity, i as bucketCount, l as metricEndpointIdentity, n as createExecutionProjection, o as createHevyClient, r as mergeAbortSignals, s as SAFE_OBSERVATION_CODES, t as createHevyMcpServer, u as isHevyHttpError } from "./src-c7i9L8Mm.mjs";
12
12
  import { ZodError, z } from "zod";
13
13
  import { deserializeMessage } from "@modelcontextprotocol/server";
14
14
  import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
@@ -69,6 +69,7 @@ function removeHomePath(value) {
69
69
  const home = process.env.HOME?.trim();
70
70
  return (home ? value.replace(new RegExp(escapeRegExp(home), "g"), "~") : value).replace(/\/(?:Users|home)\/[^/\s]+/g, "~");
71
71
  }
72
+ const EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
72
73
  function removeControlCharacters(value) {
73
74
  let output = "";
74
75
  let inEscapeSequence = false;
@@ -90,7 +91,7 @@ function removeControlCharacters(value) {
90
91
  }
91
92
  /** Remove common credentials and URL payloads without inspecting object fields. */
92
93
  function sanitizeDiagnosticText(value, maxLength = MAX_MESSAGE_LENGTH) {
93
- return truncate(removeHomePath(removeControlCharacters(value).replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/\b(api[-_ ]?key|authorization|password|secret|token)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1=[REDACTED]").replace(/\bhttps?:\/\/[^\s)<>]+/gi, "[URL]")).trim(), maxLength);
94
+ return truncate(removeHomePath(removeControlCharacters(value).replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [REDACTED]").replace(/\b(api[-_ ]?key|authorization|password|secret|token)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;]+)/gi, "$1=[REDACTED]").replace(EMAIL, "[EMAIL_REDACTED]").replace(/\bhttps?:\/\/[^\s)<>]+/gi, "[URL]")).trim(), maxLength);
94
95
  }
95
96
  function sanitizeAttributeValue(_key, value) {
96
97
  if (typeof value !== "string") return value;
@@ -162,6 +163,7 @@ function normalizeFailure(error, context) {
162
163
  const text = getExceptionText(error);
163
164
  const message = DIAGNOSTIC_DETAILS_ENABLED && text.message ? sanitizeDiagnosticText(text.message) : void 0;
164
165
  const stack = DIAGNOSTIC_DETAILS_ENABLED && text.stack ? sanitizeDiagnosticText(text.stack, MAX_STACK_LENGTH) : void 0;
166
+ const responseError = DIAGNOSTIC_DETAILS_ENABLED && isHevyHttpError(error) && error.responseError ? sanitizeDiagnosticText(error.responseError, MAX_ATTRIBUTE_LENGTH) : void 0;
165
167
  attributes["exception.type"] = exceptionType;
166
168
  addErrorAttributes(attributes, diagnostic, context.expected);
167
169
  addHttpAttributes(attributes, diagnostic);
@@ -171,6 +173,7 @@ function normalizeFailure(error, context) {
171
173
  exceptionType,
172
174
  ...message ? { message } : {},
173
175
  ...stack ? { stack } : {},
176
+ ...responseError ? { responseError } : {},
174
177
  exception: {
175
178
  name: exceptionType,
176
179
  ...message ? { message } : {},
@@ -289,13 +292,17 @@ function captureFailure(error, context) {
289
292
  scope.setFingerprint([...record.fingerprint]);
290
293
  scope.setContext("mcp", {
291
294
  kind: context.kind,
292
- ...record.attributes
295
+ ...record.attributes,
296
+ ...record.responseError ? { "hevy.api.response_error": record.responseError } : {}
293
297
  });
294
298
  return Sentry.captureException(createSentryError(record));
295
299
  });
296
300
  } catch {}
297
301
  if (span) try {
298
- if (!duplicate && !context.expected) span.recordException(record.exception);
302
+ if (!duplicate && !context.expected) {
303
+ span.recordException(record.exception);
304
+ if (record.responseError) span.addEvent("mcp.failure.detail", { "hevy.api.response_error": record.responseError });
305
+ }
299
306
  setSpanAttributes(span, record.attributes);
300
307
  if (!context.expected) span.setStatus({ code: SpanStatusCode.ERROR });
301
308
  if (eventId) span.setAttribute("sentry.event_id", eventId);
@@ -358,7 +365,7 @@ function installProcessExceptionTracking(processLike = process) {
358
365
  };
359
366
  }
360
367
  const name$1 = "hevy-mcp";
361
- const version$1 = "6.1.0";
368
+ const version$1 = "6.1.1";
362
369
  const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
363
370
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
364
371
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
@@ -1237,6 +1244,9 @@ function safeErrorAttributes(observation) {
1237
1244
  ...code && SAFE_OBSERVATION_CODES.has(code) ? { error_code: code } : {}
1238
1245
  };
1239
1246
  }
1247
+ function getDiagnosticResponseError(observation) {
1248
+ return process.env.HEVY_MCP_TELEMETRY_DIAGNOSTICS === "0" ? void 0 : observation.error?.response_error;
1249
+ }
1240
1250
  function startApiSpan(start) {
1241
1251
  const sessionId = getCurrentMcpSessionId();
1242
1252
  const diagnosticEndpoint = diagnosticEndpointIdentity(start.endpoint);
@@ -1256,10 +1266,14 @@ function finishApiSpan(span, observation) {
1256
1266
  if (observation.expectedReason) span.setAttribute("hevy.api.expected_reason", observation.expectedReason);
1257
1267
  for (const [key, value] of Object.entries(errorAttributes)) span.setAttribute(key, value);
1258
1268
  span.setStatus({ code: observation.outcome === "success" || observation.outcome === "expected" ? SpanStatusCode.OK : SpanStatusCode.ERROR });
1259
- if (observation.outcome !== "success" && observation.outcome !== "expected") span.addEvent("hevy.api.failure", {
1260
- "error.category": errorAttributes.error_category ?? "HevyHttpError",
1261
- ...errorAttributes.error_code ? { "error.code": errorAttributes.error_code } : {}
1262
- });
1269
+ if (observation.outcome !== "success" && observation.outcome !== "expected") {
1270
+ const responseError = getDiagnosticResponseError(observation);
1271
+ span.addEvent("hevy.api.failure", {
1272
+ "error.category": errorAttributes.error_category ?? "HevyHttpError",
1273
+ ...errorAttributes.error_code ? { "error.code": errorAttributes.error_code } : {},
1274
+ ...responseError ? { "hevy.api.response_error": responseError } : {}
1275
+ });
1276
+ }
1263
1277
  span.end();
1264
1278
  }
1265
1279
  /** Node-only adapter; the Worker graph never imports telemetry or metrics. */
@@ -1283,6 +1297,7 @@ function createNodeHevyClientOptions() {
1283
1297
  onRequestComplete(observation) {
1284
1298
  const retryCountBucket = bucketCount(observation.retryCount);
1285
1299
  const errorAttributes = safeErrorAttributes(observation);
1300
+ const responseError = getDiagnosticResponseError(observation);
1286
1301
  const metricEndpoint = metricEndpointIdentity(observation.endpoint);
1287
1302
  const diagnosticEndpoint = diagnosticEndpointIdentity(observation.endpoint);
1288
1303
  const metricExecutionAttributes = projectExecutionAttributes(observation, "metric");
@@ -1312,6 +1327,7 @@ function createNodeHevyClientOptions() {
1312
1327
  status: observation.status || null,
1313
1328
  retryCountBucket,
1314
1329
  outcome: observation.outcome,
1330
+ ...responseError ? { response_error: responseError } : {},
1315
1331
  ...errorAttributes
1316
1332
  });
1317
1333
  },
@@ -1905,7 +1921,15 @@ function installToolCallTracking(handlers, transport) {
1905
1921
  } else span.setStatus({ code: SpanStatusCode.OK });
1906
1922
  return result;
1907
1923
  } catch (error) {
1908
- markSdkToolFailure(span, error, toolName);
1924
+ const validation = error instanceof Error ? classifySdkValidation(error.message) : {
1925
+ kind: "unknown",
1926
+ expected: false
1927
+ };
1928
+ if (validation.kind === "tool_not_found") markSdkToolFailure(span, error, toolName, "validation", {
1929
+ expected: true,
1930
+ validationKind: validation.kind
1931
+ });
1932
+ else markSdkToolFailure(span, error, toolName);
1909
1933
  throw error;
1910
1934
  } finally {
1911
1935
  span.end();
@@ -2503,4 +2527,4 @@ async function runServer() {
2503
2527
  //#endregion
2504
2528
  export { flushTelemetry as i, runtime_exports as n, MissingHevyApiKeyError as r, runServer as t };
2505
2529
 
2506
- //# sourceMappingURL=runtime-D_9Jylpe.mjs.map
2530
+ //# sourceMappingURL=runtime-CQ5qda5F.mjs.map
@@ -291,6 +291,70 @@ const getV1WorkoutsWorkoutid200Schema = z$1.lazy(() => workoutSchema);
291
291
  z$1.any();
292
292
  z$1.lazy(() => getV1WorkoutsWorkoutid200Schema);
293
293
  //#endregion
294
+ //#region ../hevy-client/src/response-error.ts
295
+ const MAX_RESPONSE_ERROR_INPUT_LENGTH = 4096;
296
+ const MAX_RESPONSE_ERROR_LENGTH = 256;
297
+ const RESPONSE_ERROR_KEYS = [
298
+ "error",
299
+ "message",
300
+ "detail"
301
+ ];
302
+ const COOKIE_ASSIGNMENT = /(\b(?:cookie|set-cookie)\s*[:=]\s*)[^;\s]+/gi;
303
+ const SECRET_ASSIGNMENT = /(\b(?:authorization|proxy-authorization|api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|id[-_ ]?token|token|client[-_ ]?secret|password|passwd|secret|credential|signature)\b\s*[:=]\s*)(?:"[^"]*"|'[^']*'|(?:Bearer\s+)?[^\s,;}"]+)/gi;
304
+ const BEARER_TOKEN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
305
+ const JSON_WEB_TOKEN = /\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\b/g;
306
+ const SECRET_QUERY_PARAMETER = /([?&](?:api[-_ ]?key|access[-_ ]?token|refresh[-_ ]?token|token|signature|password|secret)=)[^&#\s]+/gi;
307
+ const HTTP_URL = /\bhttps?:\/\/[^\s<>"']+/gi;
308
+ const EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
309
+ const UUID = /\b[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}\b/gi;
310
+ function isResponseErrorBody(value) {
311
+ return typeof value === "object" && value !== null;
312
+ }
313
+ function ownValue(value, key) {
314
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
315
+ return descriptor && "value" in descriptor ? descriptor.value : void 0;
316
+ }
317
+ function responseErrorText(value) {
318
+ if (!isResponseErrorBody(value)) return void 0;
319
+ for (const key of RESPONSE_ERROR_KEYS) {
320
+ const candidate = ownValue(value, key);
321
+ if (typeof candidate === "string") return candidate;
322
+ if (!isResponseErrorBody(candidate)) continue;
323
+ const nestedMessage = ownValue(candidate, "message");
324
+ if (typeof nestedMessage === "string") return nestedMessage;
325
+ const nestedDetail = ownValue(candidate, "detail");
326
+ if (typeof nestedDetail === "string") return nestedDetail;
327
+ }
328
+ }
329
+ function removeControlCharacters(value) {
330
+ let output = "";
331
+ for (const character of value) {
332
+ const codePoint = character.codePointAt(0) ?? 0;
333
+ if (codePoint <= 31 || codePoint === 127) {
334
+ if (codePoint === 9 || codePoint === 10 || codePoint === 13) output += " ";
335
+ continue;
336
+ }
337
+ output += character;
338
+ }
339
+ return output;
340
+ }
341
+ function truncate(value) {
342
+ return value.length > MAX_RESPONSE_ERROR_LENGTH ? `${value.slice(0, MAX_RESPONSE_ERROR_LENGTH - 1)}…` : value;
343
+ }
344
+ function sanitizeResponseErrorText(value) {
345
+ return truncate(removeControlCharacters(value.slice(0, MAX_RESPONSE_ERROR_INPUT_LENGTH)).replace(COOKIE_ASSIGNMENT, "$1[REDACTED]").replace(SECRET_ASSIGNMENT, "$1[REDACTED]").replace(BEARER_TOKEN, "Bearer [REDACTED]").replace(JSON_WEB_TOKEN, "[JWT_REDACTED]").replace(SECRET_QUERY_PARAMETER, "$1[REDACTED]").replace(HTTP_URL, "[URL_REDACTED]").replace(EMAIL, "[EMAIL_REDACTED]").replace(UUID, "[ID_REDACTED]").replace(/\s+/g, " ").trim());
346
+ }
347
+ /** Extract one bounded, sanitized diagnostic string from an upstream error body. */
348
+ function extractSafeResponseError(data) {
349
+ try {
350
+ const text = responseErrorText(data);
351
+ if (!text) return void 0;
352
+ return sanitizeResponseErrorText(text) || void 0;
353
+ } catch {
354
+ return;
355
+ }
356
+ }
357
+ //#endregion
294
358
  //#region ../hevy-client/src/hevy-http-error.ts
295
359
  const HEVY_RETRY_EXHAUSTED_ERROR_CODE = "HEVY_RETRY_EXHAUSTED";
296
360
  const HEVY_REQUEST_ABORTED_ERROR_CODE = "HEVY_REQUEST_ABORTED";
@@ -300,6 +364,7 @@ var HevyHttpError = class extends Error {
300
364
  status;
301
365
  statusText;
302
366
  data;
367
+ responseError;
303
368
  headers;
304
369
  method;
305
370
  endpoint;
@@ -322,6 +387,7 @@ var HevyHttpError = class extends Error {
322
387
  this.status = options.status;
323
388
  this.statusText = options.statusText;
324
389
  this.data = options.data;
390
+ this.responseError = extractSafeResponseError(options.data);
325
391
  this.headers = options.headers;
326
392
  this.method = options.method;
327
393
  this.endpoint = options.endpoint;
@@ -1530,7 +1596,8 @@ function createFailureObservation(options, failure, error, commitState, safeToRe
1530
1596
  error: {
1531
1597
  status: error.status,
1532
1598
  code: typeof error.code === "string" && SAFE_OBSERVATION_CODES.has(error.code) ? error.code : void 0,
1533
- category: error.status === void 0 ? "NetworkError" : "HevyHttpError"
1599
+ category: error.status === void 0 ? "NetworkError" : "HevyHttpError",
1600
+ ...error.responseError ? { response_error: error.responseError } : {}
1534
1601
  }
1535
1602
  };
1536
1603
  }
@@ -4445,7 +4512,7 @@ function registerHevyResources(server, runtime) {
4445
4512
  //#endregion
4446
4513
  //#region ../core/src/server-metadata.ts
4447
4514
  const SERVER_NAME = "hevy-mcp";
4448
- const SERVER_VERSION = "6.1.0";
4515
+ const SERVER_VERSION = "6.1.1";
4449
4516
  const SERVER_INSTRUCTIONS = [
4450
4517
  "Hevy MCP connects clients to the authenticated user's Hevy workout-tracking data, including workouts, routines, exercise templates, routine folders, body measurements, and profile information. HEVY_API_KEY must contain a valid Hevy API key for local stdio use.",
4451
4518
  "Safety: all get-* and search-* tools are read-only. create-* and update-* tools mutate Hevy data. Creates are additive and non-idempotent, so repeating one can create duplicates. Updates can overwrite existing data. Delete operations are not available.",
@@ -5023,4 +5090,4 @@ function createHevyMcpServer(options) {
5023
5090
  //#endregion
5024
5091
  export { createSafeErrorDiagnostic as a, diagnosticEndpointIdentity as c, bucketCount as i, metricEndpointIdentity as l, createExecutionProjection as n, createHevyClient as o, mergeAbortSignals as r, SAFE_OBSERVATION_CODES as s, createHevyMcpServer as t, isHevyHttpError as u };
5025
5092
 
5026
- //# sourceMappingURL=src-tzdOTpis.mjs.map
5093
+ //# sourceMappingURL=src-c7i9L8Mm.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-mcp",
3
- "version": "6.1.0",
3
+ "version": "6.1.1",
4
4
  "private": false,
5
5
  "description": "A Model Context Protocol (MCP) server implementation that interfaces with the Hevy fitness tracking app and its API.",
6
6
  "repository": {
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/chrisdoc/hevy-mcp",
8
8
  "source": "github"
9
9
  },
10
- "version": "6.1.0",
10
+ "version": "6.1.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "6.1.0",
15
+ "version": "6.1.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },