hevy-mcp 4.1.0 → 4.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
@@ -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.1" };
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-BPbLELQf.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.1" };
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-BPbLELQf.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.1" };
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] = "3ad9084e-6d6c-4681-ad27-9168114359ab", e._sentryDebugIdIdentifier = "sentry-dbid-3ad9084e-6d6c-4681-ad27-9168114359ab");
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.1";
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.1`;
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
@@ -2586,7 +2595,7 @@ function registerHevyResources(server, runtime) {
2586
2595
  //#endregion
2587
2596
  //#region ../core/src/server-metadata.ts
2588
2597
  const SERVER_NAME = "hevy-mcp";
2589
- const SERVER_VERSION = "4.1.0";
2598
+ const SERVER_VERSION = "4.1.1";
2590
2599
  const SERVER_INSTRUCTIONS = [
2591
2600
  "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
2601
  "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.",
@@ -5542,6 +5551,7 @@ const HELP_TEXT = [
5542
5551
  " HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
5543
5552
  " HEVY_MCP_DEBUG=1 Enable verbose diagnostics on stderr",
5544
5553
  " HEVY_MCP_HTTP_BEARER_TOKEN Protect non-loopback HTTP deployments",
5554
+ " HEVY_MCP_TELEMETRY=0 Disable all project telemetry",
5545
5555
  "",
5546
5556
  "Examples:",
5547
5557
  " HEVY_API_KEY=your-key npx hevy-mcp",
@@ -5570,10 +5580,6 @@ const SAFE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
5570
5580
  "ETIMEDOUT",
5571
5581
  "HEVY_RETRY_EXHAUSTED"
5572
5582
  ]);
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
5583
  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
5584
  function getHttpStatus(error) {
5579
5585
  if (isHevyHttpError(error)) return error.status;
@@ -5638,9 +5644,7 @@ function buildServer(apiKey, transport = "stdio") {
5638
5644
  }
5639
5645
  async function createNodeMcpServer({ apiKey }, transport = "stdio") {
5640
5646
  const { apiKey: validatedApiKey } = serverConfigSchema.parse({ apiKey });
5641
- const userHash = fingerprintApiKey(validatedApiKey);
5642
- setCurrentUserHash(userHash);
5643
- Sentry.setUser({ id: userHash });
5647
+ setTelemetryUser(validatedApiKey);
5644
5648
  await validateApiKey(validatedApiKey);
5645
5649
  return buildServer(validatedApiKey, transport);
5646
5650
  }
@@ -5656,11 +5660,7 @@ async function runStdioServer() {
5656
5660
  }
5657
5661
  serverStartups.add(1, { version });
5658
5662
  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
- }
5663
+ if (configuredApiKey) setTelemetryUser(configuredApiKey);
5664
5664
  let connectAttempted = false;
5665
5665
  await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
5666
5666
  try {
@@ -5724,6 +5724,7 @@ async function runServer() {
5724
5724
  try {
5725
5725
  const cfg = parseConfig(process.env);
5726
5726
  assertApiKey(cfg.apiKey);
5727
+ setTelemetryUser(cfg.apiKey);
5727
5728
  await validateApiKey(cfg.apiKey);
5728
5729
  const handle = await startStreamableHttpServer(options, cfg.apiKey, (params) => Promise.resolve(buildServer(params.apiKey, "http")));
5729
5730
  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.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": "4.1.0",
10
+ "version": "4.1.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "4.1.0",
15
+ "version": "4.1.1",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },