hevy-mcp 4.1.0 → 4.1.2

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
@@ -414,17 +414,18 @@ self-hosted Streamable HTTP.
414
414
 
415
415
  ## Advanced configuration
416
416
 
417
- | Setting | Default | Scope | Notes |
418
- | ---------------------------- | ------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
419
- | `HEVY_API_KEY` | None; required | Local stdio or HTTP | Hevy API key from the Hevy app. Never pass it in a URL. |
420
- | `HEVY_MCP_API_TIMEOUT` | `30000` ms | Local stdio | Positive Hevy API timeout in milliseconds. Invalid values fall back to 30 seconds. |
421
- | `HEVY_MCP_DEBUG` | Disabled | Local Node | Set to exactly `1` for privacy-bounded diagnostics on stderr. Stdout remains reserved for MCP JSON-RPC. |
422
- | `HEVY_MCP_HTTP_BEARER_TOKEN` | None | Non-loopback HTTP | Required when `--host` is not loopback; use a separate token, never the Hevy API key. |
423
- | `XDG_CACHE_HOME` | `~/.cache` | Local stdio | Changes the root for the npm update-check cache at `hevy-mcp/update-check.json`. |
424
- | `SENTRY_DSN` | Packaged project DSN | Optional local Node telemetry | Overrides the Sentry destination. An empty value disables Sentry export. The Worker does not import Node telemetry. |
425
- | `SENTRY_RELEASE` | `hevy-mcp@<installed-version>` | Optional local Node telemetry | Overrides the release label attached to local Sentry events and traces. |
426
- | `-h`, `--help` | N/A | Local stdio CLI | Print supported options and exit. |
427
- | `-v`, `--version` | N/A | Local stdio CLI | Print the installed version and exit. |
417
+ | Setting | Default | Scope | Notes |
418
+ | ---------------------------- | ------------------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
419
+ | `HEVY_API_KEY` | None; required | Local stdio or HTTP | Hevy API key from the Hevy app. Never pass it in a URL. |
420
+ | `HEVY_MCP_API_TIMEOUT` | `30000` ms | Local stdio | Positive Hevy API timeout in milliseconds. Invalid values fall back to 30 seconds. |
421
+ | `HEVY_MCP_DEBUG` | Disabled | Local Node | Set to exactly `1` for privacy-bounded diagnostics on stderr. Stdout remains reserved for MCP JSON-RPC. |
422
+ | `HEVY_MCP_HTTP_BEARER_TOKEN` | None | Non-loopback HTTP | Required when `--host` is not loopback; use a separate token, never the Hevy API key. |
423
+ | `HEVY_MCP_TELEMETRY` | Enabled | Local Node | Set to exactly `0` before startup/import to disable Sentry errors/traces and OTLP traces/metrics. Takes precedence over `SENTRY_DSN` and packaged/runtime collector credentials. |
424
+ | `XDG_CACHE_HOME` | `~/.cache` | Local stdio | Changes the root for the npm update-check cache at `hevy-mcp/update-check.json`. |
425
+ | `SENTRY_DSN` | Packaged project DSN | Optional local Node telemetry | Sentry-only override for the destination. An empty value disables Sentry export. The Worker does not import Node telemetry. |
426
+ | `SENTRY_RELEASE` | `hevy-mcp@<installed-version>` | Optional local Node telemetry | Overrides the release label attached to local Sentry events and traces. |
427
+ | `-h`, `--help` | N/A | Local stdio CLI | Print supported options and exit. |
428
+ | `-v`, `--version` | N/A | Local stdio CLI | Print the installed version and exit. |
428
429
 
429
430
  The local executable uses stdio by default. To opt into Streamable HTTP, run:
430
431
 
@@ -459,6 +460,42 @@ server-scoped in-memory catalog cache:
459
460
  - Paginated `get-exercise-templates` calls always fetch their requested page.
460
461
  - Each hosted Worker request gets a fresh cache, preventing cross-key sharing.
461
462
 
463
+ ### Local Node telemetry and privacy
464
+
465
+ The local Node package enables project telemetry by default. It is local Node
466
+ behavior only; the Cloudflare Worker does not import Node telemetry. Set
467
+ `HEVY_MCP_TELEMETRY=0` before startup or import to disable all project
468
+ telemetry. Only the literal value `0` opts out: an unset value, an empty value,
469
+ `1`, `false`, and every other value remain enabled. The master setting takes
470
+ precedence over `SENTRY_DSN` and packaged or runtime `OTEL_COLLECTOR_TOKEN`
471
+ credentials, so the disabled path creates no telemetry exporters or periodic
472
+ metric readers and makes no telemetry network requests. `SENTRY_DSN` remains a
473
+ Sentry-only setting; when telemetry is enabled, an empty value disables only
474
+ Sentry export.
475
+
476
+ When enabled, errors and traces are sent to the packaged Sentry project at
477
+ <https://o4508975499575296.ingest.de.sentry.io/4509049671647312>. Traces and
478
+ metrics are sent to the collector at
479
+ <https://otel.chrisdoc.dev/v1/traces> and
480
+ <https://otel.chrisdoc.dev/v1/metrics>, which forward to Honeycomb. Metrics
481
+ export every 10 seconds.
482
+
483
+ The API key is never exported. Enabled telemetry derives a deterministic
484
+ ten-character HMAC-SHA-256 pseudonym from it solely for cross-span
485
+ correlation. The pseudonym is attached to spans only, never used as a metric
486
+ dimension, and is not intended for per-user behavior histories.
487
+
488
+ The privacy allowlist contains service/version/transport; fixed tool feature,
489
+ read/write kind, operation, and short-lived tool name; bounded
490
+ outcome/error/count/retry/duration/session/cache/workflow values; normalized API
491
+ method/endpoint/status; shape-only key names, presence, count, and boolean
492
+ fields; sanitized client/protocol tokens; and the span-only pseudonym. It
493
+ explicitly prohibits raw prompts, tool argument values, tool result content,
494
+ request bodies, API keys, raw identifiers/queries/exact dates,
495
+ workout/routine/folder/template/body-measurement content, names/titles/
496
+ descriptions/notes, measurement values, arbitrary client metadata, and
497
+ unnormalized endpoint paths.
498
+
462
499
  ## Security and mutations
463
500
 
464
501
  - Keep `HEVY_API_KEY` out of source control, URLs, logs, and screenshots.
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@4.1.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@4.1.2" };
8
8
  var n = new e.Error().stack;
9
9
  n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "625c4c9c-e20d-40b3-96b1-4e799ef130f3", e._sentryDebugIdIdentifier = "sentry-dbid-625c4c9c-e20d-40b3-96b1-4e799ef130f3");
10
10
  } catch (e) {}
11
11
  })();
12
- import { a as createSafeErrorDiagnostic, i as MissingHevyApiKeyError, n as runServer, o as flushTelemetry } from "./src-wSWxe4LJ.mjs";
12
+ import { a as createSafeErrorDiagnostic, i as MissingHevyApiKeyError, n as runServer, o as flushTelemetry } from "./src-Wa4VP4Na.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
@@ -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@4.1.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@4.1.2" };
8
8
  var n = new e.Error().stack;
9
9
  n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "e9496b31-626b-4ad1-9416-47ba4ffd2995", e._sentryDebugIdIdentifier = "sentry-dbid-e9496b31-626b-4ad1-9416-47ba4ffd2995");
10
10
  } catch (e) {}
11
11
  })();
12
- import { n as runServer, r as runStdioServer, t as createNodeMcpServer } from "./src-wSWxe4LJ.mjs";
12
+ import { n as runServer, r as runStdioServer, t as createNodeMcpServer } from "./src-Wa4VP4Na.mjs";
13
13
  export { createNodeMcpServer, runServer, runStdioServer };
@@ -4,11 +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@4.1.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@4.1.2" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "4b988c15-eb76-40ac-8399-033bebec17f8", e._sentryDebugIdIdentifier = "sentry-dbid-4b988c15-eb76-40ac-8399-033bebec17f8");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "b064b429-35aa-4617-a2a9-779c7c2ed596", e._sentryDebugIdIdentifier = "sentry-dbid-b064b429-35aa-4617-a2a9-779c7c2ed596");
10
10
  } catch (e) {}
11
11
  })();
12
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
12
13
  import * as Sentry from "@sentry/node";
13
14
  import { SentryPropagator, SentrySampler, SentrySpanProcessor } from "@sentry/opentelemetry";
14
15
  import { SpanStatusCode, metrics, trace } from "@opentelemetry/api";
@@ -18,7 +19,6 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
18
19
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
19
20
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
20
21
  import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
21
- import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
22
22
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23
23
  import { z } from "zod";
24
24
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -81,26 +81,15 @@ function sanitizeSentryMcpSpan(span) {
81
81
  * OTel Collector → Honeycomb: performance traces, metrics
82
82
  */
83
83
  const name$1 = "hevy-mcp";
84
- const version$1 = "4.1.0";
84
+ const version$1 = "4.1.2";
85
+ const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
85
86
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
86
87
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
87
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@4.1.0`;
88
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@4.1.2`;
88
89
  const resource = resourceFromAttributes({
89
90
  "service.name": name$1,
90
91
  "service.version": version$1
91
92
  });
92
- const rawDsn = process.env.SENTRY_DSN ?? "https://ce696d8333b507acbf5203eb877bce0f@o4508975499575296.ingest.de.sentry.io/4509049671647312";
93
- const isValidDsn = typeof rawDsn === "string" && rawDsn.length > 0 && !rawDsn.startsWith("*");
94
- const sentryClient = Sentry.init({
95
- dsn: isValidDsn ? rawDsn : void 0,
96
- beforeSendSpan: sanitizeSentryMcpSpan,
97
- release: sentryRelease,
98
- tracesSampleRate: 1,
99
- sendDefaultPii: false,
100
- skipOpenTelemetrySetup: true,
101
- registerEsmLoaderHooks: false,
102
- ignoreErrors: ["EPIPE", "broken pipe"]
103
- });
104
93
  let currentUserHash;
105
94
  var UserHashSpanProcessor = class {
106
95
  onStart(span) {
@@ -110,37 +99,53 @@ var UserHashSpanProcessor = class {
110
99
  async forceFlush() {}
111
100
  async shutdown() {}
112
101
  };
113
- const spanProcessors = [new UserHashSpanProcessor(), new SentrySpanProcessor()];
114
- spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({
115
- url: `${COLLECTOR_ENDPOINT}/traces`,
116
- headers: { Authorization: `Bearer ${collectorToken}` }
117
- })));
118
- const tracerProvider = new NodeTracerProvider({
119
- resource,
120
- sampler: sentryClient ? new SentrySampler(sentryClient) : void 0,
121
- spanProcessors
122
- });
123
- tracerProvider.register({
124
- propagator: new SentryPropagator(),
125
- contextManager: new Sentry.SentryContextManager()
126
- });
102
+ let tracerProvider;
127
103
  let meterProvider;
128
- meterProvider = new MeterProvider({
129
- resource,
130
- readers: [new PeriodicExportingMetricReader({
131
- exporter: new OTLPMetricExporter({
132
- url: `${COLLECTOR_ENDPOINT}/metrics`,
133
- headers: { Authorization: `Bearer ${collectorToken}` }
134
- }),
135
- exportIntervalMillis: 1e4
136
- })]
137
- });
138
- metrics.setGlobalMeterProvider(meterProvider);
139
- trace.setGlobalTracerProvider(tracerProvider);
140
- Sentry.validateOpenTelemetrySetup();
104
+ if (telemetryEnabled) {
105
+ const rawDsn = process.env.SENTRY_DSN ?? "https://ce696d8333b507acbf5203eb877bce0f@o4508975499575296.ingest.de.sentry.io/4509049671647312";
106
+ const isValidDsn = typeof rawDsn === "string" && rawDsn.length > 0 && !rawDsn.startsWith("*");
107
+ const sentryClient = Sentry.init({
108
+ dsn: isValidDsn ? rawDsn : void 0,
109
+ beforeSendSpan: sanitizeSentryMcpSpan,
110
+ release: sentryRelease,
111
+ tracesSampleRate: 1,
112
+ sendDefaultPii: false,
113
+ skipOpenTelemetrySetup: true,
114
+ registerEsmLoaderHooks: false,
115
+ ignoreErrors: ["EPIPE", "broken pipe"]
116
+ });
117
+ const spanProcessors = [new UserHashSpanProcessor(), new SentrySpanProcessor()];
118
+ spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({
119
+ url: `${COLLECTOR_ENDPOINT}/traces`,
120
+ headers: { Authorization: `Bearer ${collectorToken}` }
121
+ })));
122
+ tracerProvider = new NodeTracerProvider({
123
+ resource,
124
+ sampler: sentryClient ? new SentrySampler(sentryClient) : void 0,
125
+ spanProcessors
126
+ });
127
+ tracerProvider.register({
128
+ propagator: new SentryPropagator(),
129
+ contextManager: new Sentry.SentryContextManager()
130
+ });
131
+ meterProvider = new MeterProvider({
132
+ resource,
133
+ readers: [new PeriodicExportingMetricReader({
134
+ exporter: new OTLPMetricExporter({
135
+ url: `${COLLECTOR_ENDPOINT}/metrics`,
136
+ headers: { Authorization: `Bearer ${collectorToken}` }
137
+ }),
138
+ exportIntervalMillis: 1e4
139
+ })]
140
+ });
141
+ metrics.setGlobalMeterProvider(meterProvider);
142
+ trace.setGlobalTracerProvider(tracerProvider);
143
+ Sentry.validateOpenTelemetrySetup();
144
+ }
141
145
  async function flushTelemetry(timeoutMs = 1e3) {
146
+ if (!telemetryEnabled) return;
142
147
  const flushPromise = Promise.allSettled([
143
- tracerProvider.forceFlush(),
148
+ ...tracerProvider ? [tracerProvider.forceFlush()] : [],
144
149
  ...meterProvider ? [meterProvider.forceFlush()] : [],
145
150
  Sentry.flush(timeoutMs)
146
151
  ]);
@@ -156,8 +161,12 @@ async function flushTelemetry(timeoutMs = 1e3) {
156
161
  }
157
162
  const tracer = trace.getTracer(name$1);
158
163
  const meter = metrics.getMeter(name$1);
159
- function setCurrentUserHash(hash) {
160
- currentUserHash = hash;
164
+ const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
165
+ function setTelemetryUser(apiKey) {
166
+ if (!telemetryEnabled) return;
167
+ const userHash = createHmac("sha256", apiKey).update(SENTRY_USER_ID_CONTEXT).digest("hex").slice(0, 10);
168
+ currentUserHash = userHash;
169
+ Sentry.setUser({ id: userHash });
161
170
  }
162
171
  //#endregion
163
172
  //#region src/utils/metrics.ts
@@ -211,10 +220,15 @@ function coerceNullishNumberInput(value) {
211
220
  }
212
221
  const zNullableInt = z.preprocess(coerceNullishNumberInput, z.number().int().nullable().optional());
213
222
  const zNullableNumber = z.preprocess((value) => value === "" ? void 0 : value, z.coerce.number().nullable().optional());
214
- const zOptionalRepRange = z.preprocess((value) => value === null ? void 0 : value, z.object({
223
+ const repRangeShape = {
215
224
  start: zNullableInt,
216
225
  end: zNullableInt
217
- }).optional());
226
+ };
227
+ function optionalRepRangeSchema(schema) {
228
+ return z.preprocess((value) => value === null ? void 0 : value, schema);
229
+ }
230
+ const zOptionalRepRange = optionalRepRangeSchema(z.object(repRangeShape).optional());
231
+ optionalRepRangeSchema(z.object(repRangeShape).strict().optional());
218
232
  const setTypeEnum = z.enum([
219
233
  "warmup",
220
234
  "normal",
@@ -2586,7 +2600,7 @@ function registerHevyResources(server, runtime) {
2586
2600
  //#endregion
2587
2601
  //#region ../core/src/server-metadata.ts
2588
2602
  const SERVER_NAME = "hevy-mcp";
2589
- const SERVER_VERSION = "4.1.0";
2603
+ const SERVER_VERSION = "4.1.2";
2590
2604
  const SERVER_INSTRUCTIONS = [
2591
2605
  "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.",
2592
2606
  "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.",
@@ -2731,6 +2745,14 @@ function paginationShape({ defaultPageSize, maxPageSize, integerPage = true }) {
2731
2745
  };
2732
2746
  }
2733
2747
  const nonEmptyId = z.string().min(1);
2748
+ const exerciseTemplateInputShape = {
2749
+ title: z.string().min(1),
2750
+ exerciseType: exerciseTypeEnum,
2751
+ equipmentCategory: equipmentCategoryEnum,
2752
+ muscleGroup: muscleGroupEnum,
2753
+ otherMuscles: z.array(muscleGroupEnum).default([])
2754
+ };
2755
+ const routineFolderInputShape = { name: z.string().min(1) };
2734
2756
  const CALENDAR_DATE_MESSAGE = "Date must be in YYYY-MM-DD format";
2735
2757
  const calendarDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, CALENDAR_DATE_MESSAGE).refine((value) => {
2736
2758
  const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
@@ -2931,6 +2953,18 @@ function buildMeasurementPayload(fields) {
2931
2953
  }
2932
2954
  return payload;
2933
2955
  }
2956
+ function buildExerciseTemplateRequest(input) {
2957
+ return { exercise: {
2958
+ title: input.title,
2959
+ exercise_type: input.exerciseType,
2960
+ equipment_category: input.equipmentCategory,
2961
+ muscle_group: input.muscleGroup,
2962
+ other_muscles: input.otherMuscles
2963
+ } };
2964
+ }
2965
+ function buildRoutineFolderRequest(input) {
2966
+ return { routine_folder: { title: input.name } };
2967
+ }
2934
2968
  //#endregion
2935
2969
  //#region ../core/src/utils/hevy-error-policy.ts
2936
2970
  const READ_RESOURCE_ENDPOINTS = /* @__PURE__ */ new Set([
@@ -3110,7 +3144,7 @@ const getRoutineFoldersSchema = paginationShape({
3110
3144
  maxPageSize: 10
3111
3145
  });
3112
3146
  const getRoutineFolderSchema = { folderId: nonEmptyId };
3113
- const createRoutineFolderSchema = { name: z.string().min(1) };
3147
+ const createRoutineFolderSchema = routineFolderInputShape;
3114
3148
  const folderToolDefinitions = [
3115
3149
  {
3116
3150
  name: "get-routine-folders",
@@ -3208,8 +3242,7 @@ const folderToolDefinitions = [
3208
3242
  kind: "write",
3209
3243
  responseContract: createRoutineFolderResponse,
3210
3244
  execute: async (runtime, args) => {
3211
- const { name } = args;
3212
- return runtime.getClient().createRoutineFolder({ routine_folder: { title: name } });
3245
+ return runtime.getClient().createRoutineFolder(buildRoutineFolderRequest(args));
3213
3246
  }
3214
3247
  }
3215
3248
  ];
@@ -3374,13 +3407,7 @@ const getExerciseHistorySchema = {
3374
3407
  startDate: z.string().datetime({ offset: true }).describe("ISO 8601 start date for filtering history").optional(),
3375
3408
  endDate: z.string().datetime({ offset: true }).describe("ISO 8601 end date for filtering history").optional()
3376
3409
  };
3377
- const createExerciseTemplateSchema = {
3378
- title: z.string().min(1),
3379
- exerciseType: exerciseTypeEnum,
3380
- equipmentCategory: equipmentCategoryEnum,
3381
- muscleGroup: muscleGroupEnum,
3382
- otherMuscles: z.array(muscleGroupEnum).default([])
3383
- };
3410
+ const createExerciseTemplateSchema = exerciseTemplateInputShape;
3384
3411
  const searchExerciseTemplatesSchema = {
3385
3412
  query: z.string().min(1).describe("Case-insensitive substring to match against exercise template titles"),
3386
3413
  primaryMuscleGroup: muscleGroupEnum.optional().describe("Optional filter to restrict results to a specific primary muscle group"),
@@ -3514,14 +3541,7 @@ const templateToolDefinitions = [
3514
3541
  kind: "write",
3515
3542
  responseContract: createExerciseTemplateResponse,
3516
3543
  execute: async (runtime, args) => {
3517
- const { title, exerciseType, equipmentCategory, muscleGroup, otherMuscles } = args;
3518
- return await runtime.getClient().createExerciseTemplate({ exercise: {
3519
- title,
3520
- exercise_type: exerciseType,
3521
- equipment_category: equipmentCategory,
3522
- muscle_group: muscleGroup,
3523
- other_muscles: otherMuscles
3524
- } });
3544
+ return runtime.getClient().createExerciseTemplate(buildExerciseTemplateRequest(args));
3525
3545
  }
3526
3546
  },
3527
3547
  {
@@ -5542,6 +5562,7 @@ const HELP_TEXT = [
5542
5562
  " HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
5543
5563
  " HEVY_MCP_DEBUG=1 Enable verbose diagnostics on stderr",
5544
5564
  " HEVY_MCP_HTTP_BEARER_TOKEN Protect non-loopback HTTP deployments",
5565
+ " HEVY_MCP_TELEMETRY=0 Disable all project telemetry",
5545
5566
  "",
5546
5567
  "Examples:",
5547
5568
  " HEVY_API_KEY=your-key npx hevy-mcp",
@@ -5570,10 +5591,6 @@ const SAFE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
5570
5591
  "ETIMEDOUT",
5571
5592
  "HEVY_RETRY_EXHAUSTED"
5572
5593
  ]);
5573
- const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
5574
- function fingerprintApiKey(apiKey) {
5575
- return createHmac("sha256", apiKey).update(SENTRY_USER_ID_CONTEXT).digest("hex").slice(0, 10);
5576
- }
5577
5594
  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).") });
5578
5595
  function getHttpStatus(error) {
5579
5596
  if (isHevyHttpError(error)) return error.status;
@@ -5638,9 +5655,7 @@ function buildServer(apiKey, transport = "stdio") {
5638
5655
  }
5639
5656
  async function createNodeMcpServer({ apiKey }, transport = "stdio") {
5640
5657
  const { apiKey: validatedApiKey } = serverConfigSchema.parse({ apiKey });
5641
- const userHash = fingerprintApiKey(validatedApiKey);
5642
- setCurrentUserHash(userHash);
5643
- Sentry.setUser({ id: userHash });
5658
+ setTelemetryUser(validatedApiKey);
5644
5659
  await validateApiKey(validatedApiKey);
5645
5660
  return buildServer(validatedApiKey, transport);
5646
5661
  }
@@ -5656,11 +5671,7 @@ async function runStdioServer() {
5656
5671
  }
5657
5672
  serverStartups.add(1, { version });
5658
5673
  const configuredApiKey = process.env.HEVY_API_KEY;
5659
- const initialUserHash = configuredApiKey ? fingerprintApiKey(configuredApiKey) : void 0;
5660
- if (initialUserHash) {
5661
- setCurrentUserHash(initialUserHash);
5662
- Sentry.setUser({ id: initialUserHash });
5663
- }
5674
+ if (configuredApiKey) setTelemetryUser(configuredApiKey);
5664
5675
  let connectAttempted = false;
5665
5676
  await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
5666
5677
  try {
@@ -5724,6 +5735,7 @@ async function runServer() {
5724
5735
  try {
5725
5736
  const cfg = parseConfig(process.env);
5726
5737
  assertApiKey(cfg.apiKey);
5738
+ setTelemetryUser(cfg.apiKey);
5727
5739
  await validateApiKey(cfg.apiKey);
5728
5740
  const handle = await startStreamableHttpServer(options, cfg.apiKey, (params) => Promise.resolve(buildServer(params.apiKey, "http")));
5729
5741
  listening = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-mcp",
3
- "version": "4.1.0",
3
+ "version": "4.1.2",
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": {
@@ -35,24 +35,24 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@modelcontextprotocol/sdk": "^1.29.0",
38
- "@opentelemetry/api": "^1.9.0",
39
- "@opentelemetry/exporter-metrics-otlp-http": "^0.220.0",
40
- "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
41
- "@opentelemetry/resources": "^2.9.0",
42
- "@opentelemetry/sdk-metrics": "^2.9.0",
43
- "@opentelemetry/sdk-trace-base": "^2.9.0",
44
- "@opentelemetry/sdk-trace-node": "^2.9.0",
45
- "@sentry/node": "^10.58.0",
46
- "@sentry/opentelemetry": "^10.58.0",
38
+ "@opentelemetry/api": "^1.9.1",
39
+ "@opentelemetry/exporter-metrics-otlp-http": "^0.221.0",
40
+ "@opentelemetry/exporter-trace-otlp-http": "^0.221.0",
41
+ "@opentelemetry/resources": "^2.10.0",
42
+ "@opentelemetry/sdk-metrics": "^2.10.0",
43
+ "@opentelemetry/sdk-trace-base": "^2.10.0",
44
+ "@opentelemetry/sdk-trace-node": "^2.10.0",
45
+ "@sentry/node": "^10.68.0",
46
+ "@sentry/opentelemetry": "^10.68.0",
47
47
  "semver": "^7.8.5",
48
48
  "zod": "^4.4.3"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@hevy-mcp/core": "*",
52
52
  "@hevy-mcp/hevy-client": "*",
53
- "@types/node": "^26.0.0",
53
+ "@types/node": "^26.1.1",
54
54
  "cross-env": "^10.1.0",
55
- "tsdown": "^0.22.2",
55
+ "tsdown": "^0.22.14",
56
56
  "typescript": "^7.0.2"
57
57
  },
58
58
  "engines": {
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": "4.1.0",
10
+ "version": "4.1.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "4.1.0",
15
+ "version": "4.1.2",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },