hevy-mcp 5.0.3 → 5.0.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/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@5.0.3" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.4" };
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-BGQCPTet.mjs";
12
+ import { a as createSafeErrorDiagnostic, i as MissingHevyApiKeyError, n as runServer, o as flushTelemetry } from "./src-CJRjsomO.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@5.0.3" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.4" };
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-BGQCPTet.mjs";
12
+ import { n as runServer, r as runStdioServer, t as createNodeMcpServer } from "./src-CJRjsomO.mjs";
13
13
  export { createNodeMcpServer, runServer, runStdioServer };
@@ -4,29 +4,29 @@
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@5.0.3" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.4" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "b75e442e-2372-4f08-83bb-f0c392f5101b", e._sentryDebugIdIdentifier = "sentry-dbid-b75e442e-2372-4f08-83bb-f0c392f5101b");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "72e7b08a-987c-4dba-aabd-6437da54ce2f", e._sentryDebugIdIdentifier = "sentry-dbid-72e7b08a-987c-4dba-aabd-6437da54ce2f");
10
10
  } catch (e) {}
11
11
  })();
12
- import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
12
+ import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
13
  import * as Sentry from "@sentry/node";
14
14
  import { SentryPropagator, SentrySampler, SentrySpanProcessor } from "@sentry/opentelemetry";
15
- import { SpanStatusCode, metrics, trace } from "@opentelemetry/api";
15
+ import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
16
16
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
17
- import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
17
+ import { AggregationTemporalityPreference, OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
18
18
  import { resourceFromAttributes } from "@opentelemetry/resources";
19
19
  import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
20
20
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
21
21
  import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
22
22
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
23
+ import { AsyncLocalStorage } from "node:async_hooks";
23
24
  import { ZodError, z } from "zod";
24
25
  import { McpServer, deserializeMessage } from "@modelcontextprotocol/server";
25
26
  import { z as z$1 } from "zod/v4";
26
27
  import { isIP } from "node:net";
27
28
  import { createServer } from "node:http";
28
29
  import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
29
- import { AsyncLocalStorage } from "node:async_hooks";
30
30
  import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
31
31
  import { homedir } from "node:os";
32
32
  import { dirname, join } from "node:path";
@@ -79,16 +79,55 @@ function sanitizeSentryMcpSpan(span) {
79
79
  * Sentry SDK: error events, performance traces, release tracking
80
80
  * OTel Collector → Honeycomb: performance traces, metrics
81
81
  */
82
+ const SAFE_EXCEPTION_TYPES = /* @__PURE__ */ new Set([
83
+ "AggregateError",
84
+ "DOMException",
85
+ "Error",
86
+ "EvalError",
87
+ "HevyHttpError",
88
+ "RangeError",
89
+ "ReferenceError",
90
+ "SyntaxError",
91
+ "TypeError",
92
+ "URIError",
93
+ "ProtocolError",
94
+ "ZodError"
95
+ ]);
96
+ const SAFE_EXCEPTION_CODES = /* @__PURE__ */ new Set([
97
+ "EAI_AGAIN",
98
+ "ECONNABORTED",
99
+ "ECONNREFUSED",
100
+ "ECONNRESET",
101
+ "ENETUNREACH",
102
+ "ENOTFOUND",
103
+ "ERR_NETWORK",
104
+ "ERR_SOCKET_TIMEOUT",
105
+ "ETIMEDOUT",
106
+ "HEVY_INVALID_ENDPOINT",
107
+ "HEVY_REQUEST_ABORTED",
108
+ "HEVY_RETRY_EXHAUSTED"
109
+ ]);
82
110
  function normalizeTelemetryError(error) {
83
- const name = error instanceof Error && error.name ? error.name : "Exception";
84
- return new Error(name);
111
+ const candidate = error instanceof Error && typeof error.name === "string" ? error.name : void 0;
112
+ return {
113
+ name: candidate && SAFE_EXCEPTION_TYPES.has(candidate) ? candidate : "UnknownError",
114
+ ...error instanceof Error && error.stack ? { stack: error.stack } : {}
115
+ };
116
+ }
117
+ function getSafeExceptionCode(error) {
118
+ if (!error || typeof error !== "object" || !("code" in error)) return void 0;
119
+ const code = error.code;
120
+ return typeof code === "string" && SAFE_EXCEPTION_CODES.has(code) ? code : void 0;
85
121
  }
86
122
  function recordTelemetryException(error, attributes, span) {
87
123
  if (!telemetryEnabled) return;
88
124
  try {
89
125
  const target = span ?? trace.getActiveSpan();
90
126
  if (!target) return;
91
- target.recordException(normalizeTelemetryError(error));
127
+ const normalized = normalizeTelemetryError(error);
128
+ target.recordException(normalized);
129
+ target.setAttribute("exception.type", normalized.name);
130
+ target.setAttribute("error.category", normalized.name);
92
131
  if (attributes) target.setAttributes(attributes);
93
132
  target.setStatus({ code: SpanStatusCode.ERROR });
94
133
  } catch {}
@@ -97,9 +136,18 @@ function installProcessExceptionTracking(processLike = process) {
97
136
  if (!telemetryEnabled) return () => {};
98
137
  const recordProcessException = (source, error) => {
99
138
  try {
100
- tracer.startActiveSpan(`mcp.process.${source}`, (span) => {
101
- recordTelemetryException(error, { "exception.source": source }, span);
102
- span.end();
139
+ tracer.startActiveSpan(`mcp.process.${source}`, { attributes: { "mcp.span.category": "process" } }, (span) => {
140
+ try {
141
+ const normalized = normalizeTelemetryError(error);
142
+ const code = getSafeExceptionCode(error);
143
+ recordTelemetryException(error, {
144
+ "exception.source": source,
145
+ "error.category": normalized.name,
146
+ ...code ? { "error.code": code } : {}
147
+ }, span);
148
+ } finally {
149
+ span.end();
150
+ }
103
151
  });
104
152
  } catch {}
105
153
  };
@@ -116,14 +164,24 @@ function installProcessExceptionTracking(processLike = process) {
116
164
  };
117
165
  }
118
166
  const name$1 = "hevy-mcp";
119
- const version$1 = "5.0.3";
167
+ const version$1 = "5.0.4";
120
168
  const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
121
169
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
122
170
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
123
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@5.0.3`;
171
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `${name$1}@${version$1}`;
172
+ function createServiceInstanceId(generate = randomUUID) {
173
+ try {
174
+ const generated = generate();
175
+ if (generated.length > 0 && generated.length <= 128) return generated;
176
+ } catch {}
177
+ return randomBytes(16).toString("hex");
178
+ }
124
179
  const resource = resourceFromAttributes({
125
180
  "service.name": name$1,
126
- "service.version": version$1
181
+ "service.version": version$1,
182
+ "service.instance.id": createServiceInstanceId(),
183
+ "process.runtime.name": process.release.name,
184
+ "process.runtime.version": process.version
127
185
  });
128
186
  let currentUserHash;
129
187
  var UserHashSpanProcessor = class {
@@ -168,7 +226,8 @@ if (telemetryEnabled) {
168
226
  readers: [new PeriodicExportingMetricReader({
169
227
  exporter: new OTLPMetricExporter({
170
228
  url: `${COLLECTOR_ENDPOINT}/metrics`,
171
- headers: { Authorization: `Bearer ${collectorToken}` }
229
+ headers: { Authorization: `Bearer ${collectorToken}` },
230
+ temporalityPreference: AggregationTemporalityPreference.DELTA
172
231
  }),
173
232
  exportIntervalMillis: 1e4
174
233
  })]
@@ -196,6 +255,8 @@ async function flushTelemetry(timeoutMs = 1e3) {
196
255
  }
197
256
  const tracer = trace.getTracer(name$1);
198
257
  const meter = metrics.getMeter(name$1);
258
+ const serviceName = name$1;
259
+ const serviceVersion = version$1;
199
260
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
200
261
  function setTelemetryUser(apiKey) {
201
262
  if (!telemetryEnabled) return;
@@ -1004,6 +1065,35 @@ const SAFE_STATIC_ENDPOINTS = /* @__PURE__ */ new Set([
1004
1065
  "/v1/workouts/count",
1005
1066
  "/v1/workouts/events"
1006
1067
  ]);
1068
+ const EXPECTED_READ_404_ENDPOINTS = /* @__PURE__ */ new Set([
1069
+ "/v1/body_measurements/:date",
1070
+ "/v1/exercise_history/:exerciseTemplateId",
1071
+ "/v1/exercise_templates/:exerciseTemplateId",
1072
+ "/v1/routine_folders/:folderId",
1073
+ "/v1/routines/:routineId",
1074
+ "/v1/workouts/:workoutId"
1075
+ ]);
1076
+ const EXPECTED_LIST_404_ENDPOINTS = /* @__PURE__ */ new Set([
1077
+ "/v1/body_measurements",
1078
+ "/v1/exercise_templates",
1079
+ "/v1/routine_folders",
1080
+ "/v1/routines",
1081
+ "/v1/workouts",
1082
+ "/v1/workouts/events"
1083
+ ]);
1084
+ const SAFE_OBSERVATION_CODES = /* @__PURE__ */ new Set([
1085
+ "EAI_AGAIN",
1086
+ "ECONNABORTED",
1087
+ "ECONNREFUSED",
1088
+ "ECONNRESET",
1089
+ "ENETUNREACH",
1090
+ "ENOTFOUND",
1091
+ "ERR_NETWORK",
1092
+ "ERR_SOCKET_TIMEOUT",
1093
+ "ETIMEDOUT",
1094
+ HEVY_REQUEST_ABORTED_ERROR_CODE,
1095
+ HEVY_RETRY_EXHAUSTED_ERROR_CODE
1096
+ ]);
1007
1097
  const SAFE_DYNAMIC_ENDPOINTS = [
1008
1098
  ["/v1/body_measurements/", "/v1/body_measurements/:date"],
1009
1099
  ["/v1/exercise_history/", "/v1/exercise_history/:exerciseTemplateId"],
@@ -1049,9 +1139,11 @@ function getRequestContext(config) {
1049
1139
  let endpoint = "unknown";
1050
1140
  if (SAFE_STATIC_ENDPOINTS.has(rawEndpoint)) endpoint = rawEndpoint;
1051
1141
  else endpoint = SAFE_DYNAMIC_ENDPOINTS.find(([prefix]) => rawEndpoint.startsWith(prefix))?.[1] ?? "unknown";
1142
+ const page = config.params !== null && typeof config.params === "object" && "page" in config.params && typeof config.params.page === "number" ? config.params.page : void 0;
1052
1143
  return {
1053
1144
  method,
1054
- endpoint
1145
+ endpoint,
1146
+ page
1055
1147
  };
1056
1148
  }
1057
1149
  function emitClientLog(logger, message) {
@@ -1061,11 +1153,49 @@ function emitClientLog(logger, message) {
1061
1153
  console.error("Failed to emit structured Hevy API log");
1062
1154
  }
1063
1155
  }
1156
+ function emitRequestStart(observer, observation) {
1157
+ try {
1158
+ return observer?.(observation) ?? void 0;
1159
+ } catch {
1160
+ return;
1161
+ }
1162
+ }
1163
+ function finishRequestObservation(scope, observation) {
1164
+ try {
1165
+ scope?.finish(observation);
1166
+ } catch {}
1167
+ }
1064
1168
  function emitRequestObservation(observer, observation) {
1065
1169
  try {
1066
1170
  observer?.(observation);
1067
1171
  } catch {}
1068
1172
  }
1173
+ function runRequestObservation(scope, operation) {
1174
+ if (!scope?.run) return operation();
1175
+ let started = false;
1176
+ const trackedOperation = () => {
1177
+ started = true;
1178
+ return operation();
1179
+ };
1180
+ try {
1181
+ return scope.run(trackedOperation);
1182
+ } catch (error) {
1183
+ if (started) throw error;
1184
+ return operation();
1185
+ }
1186
+ }
1187
+ function emitRetryWait(observer, observation) {
1188
+ try {
1189
+ return observer?.(observation) ?? void 0;
1190
+ } catch {
1191
+ return;
1192
+ }
1193
+ }
1194
+ function finishRetryWait(scope) {
1195
+ try {
1196
+ scope?.finish();
1197
+ } catch {}
1198
+ }
1069
1199
  function parseRetryAfterMs(value) {
1070
1200
  if (!value) return void 0;
1071
1201
  const seconds = Number(value);
@@ -1122,7 +1252,7 @@ function createNativeClient(apiKey, baseUrl, options) {
1122
1252
  ...clientConfig,
1123
1253
  ...config
1124
1254
  };
1125
- const { method, endpoint } = getRequestContext(normalized);
1255
+ const { method, endpoint, page } = getRequestContext(normalized);
1126
1256
  const url = buildUrl(baseUrl, normalized);
1127
1257
  let retryCount = 0;
1128
1258
  while (true) {
@@ -1132,43 +1262,53 @@ function createNativeClient(apiKey, baseUrl, options) {
1132
1262
  code: HEVY_REQUEST_ABORTED_ERROR_CODE
1133
1263
  });
1134
1264
  const startedAt = Date.now();
1265
+ const observationScope = emitRequestStart(options.onRequestStart, {
1266
+ method,
1267
+ endpoint,
1268
+ retryCount
1269
+ });
1135
1270
  const controller = new AbortController();
1136
1271
  const abortFromCaller = () => controller.abort();
1137
1272
  normalized.signal?.addEventListener("abort", abortFromCaller, { once: true });
1138
1273
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
1139
1274
  try {
1140
- const headers = new Headers({ "api-key": apiKey });
1141
- if (normalized.data !== void 0 && !(normalized.data instanceof FormData)) headers.set("content-type", "application/json");
1142
- const attemptDeadline = Date.now() + timeoutMs;
1143
- const response = await withTimeout(fetchImplementation(url, {
1144
- method,
1145
- headers,
1146
- redirect: "manual",
1147
- body: normalized.data instanceof FormData ? normalized.data : normalized.data === void 0 ? void 0 : JSON.stringify(normalized.data),
1148
- signal: controller.signal
1149
- }), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1150
- const data = await withTimeout(parseResponseData(response), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1151
- if (!response.ok) throw new HevyHttpError(`Hevy API request failed (HTTP ${response.status})`, {
1152
- status: response.status,
1153
- statusText: response.statusText,
1154
- data,
1155
- headers: response.headers,
1156
- method,
1157
- endpoint
1158
- });
1159
- emitRequestObservation(options.onRequestComplete, {
1160
- method,
1161
- endpoint,
1162
- status: response.status,
1163
- durationMs: Date.now() - startedAt,
1164
- retryCount
1275
+ return await runRequestObservation(observationScope, async () => {
1276
+ const headers = new Headers({ "api-key": apiKey });
1277
+ if (normalized.data !== void 0 && !(normalized.data instanceof FormData)) headers.set("content-type", "application/json");
1278
+ const attemptDeadline = Date.now() + timeoutMs;
1279
+ const response = await withTimeout(fetchImplementation(url, {
1280
+ method,
1281
+ headers,
1282
+ redirect: "manual",
1283
+ body: normalized.data instanceof FormData ? normalized.data : normalized.data === void 0 ? void 0 : JSON.stringify(normalized.data),
1284
+ signal: controller.signal
1285
+ }), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1286
+ const data = await withTimeout(parseResponseData(response), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1287
+ if (!response.ok) throw new HevyHttpError(`Hevy API request failed (HTTP ${response.status})`, {
1288
+ status: response.status,
1289
+ statusText: response.statusText,
1290
+ data,
1291
+ headers: response.headers,
1292
+ method,
1293
+ endpoint
1294
+ });
1295
+ const observation = {
1296
+ method,
1297
+ endpoint,
1298
+ status: response.status,
1299
+ durationMs: Date.now() - startedAt,
1300
+ retryCount,
1301
+ outcome: "success"
1302
+ };
1303
+ finishRequestObservation(observationScope, observation);
1304
+ emitRequestObservation(options.onRequestComplete, observation);
1305
+ return {
1306
+ data,
1307
+ status: response.status,
1308
+ statusText: response.statusText,
1309
+ headers: response.headers
1310
+ };
1165
1311
  });
1166
- return {
1167
- data,
1168
- status: response.status,
1169
- statusText: response.statusText,
1170
- headers: response.headers
1171
- };
1172
1312
  } catch (cause) {
1173
1313
  const error = isHevyHttpError(cause) ? cause : new HevyHttpError(normalized.signal?.aborted ? "Hevy API request was canceled" : "Hevy API network request failed", {
1174
1314
  method,
@@ -1176,24 +1316,31 @@ function createNativeClient(apiKey, baseUrl, options) {
1176
1316
  code: normalized.signal?.aborted ? HEVY_REQUEST_ABORTED_ERROR_CODE : getNetworkCode(cause),
1177
1317
  cause
1178
1318
  });
1319
+ const expectedReason = error.status === 404 && method === "GET" && EXPECTED_READ_404_ENDPOINTS.has(endpoint) ? "not_found" : error.status === 404 && method === "GET" && page !== void 0 && page > 1 && EXPECTED_LIST_404_ENDPOINTS.has(endpoint) ? "end_of_list" : void 0;
1179
1320
  const canRetry = method === "GET" && isRetryable(error);
1180
- if (canRetry && retryCount >= maxGetRetries) {
1321
+ const retryExhausted = canRetry && retryCount >= maxGetRetries;
1322
+ if (retryExhausted) {
1181
1323
  error.hevyRetryExhausted = true;
1182
1324
  error.hevyRetryCount = retryCount;
1183
1325
  error.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
1184
1326
  }
1185
- emitRequestObservation(options.onRequestComplete, {
1327
+ const observation = {
1186
1328
  method,
1187
1329
  endpoint,
1188
1330
  status: error.status ?? 0,
1189
1331
  durationMs: Date.now() - startedAt,
1190
1332
  retryCount,
1333
+ outcome: expectedReason ? "expected" : canRetry && !retryExhausted ? "retryable_failure" : "terminal_failure",
1334
+ ...expectedReason ? { expectedReason } : {},
1191
1335
  error: {
1192
1336
  status: error.status,
1193
- code: error.code
1337
+ code: typeof error.code === "string" && SAFE_OBSERVATION_CODES.has(error.code) ? error.code : void 0,
1338
+ category: error.status === void 0 ? "NetworkError" : "HevyHttpError"
1194
1339
  }
1195
- });
1196
- if (!canRetry) {
1340
+ };
1341
+ finishRequestObservation(observationScope, observation);
1342
+ emitRequestObservation(options.onRequestComplete, observation);
1343
+ if (!expectedReason && !canRetry) {
1197
1344
  emitClientLog(options.onLog, {
1198
1345
  level: "error",
1199
1346
  logger: "hevy-api",
@@ -1206,7 +1353,7 @@ function createNativeClient(apiKey, baseUrl, options) {
1206
1353
  });
1207
1354
  throw error;
1208
1355
  }
1209
- if (error.hevyRetryExhausted) throw error;
1356
+ if (expectedReason || error.hevyRetryExhausted) throw error;
1210
1357
  retryCount += 1;
1211
1358
  const delayMs = getRetryDelayMs(error, retryCount);
1212
1359
  emitClientLog(options.onLog, {
@@ -1222,7 +1369,19 @@ function createNativeClient(apiKey, baseUrl, options) {
1222
1369
  endpoint
1223
1370
  }
1224
1371
  });
1225
- await sleep(delayMs);
1372
+ await runRequestObservation(observationScope, async () => {
1373
+ const retryWaitScope = emitRetryWait(options.onRetryWait, {
1374
+ method,
1375
+ endpoint,
1376
+ retryCount,
1377
+ delayMs
1378
+ });
1379
+ try {
1380
+ await sleep(delayMs);
1381
+ } finally {
1382
+ finishRetryWait(retryWaitScope);
1383
+ }
1384
+ });
1226
1385
  } finally {
1227
1386
  clearTimeout(timeout);
1228
1387
  normalized.signal?.removeEventListener("abort", abortFromCaller);
@@ -2680,7 +2839,7 @@ function registerHevyResources(server, runtime) {
2680
2839
  //#endregion
2681
2840
  //#region ../core/src/server-metadata.ts
2682
2841
  const SERVER_NAME = "hevy-mcp";
2683
- const SERVER_VERSION = "5.0.3";
2842
+ const SERVER_VERSION = "5.0.4";
2684
2843
  const SERVER_INSTRUCTIONS = [
2685
2844
  "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.",
2686
2845
  "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.",
@@ -4078,7 +4237,7 @@ function createErrorResponse(error, context) {
4078
4237
  axios: axiosErrorContext ?? void 0
4079
4238
  };
4080
4239
  const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${policy.message}`;
4081
- console.error(createMcpToolFailureEvent(context || "unknown", policy.type, diagnostic));
4240
+ console.error(JSON.stringify(createMcpToolFailureEvent(context || "unknown", policy.type, diagnostic)));
4082
4241
  return {
4083
4242
  content: [{
4084
4243
  type: "text",
@@ -4240,39 +4399,69 @@ var AsyncTtlCache = class {
4240
4399
  ttlMs;
4241
4400
  maxSize;
4242
4401
  now;
4402
+ observer;
4243
4403
  entries = /* @__PURE__ */ new Map();
4244
4404
  inFlight = /* @__PURE__ */ new Map();
4245
4405
  requestCounter = 0;
4246
4406
  constructor(options, now = Date.now) {
4247
- const { ttlMs, maxSize } = options;
4407
+ const { ttlMs, maxSize, observer } = options;
4248
4408
  if (ttlMs <= 0) throw new Error("Cache ttlMs must be greater than 0.");
4249
4409
  if (maxSize <= 0) throw new Error("Cache maxSize must be greater than 0.");
4250
4410
  this.ttlMs = ttlMs;
4251
4411
  this.maxSize = maxSize;
4252
4412
  this.now = now;
4413
+ this.observer = observer;
4414
+ }
4415
+ startObservation(state) {
4416
+ try {
4417
+ return this.observer?.start({ state }) ?? void 0;
4418
+ } catch {
4419
+ return;
4420
+ }
4421
+ }
4422
+ finishObservation(scope, metadata) {
4423
+ try {
4424
+ scope?.finish(metadata);
4425
+ } catch {}
4253
4426
  }
4254
4427
  async getOrFetch(key, fetcher, options = {}) {
4255
- const { refresh = false } = options;
4428
+ const { refresh = false, getObservationMetadata } = options;
4429
+ let observationState = refresh ? "refresh" : "miss";
4256
4430
  if (refresh) this.invalidate(key);
4257
4431
  else {
4258
4432
  const cachedEntry = this.entries.get(key);
4259
4433
  if (cachedEntry !== void 0) {
4260
4434
  if (cachedEntry.expiresAt > this.now()) {
4261
4435
  this.markAsRecentlyUsed(key, cachedEntry);
4436
+ const observationScope = this.startObservation("hit");
4437
+ this.finishObservation(observationScope);
4262
4438
  return cachedEntry.value;
4263
4439
  }
4264
4440
  this.entries.delete(key);
4441
+ observationState = "expired";
4265
4442
  }
4266
4443
  const inFlightEntry = this.inFlight.get(key);
4267
- if (inFlightEntry !== void 0) return inFlightEntry.promise;
4444
+ if (inFlightEntry !== void 0) {
4445
+ const observationScope = this.startObservation("inflight_wait");
4446
+ inFlightEntry.promise.then(() => this.finishObservation(observationScope), () => this.finishObservation(observationScope));
4447
+ return inFlightEntry.promise;
4448
+ }
4268
4449
  }
4269
4450
  const requestId = ++this.requestCounter;
4451
+ const observationScope = this.startObservation(observationState);
4270
4452
  const request = (async () => {
4271
4453
  try {
4272
4454
  const value = await fetcher();
4273
4455
  if (this.isCurrentRequest(key, requestId)) this.setValue(key, value);
4274
4456
  return value;
4275
4457
  } finally {
4458
+ let metadata;
4459
+ try {
4460
+ metadata = getObservationMetadata?.();
4461
+ } catch {
4462
+ metadata = void 0;
4463
+ }
4464
+ this.finishObservation(observationScope, metadata);
4276
4465
  if (this.inFlight.get(key)?.requestId === requestId) this.inFlight.delete(key);
4277
4466
  }
4278
4467
  })();
@@ -4321,17 +4510,20 @@ var AsyncTtlCache = class {
4321
4510
  const EXERCISE_TEMPLATE_CATALOG_CACHE_KEY = "exercise-template-catalog";
4322
4511
  const EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS = 300 * 1e3;
4323
4512
  const EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE = 1;
4324
- /** Create a cache owned by one MCP server/request lifecycle. */
4325
- function createExerciseTemplateCatalog(hevyClient) {
4513
+ function createExerciseTemplateCatalog(hevyClient, cacheObserver) {
4326
4514
  const cache = new AsyncTtlCache({
4327
4515
  ttlMs: EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS,
4328
- maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE
4516
+ maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE,
4517
+ observer: cacheObserver
4329
4518
  });
4330
4519
  return {
4331
4520
  get(options = {}) {
4332
4521
  const reason = options.refresh ? "explicit-refresh" : cache.size === 0 ? "initial-load" : "ttl-expired";
4522
+ let observationMetadata;
4523
+ let pagesLoaded = 0;
4333
4524
  return cache.getOrFetch(EXERCISE_TEMPLATE_CATALOG_CACHE_KEY, async () => {
4334
4525
  const catalog = await fetchAllPages(async (page, pageSize) => {
4526
+ pagesLoaded = page;
4335
4527
  const data = await hevyClient.getExerciseTemplates({
4336
4528
  page,
4337
4529
  pageSize
@@ -4341,9 +4533,17 @@ function createExerciseTemplateCatalog(hevyClient) {
4341
4533
  pageCount: data?.page_count
4342
4534
  };
4343
4535
  }, 100);
4536
+ observationMetadata = {
4537
+ refreshReason: reason,
4538
+ pageCountBucket: bucketCount(pagesLoaded),
4539
+ itemCountBucket: bucketCount(catalog.length)
4540
+ };
4344
4541
  options.onRefreshed?.(catalog, reason);
4345
4542
  return catalog;
4346
- }, options);
4543
+ }, {
4544
+ refresh: options.refresh,
4545
+ getObservationMetadata: () => observationMetadata
4546
+ });
4347
4547
  },
4348
4548
  reset() {
4349
4549
  cache.clear();
@@ -4405,7 +4605,7 @@ function createHevyMcpServer(options) {
4405
4605
  const client = options.createClient({ onLog: (event) => mcpLogger(event) });
4406
4606
  const runtime = createToolRuntime({
4407
4607
  client,
4408
- catalog: createExerciseTemplateCatalog(client),
4608
+ catalog: createExerciseTemplateCatalog(client, options.cacheObserver),
4409
4609
  logger: mcpLogger,
4410
4610
  observer: options.observer
4411
4611
  });
@@ -4530,13 +4730,15 @@ function extractMcpClientMetadata(message) {
4530
4730
  protocolVersion: normalizeMetadata(params.protocolVersion)
4531
4731
  };
4532
4732
  }
4533
- function createMcpSessionContext(message, transport = "stdio") {
4733
+ function createMcpSessionContext(message, transport = "stdio", options = {}) {
4734
+ const normalizedOptions = typeof options === "function" ? { generateTelemetrySessionId: options } : options;
4534
4735
  return {
4535
4736
  metadata: extractMcpClientMetadata(message),
4536
- startedAt: Date.now(),
4737
+ startedAt: (normalizedOptions.now ?? Date.now)(),
4537
4738
  toolCalls: 0,
4538
4739
  hadToolFailure: false,
4539
- transport
4740
+ transport,
4741
+ telemetrySessionId: normalizedOptions.telemetrySessionId ?? (normalizedOptions.generateTelemetrySessionId ?? randomUUID)()
4540
4742
  };
4541
4743
  }
4542
4744
  function runWithMcpSessionContext(context, callback) {
@@ -4545,6 +4747,9 @@ function runWithMcpSessionContext(context, callback) {
4545
4747
  function getSession() {
4546
4748
  return contextStorage.getStore() ?? activeStdioSession;
4547
4749
  }
4750
+ function getCurrentMcpSessionId() {
4751
+ return getSession()?.telemetrySessionId;
4752
+ }
4548
4753
  function metadataAttributes(metadata, transport) {
4549
4754
  return {
4550
4755
  client_name: metadata.name,
@@ -4985,6 +5190,38 @@ function debugLog(event, data) {
4985
5190
  }
4986
5191
  //#endregion
4987
5192
  //#region src/utils/hevy-client-observability.ts
5193
+ function safeErrorAttributes(observation) {
5194
+ if (!observation.error) return {};
5195
+ const code = observation.error.code;
5196
+ return {
5197
+ error_category: observation.error.category ?? "HevyHttpError",
5198
+ ...code && SAFE_OBSERVATION_CODES.has(code) ? { error_code: code } : {}
5199
+ };
5200
+ }
5201
+ function startApiSpan(start) {
5202
+ const sessionId = getCurrentMcpSessionId();
5203
+ return tracer.startSpan(`hevy.api.${start.method}`, { attributes: {
5204
+ "mcp.span.category": "api",
5205
+ "http.request.method": start.method,
5206
+ "hevy.api.endpoint": start.endpoint,
5207
+ "hevy.api.retry_count_bucket": bucketCount(start.retryCount),
5208
+ "mcp.transport": getCurrentMcpTransport(),
5209
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5210
+ } });
5211
+ }
5212
+ function finishApiSpan(span, observation) {
5213
+ const errorAttributes = safeErrorAttributes(observation);
5214
+ if (observation.status > 0) span.setAttribute("http.response.status_code", observation.status);
5215
+ span.setAttribute("hevy.api.outcome", observation.outcome);
5216
+ if (observation.expectedReason) span.setAttribute("hevy.api.expected_reason", observation.expectedReason);
5217
+ for (const [key, value] of Object.entries(errorAttributes)) span.setAttribute(key, value);
5218
+ span.setStatus({ code: observation.outcome === "success" || observation.outcome === "expected" ? SpanStatusCode.OK : SpanStatusCode.ERROR });
5219
+ if (observation.outcome !== "success" && observation.outcome !== "expected") span.addEvent("hevy.api.failure", {
5220
+ "error.category": errorAttributes.error_category ?? "HevyHttpError",
5221
+ ...errorAttributes.error_code ? { "error.code": errorAttributes.error_code } : {}
5222
+ });
5223
+ span.end();
5224
+ }
4988
5225
  /** Node-only adapter; the Worker graph never imports telemetry or metrics. */
4989
5226
  function createNodeHevyClientOptions() {
4990
5227
  const rawTimeout = process.env.HEVY_MCP_API_TIMEOUT;
@@ -4992,37 +5229,38 @@ function createNodeHevyClientOptions() {
4992
5229
  const timeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? Math.floor(parsedTimeout) : void 0;
4993
5230
  return {
4994
5231
  ...timeoutMs ? { timeoutMs } : {},
5232
+ onRequestStart(start) {
5233
+ const span = startApiSpan(start);
5234
+ return {
5235
+ run(operation) {
5236
+ return context.with(trace.setSpan(context.active(), span), operation);
5237
+ },
5238
+ finish(observation) {
5239
+ finishApiSpan(span, observation);
5240
+ }
5241
+ };
5242
+ },
4995
5243
  onRequestComplete(observation) {
4996
5244
  const retryCountBucket = bucketCount(observation.retryCount);
4997
- const diagnostic = observation.error ? createSafeErrorDiagnostic(observation.error) : void 0;
4998
- const safeErrorAttributes = diagnostic ? {
4999
- error_category: diagnostic.category,
5000
- ...diagnostic.code ? { error_code: diagnostic.code } : {}
5001
- } : {};
5002
- const span = tracer.startSpan(`hevy.api.${observation.method}`, { attributes: {
5003
- "http.method": observation.method,
5004
- "http.status_code": observation.status,
5005
- "hevy.api.retry_count_bucket": retryCountBucket,
5006
- "hevy.api.endpoint": observation.endpoint
5007
- } });
5008
- span.setStatus({ code: observation.error ? SpanStatusCode.ERROR : SpanStatusCode.OK });
5009
- if (diagnostic) span.addEvent("hevy.api.failure", {
5010
- "error.category": diagnostic.category,
5011
- ...diagnostic.code ? { "error.code": diagnostic.code } : {}
5012
- });
5013
- span.end();
5245
+ const errorAttributes = safeErrorAttributes(observation);
5014
5246
  apiCalls.add(1, {
5015
5247
  method: observation.method,
5016
5248
  endpoint: observation.endpoint,
5017
5249
  status_code: observation.status,
5018
5250
  retry_count_bucket: retryCountBucket,
5019
- ...safeErrorAttributes
5251
+ outcome: observation.outcome,
5252
+ transport: getCurrentMcpTransport(),
5253
+ ...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
5254
+ ...errorAttributes
5020
5255
  });
5021
5256
  apiDuration.record(observation.durationMs, {
5022
5257
  method: observation.method,
5023
5258
  endpoint: observation.endpoint,
5024
5259
  retry_count_bucket: retryCountBucket,
5025
- ...safeErrorAttributes
5260
+ outcome: observation.outcome,
5261
+ transport: getCurrentMcpTransport(),
5262
+ ...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
5263
+ ...errorAttributes
5026
5264
  });
5027
5265
  debugLog("api_response", {
5028
5266
  method: observation.method,
@@ -5030,13 +5268,43 @@ function createNodeHevyClientOptions() {
5030
5268
  durationMs: observation.durationMs,
5031
5269
  status: observation.status || null,
5032
5270
  retryCountBucket,
5033
- ...safeErrorAttributes
5271
+ outcome: observation.outcome,
5272
+ ...errorAttributes
5034
5273
  });
5274
+ },
5275
+ onRetryWait(observation) {
5276
+ const sessionId = getCurrentMcpSessionId();
5277
+ const span = tracer.startSpan("hevy.api.retry_wait", { attributes: {
5278
+ "mcp.span.category": "api",
5279
+ "http.request.method": observation.method,
5280
+ "hevy.api.retry_count_bucket": bucketCount(observation.retryCount),
5281
+ "hevy.api.retry_wait_ms": Math.max(0, Math.min(5e3, observation.delayMs)),
5282
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5283
+ } });
5284
+ return { finish: () => span.end() };
5035
5285
  }
5036
5286
  };
5037
5287
  }
5288
+ function createNodeCacheObserver() {
5289
+ return { start(observation) {
5290
+ const sessionId = getCurrentMcpSessionId();
5291
+ const span = tracer.startSpan(`hevy.cache.${observation.state}`, { attributes: {
5292
+ "mcp.span.category": "cache",
5293
+ "hevy.cache.state": observation.state,
5294
+ "mcp.transport": getCurrentMcpTransport(),
5295
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5296
+ } });
5297
+ return { finish(metadata) {
5298
+ if (metadata?.refreshReason) span.setAttribute("hevy.cache.refresh_reason", metadata.refreshReason);
5299
+ if (metadata?.pageCountBucket) span.setAttribute("hevy.cache.page_count_bucket", metadata.pageCountBucket);
5300
+ if (metadata?.itemCountBucket) span.setAttribute("hevy.cache.item_count_bucket", metadata.itemCountBucket);
5301
+ span.end();
5302
+ } };
5303
+ } };
5304
+ }
5038
5305
  //#endregion
5039
5306
  //#region src/utils/tool-observer.ts
5307
+ const DISCOVERY_TOOL_NAMES = /* @__PURE__ */ new Set(["search-routines"]);
5040
5308
  const WORKFLOW_PAGINATION_RESOURCES = /* @__PURE__ */ new Set([
5041
5309
  "workouts",
5042
5310
  "bodyMeasurements",
@@ -5059,8 +5327,11 @@ function metricAttributes(invocation, clientAttributes) {
5059
5327
  }
5060
5328
  function createAttributes(invocation) {
5061
5329
  const clientMetadata = getCurrentMcpClientMetadata();
5330
+ const isPrompt = invocation.kind === "prompt";
5331
+ const sessionId = getCurrentMcpSessionId();
5062
5332
  const attributes = {
5063
- [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5333
+ "mcp.span.category": DISCOVERY_TOOL_NAMES.has(invocation.name) ? "discovery" : "tool",
5334
+ [isPrompt ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5064
5335
  "mcp.operation.kind": invocation.kind ?? "tool",
5065
5336
  ...taxonomyAttributes(invocation),
5066
5337
  "mcp.client.name": clientMetadata.name,
@@ -5068,7 +5339,8 @@ function createAttributes(invocation) {
5068
5339
  "mcp.protocol.version": clientMetadata.protocolVersion,
5069
5340
  "mcp.transport": getCurrentMcpTransport(),
5070
5341
  "mcp.tool.args.key_count_bucket": invocation.argumentKeyCountBucket ?? "unknown",
5071
- "mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? ""
5342
+ "mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? "",
5343
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5072
5344
  };
5073
5345
  for (const key of Object.keys(invocation.argumentPresence ?? {})) attributes[`mcp.tool.args.${key}.present`] = true;
5074
5346
  for (const [key, bucket] of Object.entries(invocation.numericArgumentBuckets ?? {})) if (bucket !== void 0) attributes[`mcp.tool.args.${key}.bucket`] = bucket;
@@ -5106,14 +5378,16 @@ function resultMetricAttributes(completion) {
5106
5378
  if (summary?.setCountBucket) attributes["mcp.tool.result.set_count_bucket"] = summary.setCountBucket;
5107
5379
  return attributes;
5108
5380
  }
5109
- function setSafeErrorAttributes(span, completion) {
5381
+ function setSafeErrorAttributes(span, invocation, completion) {
5110
5382
  const diagnostic = completion.error;
5111
5383
  const errorType = completion.errorType ?? "UNKNOWN_ERROR";
5112
5384
  if (diagnostic) span.addEvent("mcp.tool.failure", {
5385
+ "mcp.tool.name": invocation.name,
5386
+ "error.type": errorType,
5113
5387
  "error.category": diagnostic.category,
5114
5388
  ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5115
- ...diagnostic.status !== void 0 ? { "http.status_code": diagnostic.status } : {},
5116
- ...diagnostic.method ? { "http.method": diagnostic.method } : {},
5389
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
5390
+ ...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
5117
5391
  ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
5118
5392
  });
5119
5393
  span.setAttribute("error.type", errorType);
@@ -5130,8 +5404,8 @@ function captureSafeToolFailure(invocation, completion) {
5130
5404
  scope.setTag("error.type", completion.errorType ?? "UNKNOWN_ERROR");
5131
5405
  scope.setTag("error.category", category);
5132
5406
  if (diagnostic?.code) scope.setTag("error.code", diagnostic.code);
5133
- if (diagnostic?.status !== void 0) scope.setTag("http.status_code", String(diagnostic.status));
5134
- if (diagnostic?.method) scope.setTag("http.method", diagnostic.method);
5407
+ if (diagnostic?.status !== void 0) scope.setTag("http.response.status_code", String(diagnostic.status));
5408
+ if (diagnostic?.method) scope.setTag("http.request.method", diagnostic.method);
5135
5409
  if (diagnostic?.endpoint) scope.setTag("hevy.api.endpoint", diagnostic.endpoint);
5136
5410
  scope.setContext(invocation.kind === "prompt" ? "mcpPrompt" : "mcpTool", {
5137
5411
  context: invocation.name,
@@ -5185,26 +5459,35 @@ function createNodeToolObserver() {
5185
5459
  activeSpan.setAttribute("mcp.tool.outcome", nextCompletion.outcome);
5186
5460
  setResultAttributes(activeSpan, nextCompletion);
5187
5461
  if (nextCompletion.outcome === "thrown_error") {
5188
- errorType = setSafeErrorAttributes(activeSpan, nextCompletion);
5462
+ errorType = setSafeErrorAttributes(activeSpan, invocation, nextCompletion);
5189
5463
  bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? errorType), {
5190
5464
  [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5191
5465
  "error.type": errorType ?? "UNKNOWN_ERROR",
5192
5466
  "error.category": nextCompletion.error?.category ?? "UnknownError",
5193
5467
  ...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
5194
- ...nextCompletion.error?.status !== void 0 ? { "http.status_code": nextCompletion.error.status } : {},
5195
- ...nextCompletion.error?.method ? { "http.method": nextCompletion.error.method } : {},
5468
+ ...nextCompletion.error?.status !== void 0 ? { "http.response.status_code": nextCompletion.error.status } : {},
5469
+ ...nextCompletion.error?.method ? { "http.request.method": nextCompletion.error.method } : {},
5470
+ ...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
5471
+ }, activeSpan));
5472
+ }
5473
+ if (nextCompletion.outcome === "returned_error") {
5474
+ activeSpan.addEvent("mcp.tool.failure", {
5475
+ "mcp.tool.name": invocation.name,
5476
+ "error.type": "UNKNOWN_ERROR",
5477
+ "error.category": "McpToolReturnedError",
5478
+ "error.code": "MCP_TOOL_RETURNED_ERROR"
5479
+ });
5480
+ activeSpan.setAttribute("error.type", "UNKNOWN_ERROR");
5481
+ bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? "UnknownError"), {
5482
+ [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5483
+ "error.type": nextCompletion.errorType ?? "UNKNOWN_ERROR",
5484
+ "error.category": nextCompletion.error?.category ?? "UnknownError",
5485
+ ...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
5486
+ ...nextCompletion.error?.status !== void 0 ? { "http.response.status_code": nextCompletion.error.status } : {},
5487
+ ...nextCompletion.error?.method ? { "http.request.method": nextCompletion.error.method } : {},
5196
5488
  ...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
5197
5489
  }, activeSpan));
5198
5490
  }
5199
- if (nextCompletion.outcome === "returned_error") bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? "UnknownError"), {
5200
- [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5201
- "error.type": nextCompletion.errorType ?? "UNKNOWN_ERROR",
5202
- "error.category": nextCompletion.error?.category ?? "UnknownError",
5203
- ...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
5204
- ...nextCompletion.error?.status !== void 0 ? { "http.status_code": nextCompletion.error.status } : {},
5205
- ...nextCompletion.error?.method ? { "http.method": nextCompletion.error.method } : {},
5206
- ...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
5207
- }, activeSpan));
5208
5491
  }
5209
5492
  } catch {} finally {
5210
5493
  if (nextCompletion.outcome === "thrown_error") {
@@ -5385,14 +5668,17 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
5385
5668
  const lineHadLeadingBom = line.startsWith(UTF8_BOM);
5386
5669
  const normalizedLine = lineHadLeadingBom ? line.slice(1) : line;
5387
5670
  const lineByteLength = Buffer.byteLength(line);
5671
+ const sessionId = getCurrentMcpSessionId();
5388
5672
  return tracer.startActiveSpan("mcp.stdio.deserialize", { attributes: {
5673
+ "mcp.span.category": "protocol",
5389
5674
  "mcp.transport": "stdio",
5390
5675
  "mcp.stdio.parse.line.char_length": line.length,
5391
5676
  "mcp.stdio.parse.line.byte_length": lineByteLength,
5392
5677
  "mcp.stdio.parse.line.had_leading_bom": lineHadLeadingBom,
5393
5678
  "mcp.stdio.parse.line.bom_stripped": lineHadLeadingBom,
5394
5679
  "mcp.stdio.parse.chunk.last_byte_length": chunkSnapshot.lastChunkByteLength,
5395
- "mcp.stdio.parse.chunk.last_had_utf8_bom": chunkSnapshot.lastChunkStartsWithUtf8Bom
5680
+ "mcp.stdio.parse.chunk.last_had_utf8_bom": chunkSnapshot.lastChunkStartsWithUtf8Bom,
5681
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5396
5682
  } }, (span) => {
5397
5683
  try {
5398
5684
  const message = deserializeMessage(normalizedLine);
@@ -5402,6 +5688,8 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
5402
5688
  if (typeof method === "string" && SAFE_MCP_METHODS[method] === true) span.setAttribute("mcp.method", method);
5403
5689
  if (method === "initialize") {
5404
5690
  const client = recordMcpSessionStart(message);
5691
+ const sessionId = getCurrentMcpSessionId();
5692
+ if (sessionId) span.setAttribute("mcp.session.id", sessionId);
5405
5693
  span.setAttribute("mcp.client.name", client.name);
5406
5694
  span.setAttribute("mcp.client.version", client.version);
5407
5695
  span.setAttribute("mcp.protocol.version", client.protocolVersion);
@@ -5568,8 +5856,8 @@ function scheduleUpdateCheck(options, overrides = {}) {
5568
5856
  }
5569
5857
  //#endregion
5570
5858
  //#region src/index.ts
5571
- const name = name$1;
5572
- const version = version$1;
5859
+ const name = serviceName;
5860
+ const version = serviceVersion;
5573
5861
  const HELP_TEXT = [
5574
5862
  "Usage:",
5575
5863
  " hevy-mcp [options]",
@@ -5629,6 +5917,159 @@ function getSafeValidationDiagnostic(error) {
5629
5917
  const code = error.code;
5630
5918
  return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
5631
5919
  }
5920
+ function recordLifecycleFailure(span, error, phase) {
5921
+ const diagnostic = createSafeErrorDiagnostic(error);
5922
+ const attributes = {
5923
+ "mcp.failure.phase": phase,
5924
+ "error.type": diagnostic.category,
5925
+ "error.category": diagnostic.category,
5926
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5927
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
5928
+ ...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
5929
+ ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
5930
+ };
5931
+ span.addEvent("mcp.lifecycle.failure", {
5932
+ "error.type": diagnostic.category,
5933
+ "error.category": diagnostic.category,
5934
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {}
5935
+ });
5936
+ recordTelemetryException(error, attributes, span);
5937
+ }
5938
+ const sdkToolNameStorage = new AsyncLocalStorage();
5939
+ const SAFE_TOOL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
5940
+ function getSdkToolName(request) {
5941
+ if (!request || typeof request !== "object") return "unknown";
5942
+ const candidate = request.params?.name;
5943
+ return typeof candidate === "string" && SAFE_TOOL_NAME_PATTERN.test(candidate) ? candidate : "unknown";
5944
+ }
5945
+ function markSdkToolFailure(span, error, toolName = sdkToolNameStorage.getStore() ?? "unknown", errorTypeOverride) {
5946
+ const diagnostic = createSafeErrorDiagnostic(error);
5947
+ const errorType = errorTypeOverride ?? diagnostic.category;
5948
+ span.addEvent("mcp.tool.failure", {
5949
+ "mcp.tool.name": toolName,
5950
+ "error.type": errorType,
5951
+ "error.category": diagnostic.category,
5952
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5953
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
5954
+ ...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
5955
+ ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
5956
+ });
5957
+ span.setAttribute("error.type", errorType);
5958
+ recordTelemetryException(error, {
5959
+ "mcp.failure.phase": "sdk",
5960
+ "error.type": errorType,
5961
+ "error.category": diagnostic.category,
5962
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5963
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {}
5964
+ }, span);
5965
+ span.setStatus({ code: SpanStatusCode.ERROR });
5966
+ }
5967
+ function installSdkErrorTracking(server) {
5968
+ const protocol = server.server;
5969
+ if (!protocol) return;
5970
+ const previous = protocol.onerror;
5971
+ protocol.onerror = (error) => {
5972
+ const diagnostic = createSafeErrorDiagnostic(error);
5973
+ const attributes = {
5974
+ "mcp.failure.phase": "sdk",
5975
+ "error.category": diagnostic.category,
5976
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5977
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {}
5978
+ };
5979
+ const activeSpan = trace.getActiveSpan();
5980
+ const sessionId = getCurrentMcpSessionId();
5981
+ if (activeSpan) {
5982
+ activeSpan.addEvent("mcp.tool.failure", {
5983
+ "mcp.tool.name": sdkToolNameStorage.getStore() ?? "unknown",
5984
+ "error.type": "UNKNOWN_ERROR",
5985
+ "error.category": diagnostic.category,
5986
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {}
5987
+ });
5988
+ recordTelemetryException(error, attributes, activeSpan);
5989
+ } else tracer.startActiveSpan("mcp.sdk.failure", { attributes: { ...sessionId ? { "mcp.session.id": sessionId } : {} } }, (span) => {
5990
+ span.addEvent("mcp.tool.failure", {
5991
+ "mcp.tool.name": "unknown",
5992
+ "error.type": "UNKNOWN_ERROR",
5993
+ "error.category": diagnostic.category,
5994
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {}
5995
+ });
5996
+ recordTelemetryException(error, attributes, span);
5997
+ span.end();
5998
+ });
5999
+ try {
6000
+ previous?.(error);
6001
+ } catch {}
6002
+ };
6003
+ const toolErrorHost = server;
6004
+ const previousCreateToolError = toolErrorHost.createToolError;
6005
+ if (previousCreateToolError) {
6006
+ const createToolError = previousCreateToolError.bind(server);
6007
+ toolErrorHost.createToolError = (message) => {
6008
+ const result = createToolError(message);
6009
+ const activeSpan = trace.getActiveSpan();
6010
+ if (activeSpan) markSdkToolFailure(activeSpan, /* @__PURE__ */ new Error("MCP tool validation failed"), void 0, "VALIDATION_ERROR");
6011
+ return result;
6012
+ };
6013
+ }
6014
+ const handlers = protocol._requestHandlers;
6015
+ if (!(handlers instanceof Map)) return;
6016
+ const toolHandler = handlers.get("tools/call");
6017
+ if (toolHandler) handlers.set("tools/call", (request, extra) => {
6018
+ const sessionId = getCurrentMcpSessionId();
6019
+ const toolName = getSdkToolName(request);
6020
+ return sdkToolNameStorage.run(toolName, () => tracer.startActiveSpan("mcp.sdk.tools.call", { attributes: {
6021
+ "mcp.span.category": "protocol",
6022
+ "mcp.operation.kind": "tool",
6023
+ "mcp.tool.name": toolName,
6024
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
6025
+ } }, async (span) => {
6026
+ try {
6027
+ const result = await toolHandler(request, extra);
6028
+ if (result?.isError === true) {
6029
+ span.setAttribute("mcp.tool.outcome", "returned_error");
6030
+ span.setStatus({ code: SpanStatusCode.ERROR });
6031
+ } else span.setStatus({ code: SpanStatusCode.OK });
6032
+ return result;
6033
+ } catch (error) {
6034
+ markSdkToolFailure(span, error, toolName);
6035
+ throw error;
6036
+ } finally {
6037
+ span.end();
6038
+ }
6039
+ }));
6040
+ });
6041
+ const discoveryHandler = handlers.get("server/discover");
6042
+ if (discoveryHandler) handlers.set("server/discover", (request, extra) => {
6043
+ const sessionId = getCurrentMcpSessionId();
6044
+ return tracer.startActiveSpan("mcp.server.discover", { attributes: {
6045
+ "mcp.span.category": "discovery",
6046
+ "mcp.transport": getCurrentMcpTransport(),
6047
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
6048
+ } }, async (span) => {
6049
+ try {
6050
+ const result = await discoveryHandler(request, extra);
6051
+ span.setStatus({ code: SpanStatusCode.OK });
6052
+ return result;
6053
+ } catch (error) {
6054
+ const diagnostic = createSafeErrorDiagnostic(error);
6055
+ span.addEvent("mcp.discovery.failure", {
6056
+ "error.type": diagnostic.category,
6057
+ "error.category": diagnostic.category,
6058
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {}
6059
+ });
6060
+ recordTelemetryException(error, {
6061
+ "mcp.failure.phase": "discovery",
6062
+ "error.type": diagnostic.category,
6063
+ "error.category": diagnostic.category,
6064
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {}
6065
+ }, span);
6066
+ throw error;
6067
+ } finally {
6068
+ span.end();
6069
+ }
6070
+ });
6071
+ });
6072
+ }
5632
6073
  async function validateApiKey(apiKey) {
5633
6074
  const startupProbeClient = createHevyClient({
5634
6075
  apiKey,
@@ -5647,6 +6088,7 @@ async function validateApiKey(apiKey) {
5647
6088
  }
5648
6089
  function buildServer(apiKey, transport = "stdio") {
5649
6090
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
6091
+ "mcp.span.category": "startup",
5650
6092
  "mcp.server.name": name,
5651
6093
  "mcp.server.version": version,
5652
6094
  "mcp.transport": transport
@@ -5663,12 +6105,15 @@ function buildServer(apiKey, transport = "stdio") {
5663
6105
  recordOutputs: false
5664
6106
  }),
5665
6107
  onToolsRegistered: (count) => span.setAttribute("mcp.tools.count", count),
5666
- observer: createNodeToolObserver()
6108
+ observer: createNodeToolObserver(),
6109
+ cacheObserver: createNodeCacheObserver()
5667
6110
  });
6111
+ installSdkErrorTracking(server);
5668
6112
  console.error("Hevy client initialized with API key");
5669
6113
  span.setStatus({ code: SpanStatusCode.OK });
5670
6114
  return server;
5671
6115
  } catch (e) {
6116
+ recordLifecycleFailure(span, e, "build");
5672
6117
  span.setStatus({ code: SpanStatusCode.ERROR });
5673
6118
  throw e;
5674
6119
  } finally {
@@ -5697,7 +6142,11 @@ async function runStdioServer() {
5697
6142
  const configuredApiKey = process.env.HEVY_API_KEY;
5698
6143
  if (configuredApiKey) setTelemetryUser(configuredApiKey);
5699
6144
  let connectAttempted = false;
5700
- await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
6145
+ let connectSucceeded = false;
6146
+ await tracer.startActiveSpan("mcp.server.run", { attributes: {
6147
+ "mcp.span.category": "startup",
6148
+ "mcp.transport": "stdio"
6149
+ } }, async (span) => {
5701
6150
  try {
5702
6151
  const apiKey = parseConfig(process.env).apiKey;
5703
6152
  assertApiKey(apiKey);
@@ -5705,11 +6154,16 @@ async function runStdioServer() {
5705
6154
  console.error("Starting MCP server in stdio mode");
5706
6155
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
5707
6156
  connectAttempted = true;
5708
- await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
6157
+ await tracer.startActiveSpan("mcp.server.connect", { attributes: {
6158
+ "mcp.span.category": "session",
6159
+ "mcp.transport": "stdio"
6160
+ } }, async (connectSpan) => {
5709
6161
  try {
5710
6162
  await server.connect(transport);
6163
+ connectSucceeded = true;
5711
6164
  connectSpan.setStatus({ code: SpanStatusCode.OK });
5712
6165
  } catch (e) {
6166
+ recordLifecycleFailure(connectSpan, e, "connect");
5713
6167
  connectSpan.setStatus({ code: SpanStatusCode.ERROR });
5714
6168
  throw e;
5715
6169
  } finally {
@@ -5717,8 +6171,8 @@ async function runStdioServer() {
5717
6171
  }
5718
6172
  });
5719
6173
  scheduleUpdateCheck({
5720
- packageName: name$1,
5721
- currentVersion: version$1
6174
+ packageName: serviceName,
6175
+ currentVersion: serviceVersion
5722
6176
  });
5723
6177
  installGracefulShutdown({
5724
6178
  target: server,
@@ -5730,6 +6184,7 @@ async function runStdioServer() {
5730
6184
  });
5731
6185
  span.setStatus({ code: SpanStatusCode.OK });
5732
6186
  } catch (e) {
6187
+ if (!connectAttempted || connectSucceeded) recordLifecycleFailure(span, e, "run");
5733
6188
  recordMcpSessionTermination(connectAttempted ? "connect_failure" : "startup_failure");
5734
6189
  span.setStatus({ code: SpanStatusCode.ERROR });
5735
6190
  cleanupProcessExceptionTracking();
@@ -5756,7 +6211,10 @@ async function runServer() {
5756
6211
  return;
5757
6212
  }
5758
6213
  const cleanupProcessExceptionTracking = installProcessExceptionTracking();
5759
- await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "http" } }, async (span) => {
6214
+ await tracer.startActiveSpan("mcp.server.run", { attributes: {
6215
+ "mcp.span.category": "startup",
6216
+ "mcp.transport": "http"
6217
+ } }, async (span) => {
5760
6218
  let listening = false;
5761
6219
  serverStartups.add(1, { version });
5762
6220
  try {
@@ -5768,8 +6226,8 @@ async function runServer() {
5768
6226
  listening = true;
5769
6227
  console.error(`Starting MCP server in HTTP mode at ${options.host}:${options.port}/mcp`);
5770
6228
  scheduleUpdateCheck({
5771
- packageName: name$1,
5772
- currentVersion: version$1
6229
+ packageName: serviceName,
6230
+ currentVersion: serviceVersion
5773
6231
  });
5774
6232
  installGracefulShutdown({
5775
6233
  target: handle,
@@ -5780,6 +6238,7 @@ async function runServer() {
5780
6238
  });
5781
6239
  span.setStatus({ code: SpanStatusCode.OK });
5782
6240
  } catch (error) {
6241
+ recordLifecycleFailure(span, error, "run");
5783
6242
  recordMcpSessionTermination(listening ? "unknown" : "startup_failure");
5784
6243
  span.setStatus({ code: SpanStatusCode.ERROR });
5785
6244
  cleanupProcessExceptionTracking();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-mcp",
3
- "version": "5.0.3",
3
+ "version": "5.0.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": {
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": "5.0.3",
10
+ "version": "5.0.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "5.0.3",
15
+ "version": "5.0.4",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },