hevy-mcp 5.0.2 → 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.2" };
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-DBkGttYB.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.2" };
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-DBkGttYB.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.2" };
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] = "f7cbf1da-83d1-4da1-bb18-1c590afb9b11", e._sentryDebugIdIdentifier = "sentry-dbid-f7cbf1da-83d1-4da1-bb18-1c590afb9b11");
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,15 +79,109 @@ 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
+ ]);
110
+ function normalizeTelemetryError(error) {
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;
121
+ }
122
+ function recordTelemetryException(error, attributes, span) {
123
+ if (!telemetryEnabled) return;
124
+ try {
125
+ const target = span ?? trace.getActiveSpan();
126
+ if (!target) return;
127
+ const normalized = normalizeTelemetryError(error);
128
+ target.recordException(normalized);
129
+ target.setAttribute("exception.type", normalized.name);
130
+ target.setAttribute("error.category", normalized.name);
131
+ if (attributes) target.setAttributes(attributes);
132
+ target.setStatus({ code: SpanStatusCode.ERROR });
133
+ } catch {}
134
+ }
135
+ function installProcessExceptionTracking(processLike = process) {
136
+ if (!telemetryEnabled) return () => {};
137
+ const recordProcessException = (source, error) => {
138
+ try {
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
+ }
151
+ });
152
+ } catch {}
153
+ };
154
+ const uncaughtException = (error) => recordProcessException("uncaughtException", error);
155
+ const unhandledRejection = (error) => recordProcessException("unhandledRejection", error);
156
+ processLike.on("uncaughtExceptionMonitor", uncaughtException);
157
+ processLike.on("unhandledRejection", unhandledRejection);
158
+ let cleaned = false;
159
+ return () => {
160
+ if (cleaned) return;
161
+ cleaned = true;
162
+ processLike.removeListener("uncaughtExceptionMonitor", uncaughtException);
163
+ processLike.removeListener("unhandledRejection", unhandledRejection);
164
+ };
165
+ }
82
166
  const name$1 = "hevy-mcp";
83
- const version$1 = "5.0.2";
167
+ const version$1 = "5.0.4";
84
168
  const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
85
169
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
86
170
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
87
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@5.0.2`;
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
+ }
88
179
  const resource = resourceFromAttributes({
89
180
  "service.name": name$1,
90
- "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
91
185
  });
92
186
  let currentUserHash;
93
187
  var UserHashSpanProcessor = class {
@@ -132,7 +226,8 @@ if (telemetryEnabled) {
132
226
  readers: [new PeriodicExportingMetricReader({
133
227
  exporter: new OTLPMetricExporter({
134
228
  url: `${COLLECTOR_ENDPOINT}/metrics`,
135
- headers: { Authorization: `Bearer ${collectorToken}` }
229
+ headers: { Authorization: `Bearer ${collectorToken}` },
230
+ temporalityPreference: AggregationTemporalityPreference.DELTA
136
231
  }),
137
232
  exportIntervalMillis: 1e4
138
233
  })]
@@ -160,6 +255,8 @@ async function flushTelemetry(timeoutMs = 1e3) {
160
255
  }
161
256
  const tracer = trace.getTracer(name$1);
162
257
  const meter = metrics.getMeter(name$1);
258
+ const serviceName = name$1;
259
+ const serviceVersion = version$1;
163
260
  const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
164
261
  function setTelemetryUser(apiKey) {
165
262
  if (!telemetryEnabled) return;
@@ -968,6 +1065,35 @@ const SAFE_STATIC_ENDPOINTS = /* @__PURE__ */ new Set([
968
1065
  "/v1/workouts/count",
969
1066
  "/v1/workouts/events"
970
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
+ ]);
971
1097
  const SAFE_DYNAMIC_ENDPOINTS = [
972
1098
  ["/v1/body_measurements/", "/v1/body_measurements/:date"],
973
1099
  ["/v1/exercise_history/", "/v1/exercise_history/:exerciseTemplateId"],
@@ -1013,9 +1139,11 @@ function getRequestContext(config) {
1013
1139
  let endpoint = "unknown";
1014
1140
  if (SAFE_STATIC_ENDPOINTS.has(rawEndpoint)) endpoint = rawEndpoint;
1015
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;
1016
1143
  return {
1017
1144
  method,
1018
- endpoint
1145
+ endpoint,
1146
+ page
1019
1147
  };
1020
1148
  }
1021
1149
  function emitClientLog(logger, message) {
@@ -1025,11 +1153,49 @@ function emitClientLog(logger, message) {
1025
1153
  console.error("Failed to emit structured Hevy API log");
1026
1154
  }
1027
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
+ }
1028
1168
  function emitRequestObservation(observer, observation) {
1029
1169
  try {
1030
1170
  observer?.(observation);
1031
1171
  } catch {}
1032
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
+ }
1033
1199
  function parseRetryAfterMs(value) {
1034
1200
  if (!value) return void 0;
1035
1201
  const seconds = Number(value);
@@ -1086,7 +1252,7 @@ function createNativeClient(apiKey, baseUrl, options) {
1086
1252
  ...clientConfig,
1087
1253
  ...config
1088
1254
  };
1089
- const { method, endpoint } = getRequestContext(normalized);
1255
+ const { method, endpoint, page } = getRequestContext(normalized);
1090
1256
  const url = buildUrl(baseUrl, normalized);
1091
1257
  let retryCount = 0;
1092
1258
  while (true) {
@@ -1096,43 +1262,53 @@ function createNativeClient(apiKey, baseUrl, options) {
1096
1262
  code: HEVY_REQUEST_ABORTED_ERROR_CODE
1097
1263
  });
1098
1264
  const startedAt = Date.now();
1265
+ const observationScope = emitRequestStart(options.onRequestStart, {
1266
+ method,
1267
+ endpoint,
1268
+ retryCount
1269
+ });
1099
1270
  const controller = new AbortController();
1100
1271
  const abortFromCaller = () => controller.abort();
1101
1272
  normalized.signal?.addEventListener("abort", abortFromCaller, { once: true });
1102
1273
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
1103
1274
  try {
1104
- const headers = new Headers({ "api-key": apiKey });
1105
- if (normalized.data !== void 0 && !(normalized.data instanceof FormData)) headers.set("content-type", "application/json");
1106
- const attemptDeadline = Date.now() + timeoutMs;
1107
- const response = await withTimeout(fetchImplementation(url, {
1108
- method,
1109
- headers,
1110
- redirect: "manual",
1111
- body: normalized.data instanceof FormData ? normalized.data : normalized.data === void 0 ? void 0 : JSON.stringify(normalized.data),
1112
- signal: controller.signal
1113
- }), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1114
- const data = await withTimeout(parseResponseData(response), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
1115
- if (!response.ok) throw new HevyHttpError(`Hevy API request failed (HTTP ${response.status})`, {
1116
- status: response.status,
1117
- statusText: response.statusText,
1118
- data,
1119
- headers: response.headers,
1120
- method,
1121
- endpoint
1122
- });
1123
- emitRequestObservation(options.onRequestComplete, {
1124
- method,
1125
- endpoint,
1126
- status: response.status,
1127
- durationMs: Date.now() - startedAt,
1128
- 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
+ };
1129
1311
  });
1130
- return {
1131
- data,
1132
- status: response.status,
1133
- statusText: response.statusText,
1134
- headers: response.headers
1135
- };
1136
1312
  } catch (cause) {
1137
1313
  const error = isHevyHttpError(cause) ? cause : new HevyHttpError(normalized.signal?.aborted ? "Hevy API request was canceled" : "Hevy API network request failed", {
1138
1314
  method,
@@ -1140,24 +1316,31 @@ function createNativeClient(apiKey, baseUrl, options) {
1140
1316
  code: normalized.signal?.aborted ? HEVY_REQUEST_ABORTED_ERROR_CODE : getNetworkCode(cause),
1141
1317
  cause
1142
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;
1143
1320
  const canRetry = method === "GET" && isRetryable(error);
1144
- if (canRetry && retryCount >= maxGetRetries) {
1321
+ const retryExhausted = canRetry && retryCount >= maxGetRetries;
1322
+ if (retryExhausted) {
1145
1323
  error.hevyRetryExhausted = true;
1146
1324
  error.hevyRetryCount = retryCount;
1147
1325
  error.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
1148
1326
  }
1149
- emitRequestObservation(options.onRequestComplete, {
1327
+ const observation = {
1150
1328
  method,
1151
1329
  endpoint,
1152
1330
  status: error.status ?? 0,
1153
1331
  durationMs: Date.now() - startedAt,
1154
1332
  retryCount,
1333
+ outcome: expectedReason ? "expected" : canRetry && !retryExhausted ? "retryable_failure" : "terminal_failure",
1334
+ ...expectedReason ? { expectedReason } : {},
1155
1335
  error: {
1156
1336
  status: error.status,
1157
- 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"
1158
1339
  }
1159
- });
1160
- if (!canRetry) {
1340
+ };
1341
+ finishRequestObservation(observationScope, observation);
1342
+ emitRequestObservation(options.onRequestComplete, observation);
1343
+ if (!expectedReason && !canRetry) {
1161
1344
  emitClientLog(options.onLog, {
1162
1345
  level: "error",
1163
1346
  logger: "hevy-api",
@@ -1170,7 +1353,7 @@ function createNativeClient(apiKey, baseUrl, options) {
1170
1353
  });
1171
1354
  throw error;
1172
1355
  }
1173
- if (error.hevyRetryExhausted) throw error;
1356
+ if (expectedReason || error.hevyRetryExhausted) throw error;
1174
1357
  retryCount += 1;
1175
1358
  const delayMs = getRetryDelayMs(error, retryCount);
1176
1359
  emitClientLog(options.onLog, {
@@ -1186,7 +1369,19 @@ function createNativeClient(apiKey, baseUrl, options) {
1186
1369
  endpoint
1187
1370
  }
1188
1371
  });
1189
- 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
+ });
1190
1385
  } finally {
1191
1386
  clearTimeout(timeout);
1192
1387
  normalized.signal?.removeEventListener("abort", abortFromCaller);
@@ -2644,7 +2839,7 @@ function registerHevyResources(server, runtime) {
2644
2839
  //#endregion
2645
2840
  //#region ../core/src/server-metadata.ts
2646
2841
  const SERVER_NAME = "hevy-mcp";
2647
- const SERVER_VERSION = "5.0.2";
2842
+ const SERVER_VERSION = "5.0.4";
2648
2843
  const SERVER_INSTRUCTIONS = [
2649
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.",
2650
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.",
@@ -4007,6 +4202,18 @@ function registerHevyTools(server, runtime) {
4007
4202
  /**
4008
4203
  * Centralized error handling utility for MCP tools
4009
4204
  */
4205
+ function createMcpToolFailureEvent(toolName, errorType, diagnostic) {
4206
+ return {
4207
+ event: "mcp.tool.failure",
4208
+ "mcp.tool.name": toolName,
4209
+ "error.type": errorType,
4210
+ "error.category": diagnostic.category,
4211
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {},
4212
+ ...diagnostic.status !== void 0 ? { "http.status_code": diagnostic.status } : {},
4213
+ ...diagnostic.method ? { "http.method": diagnostic.method } : {},
4214
+ ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
4215
+ };
4216
+ }
4010
4217
  /**
4011
4218
  * Create a standardized error response for MCP tools
4012
4219
  *
@@ -4030,7 +4237,7 @@ function createErrorResponse(error, context) {
4030
4237
  axios: axiosErrorContext ?? void 0
4031
4238
  };
4032
4239
  const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${policy.message}`;
4033
- console.error("MCP tool failure", diagnostic);
4240
+ console.error(JSON.stringify(createMcpToolFailureEvent(context || "unknown", policy.type, diagnostic)));
4034
4241
  return {
4035
4242
  content: [{
4036
4243
  type: "text",
@@ -4192,39 +4399,69 @@ var AsyncTtlCache = class {
4192
4399
  ttlMs;
4193
4400
  maxSize;
4194
4401
  now;
4402
+ observer;
4195
4403
  entries = /* @__PURE__ */ new Map();
4196
4404
  inFlight = /* @__PURE__ */ new Map();
4197
4405
  requestCounter = 0;
4198
4406
  constructor(options, now = Date.now) {
4199
- const { ttlMs, maxSize } = options;
4407
+ const { ttlMs, maxSize, observer } = options;
4200
4408
  if (ttlMs <= 0) throw new Error("Cache ttlMs must be greater than 0.");
4201
4409
  if (maxSize <= 0) throw new Error("Cache maxSize must be greater than 0.");
4202
4410
  this.ttlMs = ttlMs;
4203
4411
  this.maxSize = maxSize;
4204
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 {}
4205
4426
  }
4206
4427
  async getOrFetch(key, fetcher, options = {}) {
4207
- const { refresh = false } = options;
4428
+ const { refresh = false, getObservationMetadata } = options;
4429
+ let observationState = refresh ? "refresh" : "miss";
4208
4430
  if (refresh) this.invalidate(key);
4209
4431
  else {
4210
4432
  const cachedEntry = this.entries.get(key);
4211
4433
  if (cachedEntry !== void 0) {
4212
4434
  if (cachedEntry.expiresAt > this.now()) {
4213
4435
  this.markAsRecentlyUsed(key, cachedEntry);
4436
+ const observationScope = this.startObservation("hit");
4437
+ this.finishObservation(observationScope);
4214
4438
  return cachedEntry.value;
4215
4439
  }
4216
4440
  this.entries.delete(key);
4441
+ observationState = "expired";
4217
4442
  }
4218
4443
  const inFlightEntry = this.inFlight.get(key);
4219
- 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
+ }
4220
4449
  }
4221
4450
  const requestId = ++this.requestCounter;
4451
+ const observationScope = this.startObservation(observationState);
4222
4452
  const request = (async () => {
4223
4453
  try {
4224
4454
  const value = await fetcher();
4225
4455
  if (this.isCurrentRequest(key, requestId)) this.setValue(key, value);
4226
4456
  return value;
4227
4457
  } finally {
4458
+ let metadata;
4459
+ try {
4460
+ metadata = getObservationMetadata?.();
4461
+ } catch {
4462
+ metadata = void 0;
4463
+ }
4464
+ this.finishObservation(observationScope, metadata);
4228
4465
  if (this.inFlight.get(key)?.requestId === requestId) this.inFlight.delete(key);
4229
4466
  }
4230
4467
  })();
@@ -4273,17 +4510,20 @@ var AsyncTtlCache = class {
4273
4510
  const EXERCISE_TEMPLATE_CATALOG_CACHE_KEY = "exercise-template-catalog";
4274
4511
  const EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS = 300 * 1e3;
4275
4512
  const EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE = 1;
4276
- /** Create a cache owned by one MCP server/request lifecycle. */
4277
- function createExerciseTemplateCatalog(hevyClient) {
4513
+ function createExerciseTemplateCatalog(hevyClient, cacheObserver) {
4278
4514
  const cache = new AsyncTtlCache({
4279
4515
  ttlMs: EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS,
4280
- maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE
4516
+ maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE,
4517
+ observer: cacheObserver
4281
4518
  });
4282
4519
  return {
4283
4520
  get(options = {}) {
4284
4521
  const reason = options.refresh ? "explicit-refresh" : cache.size === 0 ? "initial-load" : "ttl-expired";
4522
+ let observationMetadata;
4523
+ let pagesLoaded = 0;
4285
4524
  return cache.getOrFetch(EXERCISE_TEMPLATE_CATALOG_CACHE_KEY, async () => {
4286
4525
  const catalog = await fetchAllPages(async (page, pageSize) => {
4526
+ pagesLoaded = page;
4287
4527
  const data = await hevyClient.getExerciseTemplates({
4288
4528
  page,
4289
4529
  pageSize
@@ -4293,9 +4533,17 @@ function createExerciseTemplateCatalog(hevyClient) {
4293
4533
  pageCount: data?.page_count
4294
4534
  };
4295
4535
  }, 100);
4536
+ observationMetadata = {
4537
+ refreshReason: reason,
4538
+ pageCountBucket: bucketCount(pagesLoaded),
4539
+ itemCountBucket: bucketCount(catalog.length)
4540
+ };
4296
4541
  options.onRefreshed?.(catalog, reason);
4297
4542
  return catalog;
4298
- }, options);
4543
+ }, {
4544
+ refresh: options.refresh,
4545
+ getObservationMetadata: () => observationMetadata
4546
+ });
4299
4547
  },
4300
4548
  reset() {
4301
4549
  cache.clear();
@@ -4357,7 +4605,7 @@ function createHevyMcpServer(options) {
4357
4605
  const client = options.createClient({ onLog: (event) => mcpLogger(event) });
4358
4606
  const runtime = createToolRuntime({
4359
4607
  client,
4360
- catalog: createExerciseTemplateCatalog(client),
4608
+ catalog: createExerciseTemplateCatalog(client, options.cacheObserver),
4361
4609
  logger: mcpLogger,
4362
4610
  observer: options.observer
4363
4611
  });
@@ -4482,13 +4730,15 @@ function extractMcpClientMetadata(message) {
4482
4730
  protocolVersion: normalizeMetadata(params.protocolVersion)
4483
4731
  };
4484
4732
  }
4485
- function createMcpSessionContext(message, transport = "stdio") {
4733
+ function createMcpSessionContext(message, transport = "stdio", options = {}) {
4734
+ const normalizedOptions = typeof options === "function" ? { generateTelemetrySessionId: options } : options;
4486
4735
  return {
4487
4736
  metadata: extractMcpClientMetadata(message),
4488
- startedAt: Date.now(),
4737
+ startedAt: (normalizedOptions.now ?? Date.now)(),
4489
4738
  toolCalls: 0,
4490
4739
  hadToolFailure: false,
4491
- transport
4740
+ transport,
4741
+ telemetrySessionId: normalizedOptions.telemetrySessionId ?? (normalizedOptions.generateTelemetrySessionId ?? randomUUID)()
4492
4742
  };
4493
4743
  }
4494
4744
  function runWithMcpSessionContext(context, callback) {
@@ -4497,6 +4747,9 @@ function runWithMcpSessionContext(context, callback) {
4497
4747
  function getSession() {
4498
4748
  return contextStorage.getStore() ?? activeStdioSession;
4499
4749
  }
4750
+ function getCurrentMcpSessionId() {
4751
+ return getSession()?.telemetrySessionId;
4752
+ }
4500
4753
  function metadataAttributes(metadata, transport) {
4501
4754
  return {
4502
4755
  client_name: metadata.name,
@@ -4937,6 +5190,38 @@ function debugLog(event, data) {
4937
5190
  }
4938
5191
  //#endregion
4939
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
+ }
4940
5225
  /** Node-only adapter; the Worker graph never imports telemetry or metrics. */
4941
5226
  function createNodeHevyClientOptions() {
4942
5227
  const rawTimeout = process.env.HEVY_MCP_API_TIMEOUT;
@@ -4944,37 +5229,38 @@ function createNodeHevyClientOptions() {
4944
5229
  const timeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? Math.floor(parsedTimeout) : void 0;
4945
5230
  return {
4946
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
+ },
4947
5243
  onRequestComplete(observation) {
4948
5244
  const retryCountBucket = bucketCount(observation.retryCount);
4949
- const diagnostic = observation.error ? createSafeErrorDiagnostic(observation.error) : void 0;
4950
- const safeErrorAttributes = diagnostic ? {
4951
- error_category: diagnostic.category,
4952
- ...diagnostic.code ? { error_code: diagnostic.code } : {}
4953
- } : {};
4954
- const span = tracer.startSpan(`hevy.api.${observation.method}`, { attributes: {
4955
- "http.method": observation.method,
4956
- "http.status_code": observation.status,
4957
- "hevy.api.retry_count_bucket": retryCountBucket,
4958
- "hevy.api.endpoint": observation.endpoint
4959
- } });
4960
- span.setStatus({ code: observation.error ? SpanStatusCode.ERROR : SpanStatusCode.OK });
4961
- if (diagnostic) span.addEvent("hevy.api.failure", {
4962
- "error.category": diagnostic.category,
4963
- ...diagnostic.code ? { "error.code": diagnostic.code } : {}
4964
- });
4965
- span.end();
5245
+ const errorAttributes = safeErrorAttributes(observation);
4966
5246
  apiCalls.add(1, {
4967
5247
  method: observation.method,
4968
5248
  endpoint: observation.endpoint,
4969
5249
  status_code: observation.status,
4970
5250
  retry_count_bucket: retryCountBucket,
4971
- ...safeErrorAttributes
5251
+ outcome: observation.outcome,
5252
+ transport: getCurrentMcpTransport(),
5253
+ ...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
5254
+ ...errorAttributes
4972
5255
  });
4973
5256
  apiDuration.record(observation.durationMs, {
4974
5257
  method: observation.method,
4975
5258
  endpoint: observation.endpoint,
4976
5259
  retry_count_bucket: retryCountBucket,
4977
- ...safeErrorAttributes
5260
+ outcome: observation.outcome,
5261
+ transport: getCurrentMcpTransport(),
5262
+ ...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
5263
+ ...errorAttributes
4978
5264
  });
4979
5265
  debugLog("api_response", {
4980
5266
  method: observation.method,
@@ -4982,13 +5268,43 @@ function createNodeHevyClientOptions() {
4982
5268
  durationMs: observation.durationMs,
4983
5269
  status: observation.status || null,
4984
5270
  retryCountBucket,
4985
- ...safeErrorAttributes
5271
+ outcome: observation.outcome,
5272
+ ...errorAttributes
4986
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() };
4987
5285
  }
4988
5286
  };
4989
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
+ }
4990
5305
  //#endregion
4991
5306
  //#region src/utils/tool-observer.ts
5307
+ const DISCOVERY_TOOL_NAMES = /* @__PURE__ */ new Set(["search-routines"]);
4992
5308
  const WORKFLOW_PAGINATION_RESOURCES = /* @__PURE__ */ new Set([
4993
5309
  "workouts",
4994
5310
  "bodyMeasurements",
@@ -5011,8 +5327,11 @@ function metricAttributes(invocation, clientAttributes) {
5011
5327
  }
5012
5328
  function createAttributes(invocation) {
5013
5329
  const clientMetadata = getCurrentMcpClientMetadata();
5330
+ const isPrompt = invocation.kind === "prompt";
5331
+ const sessionId = getCurrentMcpSessionId();
5014
5332
  const attributes = {
5015
- [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,
5016
5335
  "mcp.operation.kind": invocation.kind ?? "tool",
5017
5336
  ...taxonomyAttributes(invocation),
5018
5337
  "mcp.client.name": clientMetadata.name,
@@ -5020,7 +5339,8 @@ function createAttributes(invocation) {
5020
5339
  "mcp.protocol.version": clientMetadata.protocolVersion,
5021
5340
  "mcp.transport": getCurrentMcpTransport(),
5022
5341
  "mcp.tool.args.key_count_bucket": invocation.argumentKeyCountBucket ?? "unknown",
5023
- "mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? ""
5342
+ "mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? "",
5343
+ ...sessionId ? { "mcp.session.id": sessionId } : {}
5024
5344
  };
5025
5345
  for (const key of Object.keys(invocation.argumentPresence ?? {})) attributes[`mcp.tool.args.${key}.present`] = true;
5026
5346
  for (const [key, bucket] of Object.entries(invocation.numericArgumentBuckets ?? {})) if (bucket !== void 0) attributes[`mcp.tool.args.${key}.bucket`] = bucket;
@@ -5058,14 +5378,16 @@ function resultMetricAttributes(completion) {
5058
5378
  if (summary?.setCountBucket) attributes["mcp.tool.result.set_count_bucket"] = summary.setCountBucket;
5059
5379
  return attributes;
5060
5380
  }
5061
- function setSafeErrorAttributes(span, completion) {
5381
+ function setSafeErrorAttributes(span, invocation, completion) {
5062
5382
  const diagnostic = completion.error;
5063
5383
  const errorType = completion.errorType ?? "UNKNOWN_ERROR";
5064
5384
  if (diagnostic) span.addEvent("mcp.tool.failure", {
5385
+ "mcp.tool.name": invocation.name,
5386
+ "error.type": errorType,
5065
5387
  "error.category": diagnostic.category,
5066
5388
  ...diagnostic.code ? { "error.code": diagnostic.code } : {},
5067
- ...diagnostic.status !== void 0 ? { "http.status_code": diagnostic.status } : {},
5068
- ...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 } : {},
5069
5391
  ...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
5070
5392
  });
5071
5393
  span.setAttribute("error.type", errorType);
@@ -5077,11 +5399,13 @@ function captureSafeToolFailure(invocation, completion) {
5077
5399
  try {
5078
5400
  Sentry.withScope((scope) => {
5079
5401
  const isPrompt = invocation.kind === "prompt";
5402
+ scope.setTag("mcp.tool.name", invocation.name);
5080
5403
  if (isPrompt) scope.setTag("mcp.prompt.name", invocation.name);
5081
- else scope.setTag("mcp.tool.context", invocation.name);
5404
+ scope.setTag("error.type", completion.errorType ?? "UNKNOWN_ERROR");
5082
5405
  scope.setTag("error.category", category);
5083
5406
  if (diagnostic?.code) scope.setTag("error.code", diagnostic.code);
5084
- if (diagnostic?.status !== void 0) scope.setTag("http.status_code", String(diagnostic.status));
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);
5085
5409
  if (diagnostic?.endpoint) scope.setTag("hevy.api.endpoint", diagnostic.endpoint);
5086
5410
  scope.setContext(invocation.kind === "prompt" ? "mcpPrompt" : "mcpTool", {
5087
5411
  context: invocation.name,
@@ -5134,7 +5458,36 @@ function createNodeToolObserver() {
5134
5458
  activeSpan.setStatus({ code: nextCompletion.outcome === "success" && nextCompletion.result?.isError !== true ? SpanStatusCode.OK : SpanStatusCode.ERROR });
5135
5459
  activeSpan.setAttribute("mcp.tool.outcome", nextCompletion.outcome);
5136
5460
  setResultAttributes(activeSpan, nextCompletion);
5137
- if (nextCompletion.outcome === "thrown_error") errorType = setSafeErrorAttributes(activeSpan, nextCompletion);
5461
+ if (nextCompletion.outcome === "thrown_error") {
5462
+ errorType = setSafeErrorAttributes(activeSpan, invocation, nextCompletion);
5463
+ bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? errorType), {
5464
+ [invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
5465
+ "error.type": errorType ?? "UNKNOWN_ERROR",
5466
+ "error.category": nextCompletion.error?.category ?? "UnknownError",
5467
+ ...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
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 } : {},
5488
+ ...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
5489
+ }, activeSpan));
5490
+ }
5138
5491
  }
5139
5492
  } catch {} finally {
5140
5493
  if (nextCompletion.outcome === "thrown_error") {
@@ -5315,14 +5668,17 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
5315
5668
  const lineHadLeadingBom = line.startsWith(UTF8_BOM);
5316
5669
  const normalizedLine = lineHadLeadingBom ? line.slice(1) : line;
5317
5670
  const lineByteLength = Buffer.byteLength(line);
5671
+ const sessionId = getCurrentMcpSessionId();
5318
5672
  return tracer.startActiveSpan("mcp.stdio.deserialize", { attributes: {
5673
+ "mcp.span.category": "protocol",
5319
5674
  "mcp.transport": "stdio",
5320
5675
  "mcp.stdio.parse.line.char_length": line.length,
5321
5676
  "mcp.stdio.parse.line.byte_length": lineByteLength,
5322
5677
  "mcp.stdio.parse.line.had_leading_bom": lineHadLeadingBom,
5323
5678
  "mcp.stdio.parse.line.bom_stripped": lineHadLeadingBom,
5324
5679
  "mcp.stdio.parse.chunk.last_byte_length": chunkSnapshot.lastChunkByteLength,
5325
- "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 } : {}
5326
5682
  } }, (span) => {
5327
5683
  try {
5328
5684
  const message = deserializeMessage(normalizedLine);
@@ -5332,6 +5688,8 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
5332
5688
  if (typeof method === "string" && SAFE_MCP_METHODS[method] === true) span.setAttribute("mcp.method", method);
5333
5689
  if (method === "initialize") {
5334
5690
  const client = recordMcpSessionStart(message);
5691
+ const sessionId = getCurrentMcpSessionId();
5692
+ if (sessionId) span.setAttribute("mcp.session.id", sessionId);
5335
5693
  span.setAttribute("mcp.client.name", client.name);
5336
5694
  span.setAttribute("mcp.client.version", client.version);
5337
5695
  span.setAttribute("mcp.protocol.version", client.protocolVersion);
@@ -5498,8 +5856,8 @@ function scheduleUpdateCheck(options, overrides = {}) {
5498
5856
  }
5499
5857
  //#endregion
5500
5858
  //#region src/index.ts
5501
- const name = name$1;
5502
- const version = version$1;
5859
+ const name = serviceName;
5860
+ const version = serviceVersion;
5503
5861
  const HELP_TEXT = [
5504
5862
  "Usage:",
5505
5863
  " hevy-mcp [options]",
@@ -5559,6 +5917,159 @@ function getSafeValidationDiagnostic(error) {
5559
5917
  const code = error.code;
5560
5918
  return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
5561
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
+ }
5562
6073
  async function validateApiKey(apiKey) {
5563
6074
  const startupProbeClient = createHevyClient({
5564
6075
  apiKey,
@@ -5577,6 +6088,7 @@ async function validateApiKey(apiKey) {
5577
6088
  }
5578
6089
  function buildServer(apiKey, transport = "stdio") {
5579
6090
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
6091
+ "mcp.span.category": "startup",
5580
6092
  "mcp.server.name": name,
5581
6093
  "mcp.server.version": version,
5582
6094
  "mcp.transport": transport
@@ -5593,12 +6105,15 @@ function buildServer(apiKey, transport = "stdio") {
5593
6105
  recordOutputs: false
5594
6106
  }),
5595
6107
  onToolsRegistered: (count) => span.setAttribute("mcp.tools.count", count),
5596
- observer: createNodeToolObserver()
6108
+ observer: createNodeToolObserver(),
6109
+ cacheObserver: createNodeCacheObserver()
5597
6110
  });
6111
+ installSdkErrorTracking(server);
5598
6112
  console.error("Hevy client initialized with API key");
5599
6113
  span.setStatus({ code: SpanStatusCode.OK });
5600
6114
  return server;
5601
6115
  } catch (e) {
6116
+ recordLifecycleFailure(span, e, "build");
5602
6117
  span.setStatus({ code: SpanStatusCode.ERROR });
5603
6118
  throw e;
5604
6119
  } finally {
@@ -5622,11 +6137,16 @@ async function runStdioServer() {
5622
6137
  console.log(HELP_TEXT);
5623
6138
  return;
5624
6139
  }
6140
+ const cleanupProcessExceptionTracking = installProcessExceptionTracking();
5625
6141
  serverStartups.add(1, { version });
5626
6142
  const configuredApiKey = process.env.HEVY_API_KEY;
5627
6143
  if (configuredApiKey) setTelemetryUser(configuredApiKey);
5628
6144
  let connectAttempted = false;
5629
- 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) => {
5630
6150
  try {
5631
6151
  const apiKey = parseConfig(process.env).apiKey;
5632
6152
  assertApiKey(apiKey);
@@ -5634,11 +6154,16 @@ async function runStdioServer() {
5634
6154
  console.error("Starting MCP server in stdio mode");
5635
6155
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
5636
6156
  connectAttempted = true;
5637
- 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) => {
5638
6161
  try {
5639
6162
  await server.connect(transport);
6163
+ connectSucceeded = true;
5640
6164
  connectSpan.setStatus({ code: SpanStatusCode.OK });
5641
6165
  } catch (e) {
6166
+ recordLifecycleFailure(connectSpan, e, "connect");
5642
6167
  connectSpan.setStatus({ code: SpanStatusCode.ERROR });
5643
6168
  throw e;
5644
6169
  } finally {
@@ -5646,20 +6171,23 @@ async function runStdioServer() {
5646
6171
  }
5647
6172
  });
5648
6173
  scheduleUpdateCheck({
5649
- packageName: name$1,
5650
- currentVersion: version$1
6174
+ packageName: serviceName,
6175
+ currentVersion: serviceVersion
5651
6176
  });
5652
6177
  installGracefulShutdown({
5653
6178
  target: server,
5654
6179
  onComplete: async (succeeded) => {
5655
6180
  recordMcpSessionTermination(resolveSessionTerminationCategory(succeeded));
6181
+ cleanupProcessExceptionTracking();
5656
6182
  await flushTelemetry();
5657
6183
  }
5658
6184
  });
5659
6185
  span.setStatus({ code: SpanStatusCode.OK });
5660
6186
  } catch (e) {
6187
+ if (!connectAttempted || connectSucceeded) recordLifecycleFailure(span, e, "run");
5661
6188
  recordMcpSessionTermination(connectAttempted ? "connect_failure" : "startup_failure");
5662
6189
  span.setStatus({ code: SpanStatusCode.ERROR });
6190
+ cleanupProcessExceptionTracking();
5663
6191
  throw e;
5664
6192
  } finally {
5665
6193
  span.end();
@@ -5682,7 +6210,11 @@ async function runServer() {
5682
6210
  await runStdioServer();
5683
6211
  return;
5684
6212
  }
5685
- await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "http" } }, async (span) => {
6213
+ const cleanupProcessExceptionTracking = installProcessExceptionTracking();
6214
+ await tracer.startActiveSpan("mcp.server.run", { attributes: {
6215
+ "mcp.span.category": "startup",
6216
+ "mcp.transport": "http"
6217
+ } }, async (span) => {
5686
6218
  let listening = false;
5687
6219
  serverStartups.add(1, { version });
5688
6220
  try {
@@ -5694,19 +6226,22 @@ async function runServer() {
5694
6226
  listening = true;
5695
6227
  console.error(`Starting MCP server in HTTP mode at ${options.host}:${options.port}/mcp`);
5696
6228
  scheduleUpdateCheck({
5697
- packageName: name$1,
5698
- currentVersion: version$1
6229
+ packageName: serviceName,
6230
+ currentVersion: serviceVersion
5699
6231
  });
5700
6232
  installGracefulShutdown({
5701
6233
  target: handle,
5702
6234
  onComplete: async () => {
6235
+ cleanupProcessExceptionTracking();
5703
6236
  await flushTelemetry();
5704
6237
  }
5705
6238
  });
5706
6239
  span.setStatus({ code: SpanStatusCode.OK });
5707
6240
  } catch (error) {
6241
+ recordLifecycleFailure(span, error, "run");
5708
6242
  recordMcpSessionTermination(listening ? "unknown" : "startup_failure");
5709
6243
  span.setStatus({ code: SpanStatusCode.ERROR });
6244
+ cleanupProcessExceptionTracking();
5710
6245
  throw error;
5711
6246
  } finally {
5712
6247
  span.end();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hevy-mcp",
3
- "version": "5.0.2",
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.2",
10
+ "version": "5.0.4",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "hevy-mcp",
15
- "version": "5.0.2",
15
+ "version": "5.0.4",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },