hevy-mcp 6.1.3 → 6.1.4

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
@@ -67,8 +67,8 @@ Try asking:
67
67
 
68
68
  ### 1. Get your Hevy API key
69
69
 
70
- Create an API key in Hevy, then keep it somewhere secure. API access currently
71
- requires a Hevy PRO subscription.
70
+ Create an API key in [Hevy's API settings](https://www.hevyapp.com/api), then
71
+ keep it somewhere secure. API access currently requires a Hevy PRO subscription.
72
72
 
73
73
  ### 2. Connect `hevy-mcp` to your client
74
74
 
@@ -557,8 +557,11 @@ metadata, and unnormalized endpoint paths remain prohibited.
557
557
  `npx -y hevy-mcp --version` in a terminal.
558
558
  - **Codex cannot see the server:** run `codex mcp list`, then start a new Codex
559
559
  session after confirming the `hevy` entry exists.
560
- - **Hosted authentication fails:** confirm the key is active, belongs to a Hevy
561
- PRO account, and is sent as `Authorization: Bearer <HEVY_API_KEY>`.
560
+ - **Hevy API returns 401:** the key is invalid, expired, revoked, or
561
+ misconfigured. Verify or create an active key at
562
+ [Hevy's API settings](https://www.hevyapp.com/api), then restart the client.
563
+ - **Hosted authentication fails:** confirm the key belongs to a Hevy PRO
564
+ account and is sent as `Authorization: Bearer <HEVY_API_KEY>`.
562
565
  - **Local authentication fails:** confirm the key is active and available to the
563
566
  MCP child process as `HEVY_API_KEY`.
564
567
  - **Need diagnostics:** set `HEVY_MCP_DEBUG=1`. Diagnostic output goes to stderr
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-BefToK0H.mjs";
12
- import { a as createSafeErrorDiagnostic } from "./src-Cb_gblD1.mjs";
11
+ import { i as flushTelemetry, r as MissingHevyApiKeyError, t as runServer } from "./runtime-CokZbz-j.mjs";
12
+ import { a as createSafeErrorDiagnostic } from "./src-DCTiE-_b.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-Cb_gblD1.mjs";
4
+ import { o as createHevyClient, t as createHevyMcpServer } from "./src-DCTiE-_b.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-BefToK0H.mjs").then((n) => n.n);
35
+ const { runStdioServer: run } = await import("./runtime-CokZbz-j.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-BefToK0H.mjs").then((n) => n.n);
43
+ const { runServer: run } = await import("./runtime-CokZbz-j.mjs").then((n) => n.n);
44
44
  return run();
45
45
  }
46
46
  //#endregion
@@ -5,12 +5,13 @@
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] = "24c53fab-f96f-4e0b-bd15-3e11b3118c86", e._sentryDebugIdIdentifier = "sentry-dbid-24c53fab-f96f-4e0b-bd15-3e11b3118c86");
8
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "baee8222-0612-498c-a1f8-cf149a59a78b", e._sentryDebugIdIdentifier = "sentry-dbid-baee8222-0612-498c-a1f8-cf149a59a78b");
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-Cb_gblD1.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-DCTiE-_b.mjs";
12
12
  import { ZodError, z } from "zod";
13
13
  import { deserializeMessage } from "@modelcontextprotocol/server";
14
+ import * as crypto from "node:crypto";
14
15
  import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
15
16
  import * as Sentry from "@sentry/node";
16
17
  import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
@@ -393,18 +394,24 @@ function parseBuildString(value, fallback) {
393
394
  return z.string().parse(value ?? fallback);
394
395
  }
395
396
  const name$1 = parseBuildString(readBuildGlobal(() => "hevy-mcp"), "hevy-mcp");
396
- const version$1 = parseBuildString(readBuildGlobal(() => "6.1.3"), "dev");
397
+ const version$1 = parseBuildString(readBuildGlobal(() => "6.1.4"), "dev");
397
398
  const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
398
399
  const collectorToken = z.string().safeParse(readBuildGlobal(() => "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo")).data ?? process.env.OTEL_COLLECTOR_TOKEN ?? "";
399
400
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
400
401
  const DEFAULT_SENTRY_DSN = "https://ce696d8333b507acbf5203eb877bce0f@o4508975499575296.ingest.de.sentry.io/4509049671647312";
401
402
  const sentryRelease = process.env.SENTRY_RELEASE ?? `${name$1}@${version$1}`;
403
+ /** Hex-encode bytes without relying on Buffer typings that vary across @types/node releases. */
404
+ function toHex$1(bytes) {
405
+ let hex = "";
406
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
407
+ return hex;
408
+ }
402
409
  function createServiceInstanceId(generate = randomUUID) {
403
410
  try {
404
411
  const generated = generate();
405
412
  if (generated.length > 0 && generated.length <= 128) return generated;
406
413
  } catch {}
407
- return randomBytes(16).toString("hex");
414
+ return toHex$1(randomBytes(16));
408
415
  }
409
416
  const resource = resourceFromAttributes({
410
417
  "service.name": name$1,
@@ -609,6 +616,19 @@ const UNKNOWN_METADATA = "unknown";
609
616
  const MAX_METADATA_LENGTH = 64;
610
617
  const SAFE_METADATA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/@-]{0,63}$/u;
611
618
  const contextStorage = new AsyncLocalStorage();
619
+ /** Hex-encode bytes without relying on Buffer typings that vary across @types/node releases. */
620
+ function toHex(bytes) {
621
+ let hex = "";
622
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
623
+ return hex;
624
+ }
625
+ function generateFallbackTelemetrySessionId() {
626
+ return `${Date.now().toString(36)}-${toHex(crypto.randomBytes(16))}`;
627
+ }
628
+ const parsedRandomUUID = z.function().output(z.string()).safeParse(crypto.randomUUID);
629
+ function generateTelemetrySessionId() {
630
+ return parsedRandomUUID.success ? parsedRandomUUID.data() : generateFallbackTelemetrySessionId();
631
+ }
612
632
  let activeStdioSession;
613
633
  function isObject$3(value) {
614
634
  return z.object({}).passthrough().safeParse(value).success;
@@ -655,7 +675,7 @@ function createMcpSessionContext(message, transport = "stdio", options = {}) {
655
675
  toolCalls: 0,
656
676
  hadToolFailure: false,
657
677
  transport,
658
- telemetrySessionId: normalizedOptions.telemetrySessionId ?? (normalizedOptions.generateTelemetrySessionId ?? randomUUID)()
678
+ telemetrySessionId: normalizedOptions.telemetrySessionId ?? (normalizedOptions.generateTelemetrySessionId ?? generateTelemetrySessionId)()
659
679
  };
660
680
  }
661
681
  function runWithMcpSessionContext(context, callback) {
@@ -958,7 +978,7 @@ async function startStreamableHttpServer(options, apiKey, createMcpServer, confi
958
978
  const wildcard = options.host === "0.0.0.0" || options.host === "::" || options.host === "[::]";
959
979
  const loopback = isLoopbackHost(options.host);
960
980
  const bearerToken = process.env[HTTP_BEARER_TOKEN];
961
- if (!loopback && !bearerToken) throw new Error(`Non-loopback HTTP mode requires ${HTTP_BEARER_TOKEN} to protect the shared Hevy account.`);
981
+ if (!loopback && !bearerToken) throw new Error(`Non-loopback HTTP mode requires ${HTTP_BEARER_TOKEN} to protect the shared Hevy account. Set ${HTTP_BEARER_TOKEN} to a secure random value, then retry.`);
962
982
  const sessions = /* @__PURE__ */ new Map();
963
983
  const pendingSessions = /* @__PURE__ */ new Set();
964
984
  const initializationControllers = /* @__PURE__ */ new Set();
@@ -2679,4 +2699,4 @@ async function runServer() {
2679
2699
  //#endregion
2680
2700
  export { flushTelemetry as i, runtime_exports as n, MissingHevyApiKeyError as r, runServer as t };
2681
2701
 
2682
- //# sourceMappingURL=runtime-BefToK0H.mjs.map
2702
+ //# sourceMappingURL=runtime-CokZbz-j.mjs.map
@@ -4,6 +4,100 @@
4
4
  import { z } from "zod";
5
5
  import { z as z$1 } from "zod/v4";
6
6
  import { McpServer } from "@modelcontextprotocol/server";
7
+ //#region ../core/src/utils/type-predicates.ts
8
+ const stringSchema$2 = z.string();
9
+ const numberSchema$1 = z.number();
10
+ const booleanSchema = z.boolean();
11
+ const objectSchema$2 = z.object({}).passthrough();
12
+ const functionSchema = z.function();
13
+ function isString$2(value) {
14
+ return stringSchema$2.safeParse(value).success;
15
+ }
16
+ function isFiniteNumber(value) {
17
+ return numberSchema$1.safeParse(value).success && Number.isFinite(value);
18
+ }
19
+ function isBoolean(value) {
20
+ return booleanSchema.safeParse(value).success;
21
+ }
22
+ function isObject$2(value) {
23
+ return value !== null && objectSchema$2.safeParse(value).success;
24
+ }
25
+ function isFunction(value) {
26
+ return functionSchema.safeParse(value).success;
27
+ }
28
+ //#endregion
29
+ //#region ../core/src/utils/compact-json-schema.ts
30
+ function isRecord(value) {
31
+ return isObject$2(value) && !Array.isArray(value);
32
+ }
33
+ function isNullSchema(value) {
34
+ return isRecord(value) && value.type === "null" && Object.keys(value).length === 1;
35
+ }
36
+ let conversionCount = 0;
37
+ function compactNullableSchema(value) {
38
+ if (Array.isArray(value)) return value.map(compactNullableSchema);
39
+ if (!isRecord(value)) return value;
40
+ const anyOf = value.anyOf;
41
+ if (Array.isArray(anyOf) && anyOf.length === 2) {
42
+ const nullableBranch = anyOf.find(isNullSchema);
43
+ const valueBranch = anyOf.find((branch) => !isNullSchema(branch));
44
+ if (nullableBranch !== void 0 && isRecord(valueBranch) && isString$2(valueBranch.type)) {
45
+ const { anyOf: _anyOf, ...metadata } = value;
46
+ return compactNullableSchema({
47
+ ...valueBranch,
48
+ ...metadata,
49
+ type: [valueBranch.type, "null"]
50
+ });
51
+ }
52
+ }
53
+ return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, compactNullableSchema(nested)]));
54
+ }
55
+ function omitOutputObjectRestrictions(value) {
56
+ if (Array.isArray(value)) return value.map(omitOutputObjectRestrictions);
57
+ if (!isRecord(value)) return value;
58
+ return Object.fromEntries(Object.entries(value).filter(([key, nested]) => key !== "additionalProperties" || nested !== false).map(([key, nested]) => [key, omitOutputObjectRestrictions(nested)]));
59
+ }
60
+ function convertSchema(schema, io) {
61
+ conversionCount += 1;
62
+ const converted = z.toJSONSchema(schema, {
63
+ target: "draft-2020-12",
64
+ io
65
+ });
66
+ if (!isRecord(converted)) throw new Error("Zod emitted a non-object root schema");
67
+ const compacted = compactNullableSchema(converted);
68
+ if (!isRecord(compacted)) throw new Error("Nullable schema compaction changed the root schema");
69
+ const output = io === "output" ? omitOutputObjectRestrictions(compacted) : compacted;
70
+ if (!isRecord(output)) throw new Error("Output schema compaction changed the root schema");
71
+ delete output.$schema;
72
+ return output;
73
+ }
74
+ /**
75
+ * Keep Zod validation while advertising a compact JSON Schema.
76
+ *
77
+ * Zod emits nullable values as `anyOf`; draft 2020-12 also permits a type
78
+ * array. The MCP wire envelope does not need a repeated `$schema` marker, and
79
+ * structured output is validated by the original Zod schema before sending.
80
+ */
81
+ function compactJsonSchema(schema) {
82
+ let inputSchema;
83
+ let outputSchema;
84
+ Object.defineProperty(schema["~standard"], "jsonSchema", {
85
+ configurable: true,
86
+ value: {
87
+ input: () => {
88
+ inputSchema ??= convertSchema(schema, "input");
89
+ return inputSchema;
90
+ },
91
+ output: () => {
92
+ outputSchema ??= convertSchema(schema, "output");
93
+ return outputSchema;
94
+ }
95
+ },
96
+ writable: true
97
+ });
98
+ return schema;
99
+ }
100
+ //#endregion
7
101
  //#region ../hevy-client/src/generated/client/schemas/userInfoSchema.ts
8
102
  /**
9
103
  * Generated by Kubb (https://kubb.dev/).
@@ -292,13 +386,13 @@ z$1.any();
292
386
  z$1.lazy(() => getV1WorkoutsWorkoutid200Schema);
293
387
  //#endregion
294
388
  //#region ../hevy-client/src/response-error.ts
295
- const objectSchema$2 = z.object({}).passthrough();
296
- const stringSchema$2 = z.string();
297
- function isObject$2(value) {
298
- return objectSchema$2.safeParse(value).success;
389
+ const objectSchema$1 = z.object({}).passthrough();
390
+ const stringSchema$1 = z.string();
391
+ function isObject$1(value) {
392
+ return objectSchema$1.safeParse(value).success;
299
393
  }
300
- function isString$2(value) {
301
- return stringSchema$2.safeParse(value).success;
394
+ function isString$1(value) {
395
+ return stringSchema$1.safeParse(value).success;
302
396
  }
303
397
  const MAX_RESPONSE_ERROR_INPUT_LENGTH = 4096;
304
398
  const MAX_RESPONSE_ERROR_LENGTH = 256;
@@ -316,7 +410,7 @@ const HTTP_URL = /\bhttps?:\/\/[^\s<>"']+/gi;
316
410
  const EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
317
411
  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;
318
412
  function isResponseErrorBody(value) {
319
- return isObject$2(value);
413
+ return isObject$1(value);
320
414
  }
321
415
  function ownValue(value, key) {
322
416
  const descriptor = Object.getOwnPropertyDescriptor(value, key);
@@ -326,12 +420,12 @@ function responseErrorText(value) {
326
420
  if (!isResponseErrorBody(value)) return void 0;
327
421
  for (const key of RESPONSE_ERROR_KEYS) {
328
422
  const candidate = ownValue(value, key);
329
- if (isString$2(candidate)) return candidate;
423
+ if (isString$1(candidate)) return candidate;
330
424
  if (!isResponseErrorBody(candidate)) continue;
331
425
  const nestedMessage = ownValue(candidate, "message");
332
- if (isString$2(nestedMessage)) return nestedMessage;
426
+ if (isString$1(nestedMessage)) return nestedMessage;
333
427
  const nestedDetail = ownValue(candidate, "detail");
334
- if (isString$2(nestedDetail)) return nestedDetail;
428
+ if (isString$1(nestedDetail)) return nestedDetail;
335
429
  }
336
430
  }
337
431
  function removeControlCharacters(value) {
@@ -1280,13 +1374,13 @@ function isAbortLike(error) {
1280
1374
  }
1281
1375
  //#endregion
1282
1376
  //#region ../hevy-client/src/hevy-client-kubb.ts
1283
- const objectSchema$1 = z.object({}).passthrough();
1284
- const numberSchema$1 = z.number();
1285
- const stringSchema$1 = z.string();
1286
- const isObject$1 = (value) => objectSchema$1.safeParse(value).success;
1287
- const isNumber = (value) => numberSchema$1.safeParse(value).success;
1288
- const isString$1 = (value) => stringSchema$1.safeParse(value).success;
1289
- const DEFAULT_API_TIMEOUT_MS = 3e4;
1377
+ const objectSchema = z.object({}).passthrough();
1378
+ const numberSchema = z.number();
1379
+ const stringSchema = z.string();
1380
+ const isObject = (value) => objectSchema.safeParse(value).success;
1381
+ const isNumber = (value) => numberSchema.safeParse(value).success;
1382
+ const isString = (value) => stringSchema.safeParse(value).success;
1383
+ const DEFAULT_API_TIMEOUT_MS = 6e4;
1290
1384
  const RETRY_BACKOFF_MAX_MS = 5e3;
1291
1385
  const SAFE_OBSERVATION_CODES = /* @__PURE__ */ new Set([
1292
1386
  "EAI_AGAIN",
@@ -1373,7 +1467,7 @@ function getRequestContext(config) {
1373
1467
  return {
1374
1468
  method: (config.method ?? "GET").toUpperCase(),
1375
1469
  endpoint: canonicalEndpointIdentity(config.url ?? ""),
1376
- page: config.params !== null && isObject$1(config.params) && "page" in config.params && isNumber(config.params.page) ? config.params.page : void 0
1470
+ page: config.params !== null && isObject(config.params) && "page" in config.params && isNumber(config.params.page) ? config.params.page : void 0
1377
1471
  };
1378
1472
  }
1379
1473
  function emitClientLog(logger, message) {
@@ -1447,7 +1541,7 @@ function buildUrl(baseUrl, config) {
1447
1541
  code: "HEVY_INVALID_ENDPOINT"
1448
1542
  });
1449
1543
  const url = new URL(config.url, baseUrl);
1450
- if (config.params && isObject$1(config.params)) {
1544
+ if (config.params && isObject(config.params)) {
1451
1545
  for (const [key, value] of Object.entries(config.params)) if (value !== void 0) url.searchParams.append(key, value === null ? "null" : String(value));
1452
1546
  }
1453
1547
  return url;
@@ -1691,7 +1785,7 @@ function createFailureObservation(options, failure, error, commitState, safeToRe
1691
1785
  safeToRetry: safeToRetry && !retryExhausted,
1692
1786
  error: {
1693
1787
  status: error.status,
1694
- code: isString$1(error.code) && SAFE_OBSERVATION_CODES.has(error.code) ? error.code : void 0,
1788
+ code: isString(error.code) && SAFE_OBSERVATION_CODES.has(error.code) ? error.code : void 0,
1695
1789
  category: error.status === void 0 ? "NetworkError" : "HevyHttpError"
1696
1790
  }
1697
1791
  };
@@ -1922,7 +2016,10 @@ function createClient(apiKey, baseUrl = "https://api.hevyapp.com", options = {})
1922
2016
  getWorkoutEvents: (params, options) => getV1WorkoutsEvents(headers, params, requestOptions(options, client)),
1923
2017
  getRoutines: (params, options) => getV1Routines(headers, params, requestOptions(options, client)),
1924
2018
  getRoutineById: (routineId, options) => getV1RoutinesRoutineid(routineId, headers, requestOptions(options, client)),
1925
- createRoutine: (data, options) => postV1Routines(data, headers, requestOptions(options, client)),
2019
+ createRoutine: async (data, options) => {
2020
+ const response = await postV1Routines(data, headers, requestOptions(options, client));
2021
+ return Object.keys(response).length === 0 ? void 0 : response;
2022
+ },
1926
2023
  updateRoutine: (routineId, data, options) => putV1RoutinesRoutineid(routineId, data, headers, requestOptions(options, client)),
1927
2024
  getExerciseTemplates: (params, options) => getV1ExerciseTemplates(headers, params, requestOptions(options, client)),
1928
2025
  getExerciseTemplate: (templateId, options) => getV1ExerciseTemplatesExercisetemplateid(templateId, headers, requestOptions(options, client)),
@@ -1944,28 +2041,6 @@ function createHevyClient({ apiKey, baseUrl, ...options }) {
1944
2041
  return createClient(apiKey, baseUrl, options);
1945
2042
  }
1946
2043
  //#endregion
1947
- //#region ../core/src/utils/type-predicates.ts
1948
- const stringSchema = z.string();
1949
- const numberSchema = z.number();
1950
- const booleanSchema = z.boolean();
1951
- const objectSchema = z.object({}).passthrough();
1952
- const functionSchema = z.function();
1953
- function isString(value) {
1954
- return stringSchema.safeParse(value).success;
1955
- }
1956
- function isFiniteNumber(value) {
1957
- return numberSchema.safeParse(value).success && Number.isFinite(value);
1958
- }
1959
- function isBoolean(value) {
1960
- return booleanSchema.safeParse(value).success;
1961
- }
1962
- function isObject(value) {
1963
- return value !== null && objectSchema.safeParse(value).success;
1964
- }
1965
- function isFunction(value) {
1966
- return functionSchema.safeParse(value).success;
1967
- }
1968
- //#endregion
1969
2044
  //#region ../core/src/utils/error-policy.ts
1970
2045
  const ABORT_TIMEOUT_METADATA = {
1971
2046
  AbortError: {
@@ -2025,7 +2100,7 @@ const SAFE_SOURCE_SUFFIXES = [
2025
2100
  const PROJECT_PATH_MARKER = "/hevy-mcp/";
2026
2101
  const MAX_STACK_POSITION = 1e6;
2027
2102
  function normalizeHeaderValue(value) {
2028
- if (isString(value)) {
2103
+ if (isString$2(value)) {
2029
2104
  const trimmed = value.trim();
2030
2105
  return trimmed.length > 0 ? trimmed : void 0;
2031
2106
  }
@@ -2034,7 +2109,7 @@ function normalizeHeaderValue(value) {
2034
2109
  }
2035
2110
  function getHeaderValue(headers, key) {
2036
2111
  try {
2037
- if (!isObject(headers)) return void 0;
2112
+ if (!isObject$2(headers)) return void 0;
2038
2113
  if ("get" in headers && isFunction(headers.get)) return normalizeHeaderValue(headers.get(key));
2039
2114
  const headerRecord = headers;
2040
2115
  return normalizeHeaderValue(headerRecord[key] ?? headerRecord[key.toLowerCase()] ?? headerRecord[key.toUpperCase()]);
@@ -2054,7 +2129,7 @@ function extractErrorStatus(error) {
2054
2129
  /** Return whether the client exhausted its bounded transient retry policy. */
2055
2130
  function isRetryExhausted(error) {
2056
2131
  try {
2057
- return !!error && isObject(error) && error.hevyRetryExhausted === true;
2132
+ return !!error && isObject$2(error) && error.hevyRetryExhausted === true;
2058
2133
  } catch {
2059
2134
  return false;
2060
2135
  }
@@ -2075,11 +2150,12 @@ function getRetryAfterSeconds(error, now = Date.now()) {
2075
2150
  }
2076
2151
  }
2077
2152
  /** Map bounded Hevy HTTP statuses to stable user-facing messages. */
2078
- function getStatusErrorMessage(status) {
2153
+ function getStatusErrorMessage(status, method, endpoint) {
2154
+ if (status === 404 && method?.toUpperCase() === "PUT" && endpoint === "/v1/routines/:routineId") return "The requested routine was not found in Hevy. It may have been deleted or the routine ID is incorrect.";
2155
+ if (status === 400 || status === 422) return "The request failed Hevy validation. Check the field values and try again.";
2079
2156
  if (status === 401 || status === 403) return "The Hevy API key is invalid or has expired. Check HEVY_API_KEY.";
2080
2157
  if (status === 404) return "The requested resource was not found in Hevy.";
2081
- if (status === 409) return "A conflict occurred (e.g., a body measurement already exists for this date). Use the update tool instead.";
2082
- if (status === 422) return "The request failed Hevy validation. Check the field values and try again.";
2158
+ if (status === 409) return "A conflict occurred because the resource already exists or conflicts with the current server state. Check whether it already exists and use the update tool when appropriate.";
2083
2159
  if (status === 429) return "Rate limited by Hevy. Please wait and retry.";
2084
2160
  if (status !== void 0 && status >= 500 && status <= 599) return "Hevy API experienced an error. Please retry later.";
2085
2161
  return null;
@@ -2096,7 +2172,7 @@ function getRateLimitMessage(error) {
2096
2172
  function getRetryExhaustedMessage(error) {
2097
2173
  let retryCount;
2098
2174
  try {
2099
- retryCount = isObject(error) ? error.hevyRetryCount : void 0;
2175
+ retryCount = isObject$2(error) ? error.hevyRetryCount : void 0;
2100
2176
  } catch {
2101
2177
  retryCount = void 0;
2102
2178
  }
@@ -2142,9 +2218,9 @@ function classifyError(error) {
2142
2218
  function getSafeCode(error) {
2143
2219
  const abortTimeout = getAbortTimeoutErrorMetadata(error);
2144
2220
  if (abortTimeout) return abortTimeout.code;
2145
- if (!isObject(error) || !("code" in error)) return;
2221
+ if (!isObject$2(error) || !("code" in error)) return;
2146
2222
  const code = error.code;
2147
- return isString(code) && SAFE_ERROR_CODES.has(code) ? code : void 0;
2223
+ return isString$2(code) && SAFE_ERROR_CODES.has(code) ? code : void 0;
2148
2224
  }
2149
2225
  function getSafeMethod(error) {
2150
2226
  if (!isHevyHttpError(error)) return void 0;
@@ -2174,7 +2250,7 @@ function getExecutionFields(error) {
2174
2250
  return fields;
2175
2251
  }
2176
2252
  function parseSafeStackFrames(error) {
2177
- if (!(error instanceof Error) || !isString(error.stack)) return;
2253
+ if (!(error instanceof Error) || !isString$2(error.stack)) return;
2178
2254
  const frames = [];
2179
2255
  for (const frameLine of error.stack.split(/\r?\n/).slice(1)) {
2180
2256
  const match = /^\s{4}at (?:[^()\r\n]+ \()?([^()\s\r\n]+):(\d+):(\d+)\)?$/.exec(frameLine);
@@ -2219,7 +2295,7 @@ function createSafeErrorDiagnostic(error) {
2219
2295
  /** Resolve all bounded policy outputs used by MCP, telemetry, and reporting adapters. */
2220
2296
  function resolveErrorPolicy(error, defaultMessage, notInitializedMessage) {
2221
2297
  const diagnostic = createSafeErrorDiagnostic(error);
2222
- let message = getStatusErrorMessage(diagnostic.status) ?? (diagnostic.status !== void 0 ? `Hevy API request failed (HTTP ${diagnostic.status}).` : defaultMessage);
2298
+ let message = getStatusErrorMessage(diagnostic.status, diagnostic.method, diagnostic.endpoint) ?? (diagnostic.status !== void 0 ? `Hevy API request failed (HTTP ${diagnostic.status}).` : defaultMessage);
2223
2299
  let isNotInitialized = false;
2224
2300
  try {
2225
2301
  isNotInitialized = Boolean(notInitializedMessage) && error instanceof Error && error.message === notInitializedMessage;
@@ -2352,6 +2428,13 @@ const formattedRoutineSchema = z.object({
2352
2428
  updated_at: z.string().optional(),
2353
2429
  exercises: z.array(formattedRoutineExerciseSchema).optional()
2354
2430
  });
2431
+ const createRoutineOutputSchema = {
2432
+ created: z.literal(true),
2433
+ commit_state: z.literal("confirmed"),
2434
+ routine: formattedRoutineSchema.nullable(),
2435
+ routine_id: z.string().nullable(),
2436
+ uses_rep_ranges: z.boolean()
2437
+ };
2355
2438
  const formattedRoutineFolderSchema = z.object({
2356
2439
  id: z.number().optional(),
2357
2440
  title: z.string().optional(),
@@ -2970,10 +3053,19 @@ const compactRoutinesResponse = defineStructuredResponseContract({
2970
3053
  const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json: projectWorkout(workout) } : { text: "Failed to create workout: Server returned no data" }, (workout) => workoutResultTelemetry(workout));
2971
3054
  const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json: projectWorkout(data.workout) } : { text: `Failed to update workout with ID ${data.workout_id}` }, (data) => workoutResultTelemetry(data.workout));
2972
3055
  const repRangeDisplayWarningText = "Note: Hevy's public API stores rep ranges (rep_range), but the Hevy apps may not display them because they rely on an internal-only exercise field (input_modifier). See https://github.com/chrisdoc/hevy-mcp/issues/261 for details/workarounds.";
2973
- const createRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
2974
- json: projectRoutine(data.routine),
2975
- additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
2976
- } : { text: "Failed to create routine: Server returned no data" }, (data) => routineResultTelemetry(data.routine));
3056
+ const createRoutineResponse = defineStructuredResponseContract({
3057
+ outputSchema: createRoutineOutputSchema,
3058
+ normalize: (data) => ({
3059
+ created: true,
3060
+ commit_state: "confirmed",
3061
+ routine: data.routine ? projectRoutine(data.routine) : null,
3062
+ routine_id: data.routine?.id ?? null,
3063
+ uses_rep_ranges: data.usesRepRanges
3064
+ }),
3065
+ legacyJson: (output) => output,
3066
+ additionalText: (_data, output) => output.uses_rep_ranges ? [repRangeDisplayWarningText] : [],
3067
+ telemetry: (data) => routineResultTelemetry(data.routine)
3068
+ });
2977
3069
  const updateRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
2978
3070
  json: projectRoutine(data.routine),
2979
3071
  additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
@@ -2986,76 +3078,6 @@ const createRoutineFolderResponse = defineJsonResponseContract((folder) => folde
2986
3078
  const createBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} created successfully.` }), () => ({ itemCountBucket: "1" }));
2987
3079
  const updateBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} updated successfully.` }), () => ({ itemCountBucket: "1" }));
2988
3080
  //#endregion
2989
- //#region ../core/src/utils/compact-json-schema.ts
2990
- function isRecord(value) {
2991
- return isObject(value) && !Array.isArray(value);
2992
- }
2993
- function isNullSchema(value) {
2994
- return isRecord(value) && value.type === "null" && Object.keys(value).length === 1;
2995
- }
2996
- function compactNullableSchema(value) {
2997
- if (Array.isArray(value)) return value.map(compactNullableSchema);
2998
- if (!isRecord(value)) return value;
2999
- const anyOf = value.anyOf;
3000
- if (Array.isArray(anyOf) && anyOf.length === 2) {
3001
- const nullableBranch = anyOf.find(isNullSchema);
3002
- const valueBranch = anyOf.find((branch) => !isNullSchema(branch));
3003
- if (nullableBranch !== void 0 && isRecord(valueBranch) && isString(valueBranch.type)) {
3004
- const { anyOf: _anyOf, ...metadata } = value;
3005
- return compactNullableSchema({
3006
- ...valueBranch,
3007
- ...metadata,
3008
- type: [valueBranch.type, "null"]
3009
- });
3010
- }
3011
- }
3012
- return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, compactNullableSchema(nested)]));
3013
- }
3014
- function omitOutputObjectRestrictions(value) {
3015
- if (Array.isArray(value)) return value.map(omitOutputObjectRestrictions);
3016
- if (!isRecord(value)) return value;
3017
- return Object.fromEntries(Object.entries(value).filter(([key, nested]) => key !== "additionalProperties" || nested !== false).map(([key, nested]) => [key, omitOutputObjectRestrictions(nested)]));
3018
- }
3019
- function convertSchema(schema, io) {
3020
- const converted = z.toJSONSchema(schema, {
3021
- target: "draft-2020-12",
3022
- io
3023
- });
3024
- if (!isRecord(converted)) throw new Error("Zod emitted a non-object root schema");
3025
- const compacted = compactNullableSchema(converted);
3026
- if (!isRecord(compacted)) throw new Error("Nullable schema compaction changed the root schema");
3027
- const output = io === "output" ? omitOutputObjectRestrictions(compacted) : compacted;
3028
- if (!isRecord(output)) throw new Error("Output schema compaction changed the root schema");
3029
- delete output.$schema;
3030
- return output;
3031
- }
3032
- /**
3033
- * Keep Zod validation while advertising a compact JSON Schema.
3034
- *
3035
- * Zod emits nullable values as `anyOf`; draft 2020-12 also permits a type
3036
- * array. The MCP wire envelope does not need a repeated `$schema` marker, and
3037
- * structured output is validated by the original Zod schema before sending.
3038
- */
3039
- function compactJsonSchema(schema) {
3040
- let inputSchema;
3041
- let outputSchema;
3042
- Object.defineProperty(schema["~standard"], "jsonSchema", {
3043
- configurable: true,
3044
- value: {
3045
- input: () => {
3046
- inputSchema ??= convertSchema(schema, "input");
3047
- return inputSchema;
3048
- },
3049
- output: () => {
3050
- outputSchema ??= convertSchema(schema, "output");
3051
- return outputSchema;
3052
- }
3053
- },
3054
- writable: true
3055
- });
3056
- return schema;
3057
- }
3058
- //#endregion
3059
3081
  //#region ../core/src/utils/tool-helpers.ts
3060
3082
  /**
3061
3083
  * Type-safe utilities for MCP tool handlers
@@ -3100,21 +3122,43 @@ function createTypedToolHandler(schema, handler) {
3100
3122
  }
3101
3123
  //#endregion
3102
3124
  //#region ../core/src/tools/define-tool.ts
3125
+ /**
3126
+ * One-time-per-isolate registration metadata for each tool definition.
3127
+ *
3128
+ * Building a tool's compact JSON Schema (and the SDK's eager wire conversion
3129
+ * of the `compactJsonSchema`-wrapped schema on registration) is the dominant
3130
+ * CPU cost when constructing an MCP server. Tool definitions are module-level
3131
+ * constants, so this metadata can be computed once and shared by every server
3132
+ * a single isolate builds. The SDK's `registerTool` reads the schema strictly
3133
+ * through the Standard Schema interface and never mutates it, so sharing the
3134
+ * memoized config across concurrent servers is safe.
3135
+ *
3136
+ * Keep the memo lazy: CLI and test processes that never build a Hevy server
3137
+ * pay nothing. Isolate-based edge runtimes that want the one-time conversion
3138
+ * moved into module-scope (outside request CPU) call `preloadHevyToolSchemas`.
3139
+ */
3140
+ const registeredToolConfigCache = /* @__PURE__ */ new WeakMap();
3141
+ function getRegisteredToolConfig(definition) {
3142
+ const cached = registeredToolConfigCache.get(definition);
3143
+ if (cached) return cached;
3144
+ const config = {
3145
+ description: definition.description,
3146
+ inputSchema: compactJsonSchema(z.strictObject(definition.inputSchema)),
3147
+ annotations: definition.annotations
3148
+ };
3149
+ if (definition.outputSchema) config.outputSchema = compactJsonSchema(z.object(definition.outputSchema));
3150
+ registeredToolConfigCache.set(definition, config);
3151
+ return config;
3152
+ }
3103
3153
  function registerToolDefinition(server, runtime, definition) {
3104
3154
  const directHandler = createTypedToolHandler(definition.inputSchema, async (args, requestContext) => respond(definition.responseContract, await definition.execute(requestContext ? runtime.forExecution(requestContext) : runtime, args)));
3105
- const callback = runtime.createHandler(directHandler, definition.name, {
3155
+ const handler = runtime.createHandler(directHandler, definition.name, {
3106
3156
  feature: definition.feature,
3107
3157
  kind: definition.kind,
3108
3158
  operation: definition.operation
3109
3159
  });
3110
- const inputSchema = compactJsonSchema(z.strictObject(definition.inputSchema));
3111
- const config = {
3112
- description: definition.description,
3113
- inputSchema,
3114
- annotations: definition.annotations
3115
- };
3116
- if (definition.kind === "read") config.outputSchema = compactJsonSchema(z.object(definition.outputSchema));
3117
- server.registerTool(definition.name, config, (args, context) => callback(z.strictObject(definition.inputSchema).parse(args), context ? {
3160
+ const config = getRegisteredToolConfig(definition);
3161
+ server.registerTool(definition.name, config, (args, context) => handler(z.strictObject(definition.inputSchema).parse(args), context ? {
3118
3162
  signal: context.mcpReq.signal,
3119
3163
  requestId: String(context.mcpReq.id)
3120
3164
  } : void 0));
@@ -3165,7 +3209,7 @@ function updateAnnotations(title) {
3165
3209
  * @returns The parsed array if val is a valid JSON string, otherwise returns val unchanged
3166
3210
  */
3167
3211
  function parseJsonArray(val) {
3168
- if (isString(val)) try {
3212
+ if (isString$2(val)) try {
3169
3213
  return JSON.parse(val);
3170
3214
  } catch {
3171
3215
  return val;
@@ -3176,7 +3220,7 @@ function parseJsonArray(val) {
3176
3220
  //#region ../core/src/utils/schemas.ts
3177
3221
  function coerceNullishNumberInput(value) {
3178
3222
  if (value === null || value === void 0) return value;
3179
- if (!isString(value)) return value;
3223
+ if (!isString$2(value)) return value;
3180
3224
  const trimmed = value.trim();
3181
3225
  if (trimmed === "") return;
3182
3226
  const lowered = trimmed.toLowerCase();
@@ -3488,7 +3532,7 @@ const FETCHED_ISO_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(
3488
3532
  * supplied values remain strict.
3489
3533
  */
3490
3534
  function normalizeFetchedWorkoutTimestamp(value) {
3491
- if (!isString(value)) return value;
3535
+ if (!isString$2(value)) return value;
3492
3536
  const match = FETCHED_ISO_TIMESTAMP.exec(value);
3493
3537
  if (!match) return value;
3494
3538
  const [, year, month, day, hour, minute, second, offset] = match;
@@ -3772,6 +3816,7 @@ const routineToolDefinitions = [
3772
3816
  description: "Writes a reusable routine; use create-workout for completed sessions. Retries can create duplicates.",
3773
3817
  inputSchema: createRoutineInputFields,
3774
3818
  kind: "write",
3819
+ outputSchema: createRoutineOutputSchema,
3775
3820
  annotations: createAnnotations("Create Routine"),
3776
3821
  responseContract: createRoutineResponse,
3777
3822
  execute: async (runtime, args) => {
@@ -4403,7 +4448,7 @@ function withPromptObservation(name, observer, handler) {
4403
4448
  let scope;
4404
4449
  try {
4405
4450
  const argumentKeys = Object.keys(args).filter((key) => key === "routine_id");
4406
- const routineId = "routine_id" in args && isString(args.routine_id) ? args.routine_id : void 0;
4451
+ const routineId = "routine_id" in args && isString$2(args.routine_id) ? args.routine_id : void 0;
4407
4452
  scope = memoizeObservationScope(observer?.start({
4408
4453
  name,
4409
4454
  kind: "prompt",
@@ -4661,13 +4706,14 @@ function readBuildGlobal(value) {
4661
4706
  }
4662
4707
  }
4663
4708
  const buildName = readBuildGlobal(() => "hevy-mcp");
4664
- const buildVersion = readBuildGlobal(() => "6.1.3");
4709
+ const buildVersion = readBuildGlobal(() => "6.1.4");
4665
4710
  const SERVER_NAME = readBuildString(buildName, "hevy-mcp");
4666
4711
  const SERVER_VERSION = readBuildString(buildVersion, "dev");
4667
4712
  const SERVER_INSTRUCTIONS = [
4668
4713
  "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.",
4669
4714
  "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.",
4670
4715
  "Workflow: search exercise templates first, then use the returned template IDs when creating workouts or routines. To create a completed workout from a routine, fetch the routine as a plan, then obtain the actual completed sets and end time from the user; never invent completion data.",
4716
+ "Tool protocol: always call the exact tool name and pass inputs in the tools/call arguments object. Never append parameters to a tool name (for example, call get-workout with {\"workout_id\": \"<id>\"}, not get-workout-workoutId=<id>). The canonical discovery tools are get-workout, get-exercise-template, and get-routine-folder; get-workouts, search-exercise-templates, and the hevy://exercise-templates and hevy://routine-folders resources discover records. get-user-info is not an MCP tool; use the hevy://user resource.",
4671
4717
  "Resources: use hevy://user, hevy://workout-count, hevy://exercise-templates, and hevy://routine-folders for complete cached datasets that do not require parameters. Use search-exercise-templates when you need title or muscle-group filtering.",
4672
4718
  "Pagination: start at page 1 and fetch only the pages needed. List tools that remain available use bounded page sizes.",
4673
4719
  "Rate limits and retries: minimize repeated calls. If Hevy returns HTTP 429, follow its retry guidance. Transient read requests retry automatically, but write requests do not; confirm uncertain write outcomes before trying again."
@@ -5244,4 +5290,4 @@ function createHevyMcpServer(options) {
5244
5290
  //#endregion
5245
5291
  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 };
5246
5292
 
5247
- //# sourceMappingURL=src-Cb_gblD1.mjs.map
5293
+ //# sourceMappingURL=src-DCTiE-_b.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-mcp",
3
- "version": "6.1.3",
3
+ "version": "6.1.4",
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": {
@@ -50,7 +50,7 @@
50
50
  "devDependencies": {
51
51
  "@hevy-mcp/core": "*",
52
52
  "@hevy-mcp/hevy-client": "*",
53
- "@types/node": "^26.1.2",
53
+ "@types/node": "^26.2.0",
54
54
  "cross-env": "^10.1.0",
55
55
  "tsdown": "^0.22.14",
56
56
  "typescript": "^7.0.2"
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.3",
10
+ "version": "6.1.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "6.1.3",
15
+ "version": "6.1.4",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },