hevy-mcp 5.0.3 → 5.0.5

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.5" };
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-TBD9ENR3.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.5" };
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-TBD9ENR3.mjs";
13
13
  export { createNodeMcpServer, runServer, runStdioServer };
@@ -4,17 +4,17 @@
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.5" };
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] = "f5270278-73ea-40fd-92e2-2ae5127e6780", e._sentryDebugIdIdentifier = "sentry-dbid-f5270278-73ea-40fd-92e2-2ae5127e6780");
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";
@@ -79,16 +79,59 @@ 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 PROCESS_FAILURE_TAXONOMY = {
83
+ uncaughtException: "uncaught_exception",
84
+ unhandledRejection: "unhandled_rejection"
85
+ };
86
+ const SAFE_EXCEPTION_TYPES = /* @__PURE__ */ new Set([
87
+ "AggregateError",
88
+ "DOMException",
89
+ "Error",
90
+ "EvalError",
91
+ "HevyHttpError",
92
+ "RangeError",
93
+ "ReferenceError",
94
+ "SyntaxError",
95
+ "TypeError",
96
+ "URIError",
97
+ "ProtocolError",
98
+ "ZodError"
99
+ ]);
100
+ const SAFE_EXCEPTION_CODES = /* @__PURE__ */ new Set([
101
+ "EAI_AGAIN",
102
+ "ECONNABORTED",
103
+ "ECONNREFUSED",
104
+ "ECONNRESET",
105
+ "ENETUNREACH",
106
+ "ENOTFOUND",
107
+ "ERR_NETWORK",
108
+ "ERR_SOCKET_TIMEOUT",
109
+ "ETIMEDOUT",
110
+ "HEVY_INVALID_ENDPOINT",
111
+ "HEVY_REQUEST_ABORTED",
112
+ "HEVY_RETRY_EXHAUSTED"
113
+ ]);
82
114
  function normalizeTelemetryError(error) {
83
- const name = error instanceof Error && error.name ? error.name : "Exception";
84
- return new Error(name);
115
+ const candidate = error instanceof Error && typeof error.name === "string" ? error.name : void 0;
116
+ return { name: candidate && SAFE_EXCEPTION_TYPES.has(candidate) ? candidate : "UnknownError" };
117
+ }
118
+ function getSafeExceptionCode(error) {
119
+ if (!error || typeof error !== "object" || !("code" in error)) return void 0;
120
+ const code = error.code;
121
+ return typeof code === "string" && SAFE_EXCEPTION_CODES.has(code) ? code : void 0;
85
122
  }
86
123
  function recordTelemetryException(error, attributes, span) {
87
124
  if (!telemetryEnabled) return;
88
125
  try {
89
126
  const target = span ?? trace.getActiveSpan();
90
127
  if (!target) return;
91
- target.recordException(normalizeTelemetryError(error));
128
+ const normalized = normalizeTelemetryError(error);
129
+ target.addEvent("exception", {
130
+ ...attributes,
131
+ "exception.type": normalized.name
132
+ });
133
+ target.setAttribute("exception.type", normalized.name);
134
+ target.setAttribute("error.category", normalized.name);
92
135
  if (attributes) target.setAttributes(attributes);
93
136
  target.setStatus({ code: SpanStatusCode.ERROR });
94
137
  } catch {}
@@ -97,9 +140,19 @@ function installProcessExceptionTracking(processLike = process) {
97
140
  if (!telemetryEnabled) return () => {};
98
141
  const recordProcessException = (source, error) => {
99
142
  try {
100
- tracer.startActiveSpan(`mcp.process.${source}`, (span) => {
101
- recordTelemetryException(error, { "exception.source": source }, span);
102
- span.end();
143
+ tracer.startActiveSpan(`mcp.process.${source}`, { attributes: { "mcp.span.category": "process" } }, (span) => {
144
+ try {
145
+ const code = getSafeExceptionCode(error);
146
+ recordTelemetryException(error, {
147
+ "exception.source": source,
148
+ "mcp.failure.phase": PROCESS_FAILURE_TAXONOMY[source],
149
+ "error.type": "MCP_PROCESS_EXCEPTION",
150
+ "error.category": "McpProcessFailure",
151
+ ...code ? { "error.code": code } : {}
152
+ }, span);
153
+ } finally {
154
+ span.end();
155
+ }
103
156
  });
104
157
  } catch {}
105
158
  };
@@ -116,14 +169,24 @@ function installProcessExceptionTracking(processLike = process) {
116
169
  };
117
170
  }
118
171
  const name$1 = "hevy-mcp";
119
- const version$1 = "5.0.3";
172
+ const version$1 = "5.0.5";
120
173
  const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
121
174
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
122
175
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
123
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@5.0.3`;
176
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `${name$1}@${version$1}`;
177
+ function createServiceInstanceId(generate = randomUUID) {
178
+ try {
179
+ const generated = generate();
180
+ if (generated.length > 0 && generated.length <= 128) return generated;
181
+ } catch {}
182
+ return randomBytes(16).toString("hex");
183
+ }
124
184
  const resource = resourceFromAttributes({
125
185
  "service.name": name$1,
126
- "service.version": version$1
186
+ "service.version": version$1,
187
+ "service.instance.id": createServiceInstanceId(),
188
+ "process.runtime.name": process.release.name,
189
+ "process.runtime.version": process.version
127
190
  });
128
191
  let currentUserHash;
129
192
  var UserHashSpanProcessor = class {
@@ -168,7 +231,8 @@ if (telemetryEnabled) {
168
231
  readers: [new PeriodicExportingMetricReader({
169
232
  exporter: new OTLPMetricExporter({
170
233
  url: `${COLLECTOR_ENDPOINT}/metrics`,
171
- headers: { Authorization: `Bearer ${collectorToken}` }
234
+ headers: { Authorization: `Bearer ${collectorToken}` },
235
+ temporalityPreference: AggregationTemporalityPreference.DELTA
172
236
  }),
173
237
  exportIntervalMillis: 1e4
174
238
  })]
@@ -196,6 +260,8 @@ async function flushTelemetry(timeoutMs = 1e3) {
196
260
  }
197
261
  const tracer = trace.getTracer(name$1);
198
262
  const meter = metrics.getMeter(name$1);
263
+ const serviceName = name$1;
264
+ const serviceVersion = version$1;
199
265
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
200
266
  function setTelemetryUser(apiKey) {
201
267
  if (!telemetryEnabled) return;
@@ -1004,6 +1070,35 @@ const SAFE_STATIC_ENDPOINTS = /* @__PURE__ */ new Set([
1004
1070
  "/v1/workouts/count",
1005
1071
  "/v1/workouts/events"
1006
1072
  ]);
1073
+ const EXPECTED_READ_404_ENDPOINTS = /* @__PURE__ */ new Set([
1074
+ "/v1/body_measurements/:date",
1075
+ "/v1/exercise_history/:exerciseTemplateId",
1076
+ "/v1/exercise_templates/:exerciseTemplateId",
1077
+ "/v1/routine_folders/:folderId",
1078
+ "/v1/routines/:routineId",
1079
+ "/v1/workouts/:workoutId"
1080
+ ]);
1081
+ const EXPECTED_LIST_404_ENDPOINTS = /* @__PURE__ */ new Set([
1082
+ "/v1/body_measurements",
1083
+ "/v1/exercise_templates",
1084
+ "/v1/routine_folders",
1085
+ "/v1/routines",
1086
+ "/v1/workouts",
1087
+ "/v1/workouts/events"
1088
+ ]);
1089
+ const SAFE_OBSERVATION_CODES = /* @__PURE__ */ new Set([
1090
+ "EAI_AGAIN",
1091
+ "ECONNABORTED",
1092
+ "ECONNREFUSED",
1093
+ "ECONNRESET",
1094
+ "ENETUNREACH",
1095
+ "ENOTFOUND",
1096
+ "ERR_NETWORK",
1097
+ "ERR_SOCKET_TIMEOUT",
1098
+ "ETIMEDOUT",
1099
+ HEVY_REQUEST_ABORTED_ERROR_CODE,
1100
+ HEVY_RETRY_EXHAUSTED_ERROR_CODE
1101
+ ]);
1007
1102
  const SAFE_DYNAMIC_ENDPOINTS = [
1008
1103
  ["/v1/body_measurements/", "/v1/body_measurements/:date"],
1009
1104
  ["/v1/exercise_history/", "/v1/exercise_history/:exerciseTemplateId"],
@@ -1049,9 +1144,11 @@ function getRequestContext(config) {
1049
1144
  let endpoint = "unknown";
1050
1145
  if (SAFE_STATIC_ENDPOINTS.has(rawEndpoint)) endpoint = rawEndpoint;
1051
1146
  else endpoint = SAFE_DYNAMIC_ENDPOINTS.find(([prefix]) => rawEndpoint.startsWith(prefix))?.[1] ?? "unknown";
1147
+ const page = config.params !== null && typeof config.params === "object" && "page" in config.params && typeof config.params.page === "number" ? config.params.page : void 0;
1052
1148
  return {
1053
1149
  method,
1054
- endpoint
1150
+ endpoint,
1151
+ page
1055
1152
  };
1056
1153
  }
1057
1154
  function emitClientLog(logger, message) {
@@ -1061,11 +1158,49 @@ function emitClientLog(logger, message) {
1061
1158
  console.error("Failed to emit structured Hevy API log");
1062
1159
  }
1063
1160
  }
1161
+ function emitRequestStart(observer, observation) {
1162
+ try {
1163
+ return observer?.(observation) ?? void 0;
1164
+ } catch {
1165
+ return;
1166
+ }
1167
+ }
1168
+ function finishRequestObservation(scope, observation) {
1169
+ try {
1170
+ scope?.finish(observation);
1171
+ } catch {}
1172
+ }
1064
1173
  function emitRequestObservation(observer, observation) {
1065
1174
  try {
1066
1175
  observer?.(observation);
1067
1176
  } catch {}
1068
1177
  }
1178
+ function runRequestObservation(scope, operation) {
1179
+ if (!scope?.run) return operation();
1180
+ let started = false;
1181
+ const trackedOperation = () => {
1182
+ started = true;
1183
+ return operation();
1184
+ };
1185
+ try {
1186
+ return scope.run(trackedOperation);
1187
+ } catch (error) {
1188
+ if (started) throw error;
1189
+ return operation();
1190
+ }
1191
+ }
1192
+ function emitRetryWait(observer, observation) {
1193
+ try {
1194
+ return observer?.(observation) ?? void 0;
1195
+ } catch {
1196
+ return;
1197
+ }
1198
+ }
1199
+ function finishRetryWait(scope) {
1200
+ try {
1201
+ scope?.finish();
1202
+ } catch {}
1203
+ }
1069
1204
  function parseRetryAfterMs(value) {
1070
1205
  if (!value) return void 0;
1071
1206
  const seconds = Number(value);
@@ -1122,7 +1257,7 @@ function createNativeClient(apiKey, baseUrl, options) {
1122
1257
  ...clientConfig,
1123
1258
  ...config
1124
1259
  };
1125
- const { method, endpoint } = getRequestContext(normalized);
1260
+ const { method, endpoint, page } = getRequestContext(normalized);
1126
1261
  const url = buildUrl(baseUrl, normalized);
1127
1262
  let retryCount = 0;
1128
1263
  while (true) {
@@ -1132,43 +1267,53 @@ function createNativeClient(apiKey, baseUrl, options) {
1132
1267
  code: HEVY_REQUEST_ABORTED_ERROR_CODE
1133
1268
  });
1134
1269
  const startedAt = Date.now();
1270
+ const observationScope = emitRequestStart(options.onRequestStart, {
1271
+ method,
1272
+ endpoint,
1273
+ retryCount
1274
+ });
1135
1275
  const controller = new AbortController();
1136
1276
  const abortFromCaller = () => controller.abort();
1137
1277
  normalized.signal?.addEventListener("abort", abortFromCaller, { once: true });
1138
1278
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
1139
1279
  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
1280
+ return await runRequestObservation(observationScope, async () => {
1281
+ const headers = new Headers({ "api-key": apiKey });
1282
+ if (normalized.data !== void 0 && !(normalized.data instanceof FormData)) headers.set("content-type", "application/json");
1283
+ const attemptDeadline = Date.now() + timeoutMs;
1284
+ const response = await withTimeout(fetchImplementation(url, {
1285
+ method,
1286
+ headers,
1287
+ redirect: "manual",
1288
+ body: normalized.data instanceof FormData ? normalized.data : normalized.data === void 0 ? void 0 : JSON.stringify(normalized.data),
1289
+ signal: controller.signal
1290
+ }), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1291
+ const data = await withTimeout(parseResponseData(response), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1292
+ if (!response.ok) throw new HevyHttpError(`Hevy API request failed (HTTP ${response.status})`, {
1293
+ status: response.status,
1294
+ statusText: response.statusText,
1295
+ data,
1296
+ headers: response.headers,
1297
+ method,
1298
+ endpoint
1299
+ });
1300
+ const observation = {
1301
+ method,
1302
+ endpoint,
1303
+ status: response.status,
1304
+ durationMs: Date.now() - startedAt,
1305
+ retryCount,
1306
+ outcome: "success"
1307
+ };
1308
+ finishRequestObservation(observationScope, observation);
1309
+ emitRequestObservation(options.onRequestComplete, observation);
1310
+ return {
1311
+ data,
1312
+ status: response.status,
1313
+ statusText: response.statusText,
1314
+ headers: response.headers
1315
+ };
1165
1316
  });
1166
- return {
1167
- data,
1168
- status: response.status,
1169
- statusText: response.statusText,
1170
- headers: response.headers
1171
- };
1172
1317
  } catch (cause) {
1173
1318
  const error = isHevyHttpError(cause) ? cause : new HevyHttpError(normalized.signal?.aborted ? "Hevy API request was canceled" : "Hevy API network request failed", {
1174
1319
  method,
@@ -1176,24 +1321,31 @@ function createNativeClient(apiKey, baseUrl, options) {
1176
1321
  code: normalized.signal?.aborted ? HEVY_REQUEST_ABORTED_ERROR_CODE : getNetworkCode(cause),
1177
1322
  cause
1178
1323
  });
1324
+ 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
1325
  const canRetry = method === "GET" && isRetryable(error);
1180
- if (canRetry && retryCount >= maxGetRetries) {
1326
+ const retryExhausted = canRetry && retryCount >= maxGetRetries;
1327
+ if (retryExhausted) {
1181
1328
  error.hevyRetryExhausted = true;
1182
1329
  error.hevyRetryCount = retryCount;
1183
1330
  error.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
1184
1331
  }
1185
- emitRequestObservation(options.onRequestComplete, {
1332
+ const observation = {
1186
1333
  method,
1187
1334
  endpoint,
1188
1335
  status: error.status ?? 0,
1189
1336
  durationMs: Date.now() - startedAt,
1190
1337
  retryCount,
1338
+ outcome: expectedReason ? "expected" : canRetry && !retryExhausted ? "retryable_failure" : "terminal_failure",
1339
+ ...expectedReason ? { expectedReason } : {},
1191
1340
  error: {
1192
1341
  status: error.status,
1193
- code: error.code
1342
+ code: typeof error.code === "string" && SAFE_OBSERVATION_CODES.has(error.code) ? error.code : void 0,
1343
+ category: error.status === void 0 ? "NetworkError" : "HevyHttpError"
1194
1344
  }
1195
- });
1196
- if (!canRetry) {
1345
+ };
1346
+ finishRequestObservation(observationScope, observation);
1347
+ emitRequestObservation(options.onRequestComplete, observation);
1348
+ if (!expectedReason && !canRetry) {
1197
1349
  emitClientLog(options.onLog, {
1198
1350
  level: "error",
1199
1351
  logger: "hevy-api",
@@ -1206,7 +1358,7 @@ function createNativeClient(apiKey, baseUrl, options) {
1206
1358
  });
1207
1359
  throw error;
1208
1360
  }
1209
- if (error.hevyRetryExhausted) throw error;
1361
+ if (expectedReason || error.hevyRetryExhausted) throw error;
1210
1362
  retryCount += 1;
1211
1363
  const delayMs = getRetryDelayMs(error, retryCount);
1212
1364
  emitClientLog(options.onLog, {
@@ -1222,7 +1374,19 @@ function createNativeClient(apiKey, baseUrl, options) {
1222
1374
  endpoint
1223
1375
  }
1224
1376
  });
1225
- await sleep(delayMs);
1377
+ await runRequestObservation(observationScope, async () => {
1378
+ const retryWaitScope = emitRetryWait(options.onRetryWait, {
1379
+ method,
1380
+ endpoint,
1381
+ retryCount,
1382
+ delayMs
1383
+ });
1384
+ try {
1385
+ await sleep(delayMs);
1386
+ } finally {
1387
+ finishRetryWait(retryWaitScope);
1388
+ }
1389
+ });
1226
1390
  } finally {
1227
1391
  clearTimeout(timeout);
1228
1392
  normalized.signal?.removeEventListener("abort", abortFromCaller);
@@ -2680,7 +2844,7 @@ function registerHevyResources(server, runtime) {
2680
2844
  //#endregion
2681
2845
  //#region ../core/src/server-metadata.ts
2682
2846
  const SERVER_NAME = "hevy-mcp";
2683
- const SERVER_VERSION = "5.0.3";
2847
+ const SERVER_VERSION = "5.0.5";
2684
2848
  const SERVER_INSTRUCTIONS = [
2685
2849
  "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
2850
  "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 +4242,7 @@ function createErrorResponse(error, context) {
4078
4242
  axios: axiosErrorContext ?? void 0
4079
4243
  };
4080
4244
  const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${policy.message}`;
4081
- console.error(createMcpToolFailureEvent(context || "unknown", policy.type, diagnostic));
4245
+ console.error(JSON.stringify(createMcpToolFailureEvent(context || "unknown", policy.type, diagnostic)));
4082
4246
  return {
4083
4247
  content: [{
4084
4248
  type: "text",
@@ -4240,39 +4404,69 @@ var AsyncTtlCache = class {
4240
4404
  ttlMs;
4241
4405
  maxSize;
4242
4406
  now;
4407
+ observer;
4243
4408
  entries = /* @__PURE__ */ new Map();
4244
4409
  inFlight = /* @__PURE__ */ new Map();
4245
4410
  requestCounter = 0;
4246
4411
  constructor(options, now = Date.now) {
4247
- const { ttlMs, maxSize } = options;
4412
+ const { ttlMs, maxSize, observer } = options;
4248
4413
  if (ttlMs <= 0) throw new Error("Cache ttlMs must be greater than 0.");
4249
4414
  if (maxSize <= 0) throw new Error("Cache maxSize must be greater than 0.");
4250
4415
  this.ttlMs = ttlMs;
4251
4416
  this.maxSize = maxSize;
4252
4417
  this.now = now;
4418
+ this.observer = observer;
4419
+ }
4420
+ startObservation(state) {
4421
+ try {
4422
+ return this.observer?.start({ state }) ?? void 0;
4423
+ } catch {
4424
+ return;
4425
+ }
4426
+ }
4427
+ finishObservation(scope, metadata) {
4428
+ try {
4429
+ scope?.finish(metadata);
4430
+ } catch {}
4253
4431
  }
4254
4432
  async getOrFetch(key, fetcher, options = {}) {
4255
- const { refresh = false } = options;
4433
+ const { refresh = false, getObservationMetadata } = options;
4434
+ let observationState = refresh ? "refresh" : "miss";
4256
4435
  if (refresh) this.invalidate(key);
4257
4436
  else {
4258
4437
  const cachedEntry = this.entries.get(key);
4259
4438
  if (cachedEntry !== void 0) {
4260
4439
  if (cachedEntry.expiresAt > this.now()) {
4261
4440
  this.markAsRecentlyUsed(key, cachedEntry);
4441
+ const observationScope = this.startObservation("hit");
4442
+ this.finishObservation(observationScope);
4262
4443
  return cachedEntry.value;
4263
4444
  }
4264
4445
  this.entries.delete(key);
4446
+ observationState = "expired";
4265
4447
  }
4266
4448
  const inFlightEntry = this.inFlight.get(key);
4267
- if (inFlightEntry !== void 0) return inFlightEntry.promise;
4449
+ if (inFlightEntry !== void 0) {
4450
+ const observationScope = this.startObservation("inflight_wait");
4451
+ inFlightEntry.promise.then(() => this.finishObservation(observationScope), () => this.finishObservation(observationScope));
4452
+ return inFlightEntry.promise;
4453
+ }
4268
4454
  }
4269
4455
  const requestId = ++this.requestCounter;
4456
+ const observationScope = this.startObservation(observationState);
4270
4457
  const request = (async () => {
4271
4458
  try {
4272
4459
  const value = await fetcher();
4273
4460
  if (this.isCurrentRequest(key, requestId)) this.setValue(key, value);
4274
4461
  return value;
4275
4462
  } finally {
4463
+ let metadata;
4464
+ try {
4465
+ metadata = getObservationMetadata?.();
4466
+ } catch {
4467
+ metadata = void 0;
4468
+ }
4469
+ this.finishObservation(observationScope, metadata);
4276
4470
  if (this.inFlight.get(key)?.requestId === requestId) this.inFlight.delete(key);
4277
4471
  }
4278
4472
  })();
@@ -4321,17 +4515,20 @@ var AsyncTtlCache = class {
4321
4515
  const EXERCISE_TEMPLATE_CATALOG_CACHE_KEY = "exercise-template-catalog";
4322
4516
  const EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS = 300 * 1e3;
4323
4517
  const EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE = 1;
4324
- /** Create a cache owned by one MCP server/request lifecycle. */
4325
- function createExerciseTemplateCatalog(hevyClient) {
4518
+ function createExerciseTemplateCatalog(hevyClient, cacheObserver) {
4326
4519
  const cache = new AsyncTtlCache({
4327
4520
  ttlMs: EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS,
4328
- maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE
4521
+ maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE,
4522
+ observer: cacheObserver
4329
4523
  });
4330
4524
  return {
4331
4525
  get(options = {}) {
4332
4526
  const reason = options.refresh ? "explicit-refresh" : cache.size === 0 ? "initial-load" : "ttl-expired";
4527
+ let observationMetadata;
4528
+ let pagesLoaded = 0;
4333
4529
  return cache.getOrFetch(EXERCISE_TEMPLATE_CATALOG_CACHE_KEY, async () => {
4334
4530
  const catalog = await fetchAllPages(async (page, pageSize) => {
4531
+ pagesLoaded = page;
4335
4532
  const data = await hevyClient.getExerciseTemplates({
4336
4533
  page,
4337
4534
  pageSize
@@ -4341,9 +4538,17 @@ function createExerciseTemplateCatalog(hevyClient) {
4341
4538
  pageCount: data?.page_count
4342
4539
  };
4343
4540
  }, 100);
4541
+ observationMetadata = {
4542
+ refreshReason: reason,
4543
+ pageCountBucket: bucketCount(pagesLoaded),
4544
+ itemCountBucket: bucketCount(catalog.length)
4545
+ };
4344
4546
  options.onRefreshed?.(catalog, reason);
4345
4547
  return catalog;
4346
- }, options);
4548
+ }, {
4549
+ refresh: options.refresh,
4550
+ getObservationMetadata: () => observationMetadata
4551
+ });
4347
4552
  },
4348
4553
  reset() {
4349
4554
  cache.clear();
@@ -4405,7 +4610,7 @@ function createHevyMcpServer(options) {
4405
4610
  const client = options.createClient({ onLog: (event) => mcpLogger(event) });
4406
4611
  const runtime = createToolRuntime({
4407
4612
  client,
4408
- catalog: createExerciseTemplateCatalog(client),
4613
+ catalog: createExerciseTemplateCatalog(client, options.cacheObserver),
4409
4614
  logger: mcpLogger,
4410
4615
  observer: options.observer
4411
4616
  });
@@ -4530,13 +4735,15 @@ function extractMcpClientMetadata(message) {
4530
4735
  protocolVersion: normalizeMetadata(params.protocolVersion)
4531
4736
  };
4532
4737
  }
4533
- function createMcpSessionContext(message, transport = "stdio") {
4738
+ function createMcpSessionContext(message, transport = "stdio", options = {}) {
4739
+ const normalizedOptions = typeof options === "function" ? { generateTelemetrySessionId: options } : options;
4534
4740
  return {
4535
4741
  metadata: extractMcpClientMetadata(message),
4536
- startedAt: Date.now(),
4742
+ startedAt: (normalizedOptions.now ?? Date.now)(),
4537
4743
  toolCalls: 0,
4538
4744
  hadToolFailure: false,
4539
- transport
4745
+ transport,
4746
+ telemetrySessionId: normalizedOptions.telemetrySessionId ?? (normalizedOptions.generateTelemetrySessionId ?? randomUUID)()
4540
4747
  };
4541
4748
  }
4542
4749
  function runWithMcpSessionContext(context, callback) {
@@ -4545,6 +4752,9 @@ function runWithMcpSessionContext(context, callback) {
4545
4752
  function getSession() {
4546
4753
  return contextStorage.getStore() ?? activeStdioSession;
4547
4754
  }
4755
+ function getCurrentMcpSessionId() {
4756
+ return getSession()?.telemetrySessionId;
4757
+ }
4548
4758
  function metadataAttributes(metadata, transport) {
4549
4759
  return {
4550
4760
  client_name: metadata.name,
@@ -4985,6 +5195,38 @@ function debugLog(event, data) {
4985
5195
  }
4986
5196
  //#endregion
4987
5197
  //#region src/utils/hevy-client-observability.ts
5198
+ function safeErrorAttributes(observation) {
5199
+ if (!observation.error) return {};
5200
+ const code = observation.error.code;
5201
+ return {
5202
+ error_category: observation.error.category ?? "HevyHttpError",
5203
+ ...code && SAFE_OBSERVATION_CODES.has(code) ? { error_code: code } : {}
5204
+ };
5205
+ }
5206
+ function startApiSpan(start) {
5207
+ const sessionId = getCurrentMcpSessionId();
5208
+ return tracer.startSpan(`hevy.api.${start.method}`, { attributes: {
5209
+ "mcp.span.category": "api",
5210
+ "http.request.method": start.method,
5211
+ "hevy.api.endpoint": start.endpoint,
5212
+ "hevy.api.retry_count_bucket": bucketCount(start.retryCount),
5213
+ "mcp.transport": getCurrentMcpTransport(),
5214
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5215
+ } });
5216
+ }
5217
+ function finishApiSpan(span, observation) {
5218
+ const errorAttributes = safeErrorAttributes(observation);
5219
+ if (observation.status > 0) span.setAttribute("http.response.status_code", observation.status);
5220
+ span.setAttribute("hevy.api.outcome", observation.outcome);
5221
+ if (observation.expectedReason) span.setAttribute("hevy.api.expected_reason", observation.expectedReason);
5222
+ for (const [key, value] of Object.entries(errorAttributes)) span.setAttribute(key, value);
5223
+ span.setStatus({ code: observation.outcome === "success" || observation.outcome === "expected" ? SpanStatusCode.OK : SpanStatusCode.ERROR });
5224
+ if (observation.outcome !== "success" && observation.outcome !== "expected") span.addEvent("hevy.api.failure", {
5225
+ "error.category": errorAttributes.error_category ?? "HevyHttpError",
5226
+ ...errorAttributes.error_code ? { "error.code": errorAttributes.error_code } : {}
5227
+ });
5228
+ span.end();
5229
+ }
4988
5230
  /** Node-only adapter; the Worker graph never imports telemetry or metrics. */
4989
5231
  function createNodeHevyClientOptions() {
4990
5232
  const rawTimeout = process.env.HEVY_MCP_API_TIMEOUT;
@@ -4992,37 +5234,38 @@ function createNodeHevyClientOptions() {
4992
5234
  const timeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? Math.floor(parsedTimeout) : void 0;
4993
5235
  return {
4994
5236
  ...timeoutMs ? { timeoutMs } : {},
5237
+ onRequestStart(start) {
5238
+ const span = startApiSpan(start);
5239
+ return {
5240
+ run(operation) {
5241
+ return context.with(trace.setSpan(context.active(), span), operation);
5242
+ },
5243
+ finish(observation) {
5244
+ finishApiSpan(span, observation);
5245
+ }
5246
+ };
5247
+ },
4995
5248
  onRequestComplete(observation) {
4996
5249
  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();
5250
+ const errorAttributes = safeErrorAttributes(observation);
5014
5251
  apiCalls.add(1, {
5015
5252
  method: observation.method,
5016
5253
  endpoint: observation.endpoint,
5017
5254
  status_code: observation.status,
5018
5255
  retry_count_bucket: retryCountBucket,
5019
- ...safeErrorAttributes
5256
+ outcome: observation.outcome,
5257
+ transport: getCurrentMcpTransport(),
5258
+ ...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
5259
+ ...errorAttributes
5020
5260
  });
5021
5261
  apiDuration.record(observation.durationMs, {
5022
5262
  method: observation.method,
5023
5263
  endpoint: observation.endpoint,
5024
5264
  retry_count_bucket: retryCountBucket,
5025
- ...safeErrorAttributes
5265
+ outcome: observation.outcome,
5266
+ transport: getCurrentMcpTransport(),
5267
+ ...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
5268
+ ...errorAttributes
5026
5269
  });
5027
5270
  debugLog("api_response", {
5028
5271
  method: observation.method,
@@ -5030,13 +5273,43 @@ function createNodeHevyClientOptions() {
5030
5273
  durationMs: observation.durationMs,
5031
5274
  status: observation.status || null,
5032
5275
  retryCountBucket,
5033
- ...safeErrorAttributes
5276
+ outcome: observation.outcome,
5277
+ ...errorAttributes
5034
5278
  });
5279
+ },
5280
+ onRetryWait(observation) {
5281
+ const sessionId = getCurrentMcpSessionId();
5282
+ const span = tracer.startSpan("hevy.api.retry_wait", { attributes: {
5283
+ "mcp.span.category": "api",
5284
+ "http.request.method": observation.method,
5285
+ "hevy.api.retry_count_bucket": bucketCount(observation.retryCount),
5286
+ "hevy.api.retry_wait_ms": Math.max(0, Math.min(5e3, observation.delayMs)),
5287
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5288
+ } });
5289
+ return { finish: () => span.end() };
5035
5290
  }
5036
5291
  };
5037
5292
  }
5293
+ function createNodeCacheObserver() {
5294
+ return { start(observation) {
5295
+ const sessionId = getCurrentMcpSessionId();
5296
+ const span = tracer.startSpan(`hevy.cache.${observation.state}`, { attributes: {
5297
+ "mcp.span.category": "cache",
5298
+ "hevy.cache.state": observation.state,
5299
+ "mcp.transport": getCurrentMcpTransport(),
5300
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5301
+ } });
5302
+ return { finish(metadata) {
5303
+ if (metadata?.refreshReason) span.setAttribute("hevy.cache.refresh_reason", metadata.refreshReason);
5304
+ if (metadata?.pageCountBucket) span.setAttribute("hevy.cache.page_count_bucket", metadata.pageCountBucket);
5305
+ if (metadata?.itemCountBucket) span.setAttribute("hevy.cache.item_count_bucket", metadata.itemCountBucket);
5306
+ span.end();
5307
+ } };
5308
+ } };
5309
+ }
5038
5310
  //#endregion
5039
5311
  //#region src/utils/tool-observer.ts
5312
+ const DISCOVERY_TOOL_NAMES = /* @__PURE__ */ new Set(["search-routines"]);
5040
5313
  const WORKFLOW_PAGINATION_RESOURCES = /* @__PURE__ */ new Set([
5041
5314
  "workouts",
5042
5315
  "bodyMeasurements",
@@ -5059,8 +5332,11 @@ function metricAttributes(invocation, clientAttributes) {
5059
5332
  }
5060
5333
  function createAttributes(invocation) {
5061
5334
  const clientMetadata = getCurrentMcpClientMetadata();
5335
+ const isPrompt = invocation.kind === "prompt";
5336
+ const sessionId = getCurrentMcpSessionId();
5062
5337
  const attributes = {
5063
- [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5338
+ "mcp.span.category": DISCOVERY_TOOL_NAMES.has(invocation.name) ? "discovery" : "tool",
5339
+ [isPrompt ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5064
5340
  "mcp.operation.kind": invocation.kind ?? "tool",
5065
5341
  ...taxonomyAttributes(invocation),
5066
5342
  "mcp.client.name": clientMetadata.name,
@@ -5068,7 +5344,8 @@ function createAttributes(invocation) {
5068
5344
  "mcp.protocol.version": clientMetadata.protocolVersion,
5069
5345
  "mcp.transport": getCurrentMcpTransport(),
5070
5346
  "mcp.tool.args.key_count_bucket": invocation.argumentKeyCountBucket ?? "unknown",
5071
- "mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? ""
5347
+ "mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? "",
5348
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5072
5349
  };
5073
5350
  for (const key of Object.keys(invocation.argumentPresence ?? {})) attributes[`mcp.tool.args.${key}.present`] = true;
5074
5351
  for (const [key, bucket] of Object.entries(invocation.numericArgumentBuckets ?? {})) if (bucket !== void 0) attributes[`mcp.tool.args.${key}.bucket`] = bucket;
@@ -5106,14 +5383,16 @@ function resultMetricAttributes(completion) {
5106
5383
  if (summary?.setCountBucket) attributes["mcp.tool.result.set_count_bucket"] = summary.setCountBucket;
5107
5384
  return attributes;
5108
5385
  }
5109
- function setSafeErrorAttributes(span, completion) {
5386
+ function setSafeErrorAttributes(span, invocation, completion) {
5110
5387
  const diagnostic = completion.error;
5111
5388
  const errorType = completion.errorType ?? "UNKNOWN_ERROR";
5112
5389
  if (diagnostic) span.addEvent("mcp.tool.failure", {
5390
+ "mcp.tool.name": invocation.name,
5391
+ "error.type": errorType,
5113
5392
  "error.category": diagnostic.category,
5114
5393
  ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5115
- ...diagnostic.status !== void 0 ? { "http.status_code": diagnostic.status } : {},
5116
- ...diagnostic.method ? { "http.method": diagnostic.method } : {},
5394
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
5395
+ ...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
5117
5396
  ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
5118
5397
  });
5119
5398
  span.setAttribute("error.type", errorType);
@@ -5130,8 +5409,8 @@ function captureSafeToolFailure(invocation, completion) {
5130
5409
  scope.setTag("error.type", completion.errorType ?? "UNKNOWN_ERROR");
5131
5410
  scope.setTag("error.category", category);
5132
5411
  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);
5412
+ if (diagnostic?.status !== void 0) scope.setTag("http.response.status_code", String(diagnostic.status));
5413
+ if (diagnostic?.method) scope.setTag("http.request.method", diagnostic.method);
5135
5414
  if (diagnostic?.endpoint) scope.setTag("hevy.api.endpoint", diagnostic.endpoint);
5136
5415
  scope.setContext(invocation.kind === "prompt" ? "mcpPrompt" : "mcpTool", {
5137
5416
  context: invocation.name,
@@ -5185,26 +5464,35 @@ function createNodeToolObserver() {
5185
5464
  activeSpan.setAttribute("mcp.tool.outcome", nextCompletion.outcome);
5186
5465
  setResultAttributes(activeSpan, nextCompletion);
5187
5466
  if (nextCompletion.outcome === "thrown_error") {
5188
- errorType = setSafeErrorAttributes(activeSpan, nextCompletion);
5467
+ errorType = setSafeErrorAttributes(activeSpan, invocation, nextCompletion);
5189
5468
  bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? errorType), {
5190
5469
  [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5191
5470
  "error.type": errorType ?? "UNKNOWN_ERROR",
5192
5471
  "error.category": nextCompletion.error?.category ?? "UnknownError",
5193
5472
  ...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 } : {},
5473
+ ...nextCompletion.error?.status !== void 0 ? { "http.response.status_code": nextCompletion.error.status } : {},
5474
+ ...nextCompletion.error?.method ? { "http.request.method": nextCompletion.error.method } : {},
5475
+ ...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
5476
+ }, activeSpan));
5477
+ }
5478
+ if (nextCompletion.outcome === "returned_error") {
5479
+ activeSpan.addEvent("mcp.tool.failure", {
5480
+ "mcp.tool.name": invocation.name,
5481
+ "error.type": "UNKNOWN_ERROR",
5482
+ "error.category": "McpToolReturnedError",
5483
+ "error.code": "MCP_TOOL_RETURNED_ERROR"
5484
+ });
5485
+ activeSpan.setAttribute("error.type", "UNKNOWN_ERROR");
5486
+ bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? "UnknownError"), {
5487
+ [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5488
+ "error.type": nextCompletion.errorType ?? "UNKNOWN_ERROR",
5489
+ "error.category": nextCompletion.error?.category ?? "UnknownError",
5490
+ ...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
5491
+ ...nextCompletion.error?.status !== void 0 ? { "http.response.status_code": nextCompletion.error.status } : {},
5492
+ ...nextCompletion.error?.method ? { "http.request.method": nextCompletion.error.method } : {},
5196
5493
  ...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
5197
5494
  }, activeSpan));
5198
5495
  }
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
5496
  }
5209
5497
  } catch {} finally {
5210
5498
  if (nextCompletion.outcome === "thrown_error") {
@@ -5385,14 +5673,17 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
5385
5673
  const lineHadLeadingBom = line.startsWith(UTF8_BOM);
5386
5674
  const normalizedLine = lineHadLeadingBom ? line.slice(1) : line;
5387
5675
  const lineByteLength = Buffer.byteLength(line);
5676
+ const sessionId = getCurrentMcpSessionId();
5388
5677
  return tracer.startActiveSpan("mcp.stdio.deserialize", { attributes: {
5678
+ "mcp.span.category": "protocol",
5389
5679
  "mcp.transport": "stdio",
5390
5680
  "mcp.stdio.parse.line.char_length": line.length,
5391
5681
  "mcp.stdio.parse.line.byte_length": lineByteLength,
5392
5682
  "mcp.stdio.parse.line.had_leading_bom": lineHadLeadingBom,
5393
5683
  "mcp.stdio.parse.line.bom_stripped": lineHadLeadingBom,
5394
5684
  "mcp.stdio.parse.chunk.last_byte_length": chunkSnapshot.lastChunkByteLength,
5395
- "mcp.stdio.parse.chunk.last_had_utf8_bom": chunkSnapshot.lastChunkStartsWithUtf8Bom
5685
+ "mcp.stdio.parse.chunk.last_had_utf8_bom": chunkSnapshot.lastChunkStartsWithUtf8Bom,
5686
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5396
5687
  } }, (span) => {
5397
5688
  try {
5398
5689
  const message = deserializeMessage(normalizedLine);
@@ -5402,6 +5693,8 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
5402
5693
  if (typeof method === "string" && SAFE_MCP_METHODS[method] === true) span.setAttribute("mcp.method", method);
5403
5694
  if (method === "initialize") {
5404
5695
  const client = recordMcpSessionStart(message);
5696
+ const sessionId = getCurrentMcpSessionId();
5697
+ if (sessionId) span.setAttribute("mcp.session.id", sessionId);
5405
5698
  span.setAttribute("mcp.client.name", client.name);
5406
5699
  span.setAttribute("mcp.client.version", client.version);
5407
5700
  span.setAttribute("mcp.protocol.version", client.protocolVersion);
@@ -5440,6 +5733,177 @@ function createInstrumentedStdioTransport(transport) {
5440
5733
  return transport;
5441
5734
  }
5442
5735
  //#endregion
5736
+ //#region src/utils/sdk-observability.ts
5737
+ const sdkToolNameStorage = new AsyncLocalStorage();
5738
+ const SAFE_TOOL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
5739
+ const SDK_FAILURE_TAXONOMY = {
5740
+ protocol: {
5741
+ errorType: "UNKNOWN_ERROR",
5742
+ errorCategory: "McpSdkProtocolFailure"
5743
+ },
5744
+ tool_call: {
5745
+ errorType: "UNKNOWN_ERROR",
5746
+ errorCategory: "McpSdkToolCallFailure"
5747
+ },
5748
+ validation: {
5749
+ errorType: "VALIDATION_ERROR",
5750
+ errorCategory: "McpSdkValidationFailure"
5751
+ }
5752
+ };
5753
+ function createFailureAttributes(error, phase, errorType, errorCategory) {
5754
+ const diagnostic = createSafeErrorDiagnostic(error);
5755
+ return {
5756
+ "mcp.failure.phase": phase,
5757
+ "error.type": errorType,
5758
+ "error.category": errorCategory,
5759
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5760
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
5761
+ ...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
5762
+ ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
5763
+ };
5764
+ }
5765
+ function enrichActiveSdkSpan(attributes) {
5766
+ try {
5767
+ trace.getActiveSpan()?.setAttributes(attributes);
5768
+ } catch {}
5769
+ }
5770
+ function getSdkToolName(request) {
5771
+ if (!request || typeof request !== "object") return "unknown";
5772
+ const candidate = request.params?.name;
5773
+ return typeof candidate === "string" && SAFE_TOOL_NAME_PATTERN.test(candidate) ? candidate : "unknown";
5774
+ }
5775
+ function markSdkToolFailure(span, error, toolName = sdkToolNameStorage.getStore() ?? "unknown", kind = "tool_call") {
5776
+ const taxonomy = SDK_FAILURE_TAXONOMY[kind];
5777
+ const attributes = createFailureAttributes(error, "sdk", taxonomy.errorType, taxonomy.errorCategory);
5778
+ span.addEvent("mcp.tool.failure", {
5779
+ "mcp.tool.name": toolName,
5780
+ ...attributes
5781
+ });
5782
+ span.setAttribute("error.type", taxonomy.errorType);
5783
+ recordTelemetryException(error, attributes, span);
5784
+ span.setStatus({ code: SpanStatusCode.ERROR });
5785
+ }
5786
+ function installProtocolErrorTracking(server, transport) {
5787
+ const protocol = server.server;
5788
+ if (!protocol) return;
5789
+ const previous = protocol.onerror;
5790
+ protocol.onerror = (error) => {
5791
+ const taxonomy = SDK_FAILURE_TAXONOMY.protocol;
5792
+ const attributes = createFailureAttributes(error, "sdk", taxonomy.errorType, taxonomy.errorCategory);
5793
+ const activeSpan = trace.getActiveSpan();
5794
+ const sessionId = getCurrentMcpSessionId();
5795
+ if (activeSpan) {
5796
+ activeSpan.addEvent("mcp.tool.failure", {
5797
+ "mcp.tool.name": sdkToolNameStorage.getStore() ?? "unknown",
5798
+ ...attributes
5799
+ });
5800
+ recordTelemetryException(error, attributes, activeSpan);
5801
+ } else tracer.startActiveSpan("mcp.sdk.failure", { attributes: {
5802
+ "mcp.span.category": "protocol",
5803
+ "mcp.transport": transport,
5804
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5805
+ } }, (span) => {
5806
+ span.addEvent("mcp.tool.failure", {
5807
+ "mcp.tool.name": "unknown",
5808
+ ...attributes
5809
+ });
5810
+ recordTelemetryException(error, attributes, span);
5811
+ span.end();
5812
+ });
5813
+ try {
5814
+ previous?.(error);
5815
+ } catch {}
5816
+ };
5817
+ }
5818
+ function installValidationErrorTracking(server) {
5819
+ const toolErrorHost = server;
5820
+ const previousCreateToolError = toolErrorHost.createToolError;
5821
+ if (!previousCreateToolError) return;
5822
+ const createToolError = previousCreateToolError.bind(server);
5823
+ toolErrorHost.createToolError = (message) => {
5824
+ const result = createToolError(message);
5825
+ const activeSpan = trace.getActiveSpan();
5826
+ if (activeSpan) markSdkToolFailure(activeSpan, /* @__PURE__ */ new Error("MCP tool validation failed"), void 0, "validation");
5827
+ return result;
5828
+ };
5829
+ }
5830
+ function installInitializeEnrichment(handlers, transport) {
5831
+ const initializeHandler = handlers.get("initialize");
5832
+ if (!initializeHandler) return;
5833
+ handlers.set("initialize", (request, extra) => {
5834
+ enrichActiveSdkSpan({
5835
+ "mcp.span.category": "protocol",
5836
+ "mcp.transport": transport
5837
+ });
5838
+ return initializeHandler(request, extra);
5839
+ });
5840
+ }
5841
+ function installToolCallTracking(handlers, transport) {
5842
+ const toolHandler = handlers.get("tools/call");
5843
+ if (!toolHandler) return;
5844
+ handlers.set("tools/call", (request, extra) => {
5845
+ const sessionId = getCurrentMcpSessionId();
5846
+ const toolName = getSdkToolName(request);
5847
+ const attributes = {
5848
+ "mcp.span.category": "protocol",
5849
+ "mcp.transport": transport,
5850
+ "mcp.operation.kind": "tool",
5851
+ "mcp.tool.name": toolName,
5852
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5853
+ };
5854
+ enrichActiveSdkSpan(attributes);
5855
+ return sdkToolNameStorage.run(toolName, () => tracer.startActiveSpan("mcp.sdk.tools.call", { attributes }, async (span) => {
5856
+ try {
5857
+ const result = await toolHandler(request, extra);
5858
+ if (result?.isError === true) {
5859
+ span.setAttribute("mcp.tool.outcome", "returned_error");
5860
+ span.setStatus({ code: SpanStatusCode.ERROR });
5861
+ } else span.setStatus({ code: SpanStatusCode.OK });
5862
+ return result;
5863
+ } catch (error) {
5864
+ markSdkToolFailure(span, error, toolName);
5865
+ throw error;
5866
+ } finally {
5867
+ span.end();
5868
+ }
5869
+ }));
5870
+ });
5871
+ }
5872
+ function installDiscoveryTracking(handlers) {
5873
+ const discoveryHandler = handlers.get("server/discover");
5874
+ if (!discoveryHandler) return;
5875
+ handlers.set("server/discover", (request, extra) => {
5876
+ const sessionId = getCurrentMcpSessionId();
5877
+ return tracer.startActiveSpan("mcp.server.discover", { attributes: {
5878
+ "mcp.span.category": "discovery",
5879
+ "mcp.transport": getCurrentMcpTransport(),
5880
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5881
+ } }, async (span) => {
5882
+ try {
5883
+ const result = await discoveryHandler(request, extra);
5884
+ span.setStatus({ code: SpanStatusCode.OK });
5885
+ return result;
5886
+ } catch (error) {
5887
+ const attributes = createFailureAttributes(error, "discovery", "UNKNOWN_ERROR", "McpServerDiscoveryFailure");
5888
+ span.addEvent("mcp.discovery.failure", attributes);
5889
+ recordTelemetryException(error, attributes, span);
5890
+ throw error;
5891
+ } finally {
5892
+ span.end();
5893
+ }
5894
+ });
5895
+ });
5896
+ }
5897
+ function installSdkErrorTracking(server, transport) {
5898
+ installProtocolErrorTracking(server, transport);
5899
+ installValidationErrorTracking(server);
5900
+ const handlers = server.server?._requestHandlers;
5901
+ if (!(handlers instanceof Map)) return;
5902
+ installInitializeEnrichment(handlers, transport);
5903
+ installToolCallTracking(handlers, transport);
5904
+ installDiscoveryTracking(handlers);
5905
+ }
5906
+ //#endregion
5443
5907
  //#region src/utils/version-check.ts
5444
5908
  const CACHE_TTL_MS = 1440 * 60 * 1e3;
5445
5909
  const REQUEST_TIMEOUT_MS = 3e3;
@@ -5568,8 +6032,8 @@ function scheduleUpdateCheck(options, overrides = {}) {
5568
6032
  }
5569
6033
  //#endregion
5570
6034
  //#region src/index.ts
5571
- const name = name$1;
5572
- const version = version$1;
6035
+ const name = serviceName;
6036
+ const version = serviceVersion;
5573
6037
  const HELP_TEXT = [
5574
6038
  "Usage:",
5575
6039
  " hevy-mcp [options]",
@@ -5629,6 +6093,46 @@ function getSafeValidationDiagnostic(error) {
5629
6093
  const code = error.code;
5630
6094
  return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
5631
6095
  }
6096
+ const LIFECYCLE_FAILURE_TAXONOMY = {
6097
+ config: {
6098
+ errorType: "MCP_SERVER_CONFIG_ERROR",
6099
+ errorCategory: "McpServerConfigFailure"
6100
+ },
6101
+ api_key_validation: {
6102
+ errorType: "MCP_API_KEY_VALIDATION_ERROR",
6103
+ errorCategory: "McpApiKeyValidationFailure"
6104
+ },
6105
+ build: {
6106
+ errorType: "MCP_SERVER_BUILD_ERROR",
6107
+ errorCategory: "McpServerBuildFailure"
6108
+ },
6109
+ connect: {
6110
+ errorType: "MCP_TRANSPORT_CONNECT_ERROR",
6111
+ errorCategory: "McpTransportConnectFailure"
6112
+ },
6113
+ run: {
6114
+ errorType: "MCP_SERVER_RUN_ERROR",
6115
+ errorCategory: "McpServerRunFailure"
6116
+ }
6117
+ };
6118
+ function createLifecycleFailureAttributes(error, phase) {
6119
+ const diagnostic = createSafeErrorDiagnostic(error);
6120
+ const taxonomy = LIFECYCLE_FAILURE_TAXONOMY[phase];
6121
+ return {
6122
+ "mcp.failure.phase": phase,
6123
+ "error.type": taxonomy.errorType,
6124
+ "error.category": taxonomy.errorCategory,
6125
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
6126
+ ...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
6127
+ ...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
6128
+ ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
6129
+ };
6130
+ }
6131
+ function recordLifecycleFailure(span, error, phase) {
6132
+ const attributes = createLifecycleFailureAttributes(error, phase);
6133
+ span.addEvent("mcp.lifecycle.failure", attributes);
6134
+ recordTelemetryException(error, attributes, span);
6135
+ }
5632
6136
  async function validateApiKey(apiKey) {
5633
6137
  const startupProbeClient = createHevyClient({
5634
6138
  apiKey,
@@ -5647,6 +6151,7 @@ async function validateApiKey(apiKey) {
5647
6151
  }
5648
6152
  function buildServer(apiKey, transport = "stdio") {
5649
6153
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
6154
+ "mcp.span.category": "startup",
5650
6155
  "mcp.server.name": name,
5651
6156
  "mcp.server.version": version,
5652
6157
  "mcp.transport": transport
@@ -5663,12 +6168,15 @@ function buildServer(apiKey, transport = "stdio") {
5663
6168
  recordOutputs: false
5664
6169
  }),
5665
6170
  onToolsRegistered: (count) => span.setAttribute("mcp.tools.count", count),
5666
- observer: createNodeToolObserver()
6171
+ observer: createNodeToolObserver(),
6172
+ cacheObserver: createNodeCacheObserver()
5667
6173
  });
6174
+ installSdkErrorTracking(server, transport);
5668
6175
  console.error("Hevy client initialized with API key");
5669
6176
  span.setStatus({ code: SpanStatusCode.OK });
5670
6177
  return server;
5671
6178
  } catch (e) {
6179
+ recordLifecycleFailure(span, e, "build");
5672
6180
  span.setStatus({ code: SpanStatusCode.ERROR });
5673
6181
  throw e;
5674
6182
  } finally {
@@ -5697,7 +6205,11 @@ async function runStdioServer() {
5697
6205
  const configuredApiKey = process.env.HEVY_API_KEY;
5698
6206
  if (configuredApiKey) setTelemetryUser(configuredApiKey);
5699
6207
  let connectAttempted = false;
5700
- await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
6208
+ let connectSucceeded = false;
6209
+ await tracer.startActiveSpan("mcp.server.run", { attributes: {
6210
+ "mcp.span.category": "startup",
6211
+ "mcp.transport": "stdio"
6212
+ } }, async (span) => {
5701
6213
  try {
5702
6214
  const apiKey = parseConfig(process.env).apiKey;
5703
6215
  assertApiKey(apiKey);
@@ -5705,11 +6217,16 @@ async function runStdioServer() {
5705
6217
  console.error("Starting MCP server in stdio mode");
5706
6218
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
5707
6219
  connectAttempted = true;
5708
- await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
6220
+ await tracer.startActiveSpan("mcp.server.connect", { attributes: {
6221
+ "mcp.span.category": "session",
6222
+ "mcp.transport": "stdio"
6223
+ } }, async (connectSpan) => {
5709
6224
  try {
5710
6225
  await server.connect(transport);
6226
+ connectSucceeded = true;
5711
6227
  connectSpan.setStatus({ code: SpanStatusCode.OK });
5712
6228
  } catch (e) {
6229
+ recordLifecycleFailure(connectSpan, e, "connect");
5713
6230
  connectSpan.setStatus({ code: SpanStatusCode.ERROR });
5714
6231
  throw e;
5715
6232
  } finally {
@@ -5717,8 +6234,8 @@ async function runStdioServer() {
5717
6234
  }
5718
6235
  });
5719
6236
  scheduleUpdateCheck({
5720
- packageName: name$1,
5721
- currentVersion: version$1
6237
+ packageName: serviceName,
6238
+ currentVersion: serviceVersion
5722
6239
  });
5723
6240
  installGracefulShutdown({
5724
6241
  target: server,
@@ -5730,6 +6247,7 @@ async function runStdioServer() {
5730
6247
  });
5731
6248
  span.setStatus({ code: SpanStatusCode.OK });
5732
6249
  } catch (e) {
6250
+ if (!connectAttempted || connectSucceeded) recordLifecycleFailure(span, e, "run");
5733
6251
  recordMcpSessionTermination(connectAttempted ? "connect_failure" : "startup_failure");
5734
6252
  span.setStatus({ code: SpanStatusCode.ERROR });
5735
6253
  cleanupProcessExceptionTracking();
@@ -5756,7 +6274,10 @@ async function runServer() {
5756
6274
  return;
5757
6275
  }
5758
6276
  const cleanupProcessExceptionTracking = installProcessExceptionTracking();
5759
- await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "http" } }, async (span) => {
6277
+ await tracer.startActiveSpan("mcp.server.run", { attributes: {
6278
+ "mcp.span.category": "startup",
6279
+ "mcp.transport": "http"
6280
+ } }, async (span) => {
5760
6281
  let listening = false;
5761
6282
  serverStartups.add(1, { version });
5762
6283
  try {
@@ -5768,8 +6289,8 @@ async function runServer() {
5768
6289
  listening = true;
5769
6290
  console.error(`Starting MCP server in HTTP mode at ${options.host}:${options.port}/mcp`);
5770
6291
  scheduleUpdateCheck({
5771
- packageName: name$1,
5772
- currentVersion: version$1
6292
+ packageName: serviceName,
6293
+ currentVersion: serviceVersion
5773
6294
  });
5774
6295
  installGracefulShutdown({
5775
6296
  target: handle,
@@ -5780,6 +6301,7 @@ async function runServer() {
5780
6301
  });
5781
6302
  span.setStatus({ code: SpanStatusCode.OK });
5782
6303
  } catch (error) {
6304
+ recordLifecycleFailure(span, error, "run");
5783
6305
  recordMcpSessionTermination(listening ? "unknown" : "startup_failure");
5784
6306
  span.setStatus({ code: SpanStatusCode.ERROR });
5785
6307
  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.5",
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.5",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "5.0.3",
15
+ "version": "5.0.5",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },