hevy-mcp 3.3.0 → 3.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,9 +4,9 @@
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@3.3.0" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.4.1" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "d4c67aeb-89ed-4cf7-ae4a-09c19c187b3d", e._sentryDebugIdIdentifier = "sentry-dbid-d4c67aeb-89ed-4cf7-ae4a-09c19c187b3d");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "474ddca0-d028-4c83-9184-bac22b12845e", e._sentryDebugIdIdentifier = "sentry-dbid-474ddca0-d028-4c83-9184-bac22b12845e");
10
10
  } catch (e) {}
11
11
  })();
12
12
  import * as Sentry from "@sentry/node";
@@ -28,6 +28,43 @@ import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
28
28
  import { homedir } from "node:os";
29
29
  import { dirname, join } from "node:path";
30
30
  import semver from "semver";
31
+ //#region src/utils/sentry-privacy.ts
32
+ const SENTRY_PROHIBITED_MCP_ATTRIBUTES = /* @__PURE__ */ new Set([
33
+ "mcp.request.id",
34
+ "mcp.session.id",
35
+ "mcp.cancelled.request_id",
36
+ "mcp.cancelled.reason",
37
+ "mcp.progress.token",
38
+ "mcp.progress.message",
39
+ "mcp.resource.uri",
40
+ "mcp.method.name",
41
+ "mcp.resource.protocol",
42
+ "mcp.prompt.name",
43
+ "mcp.protocol.version",
44
+ "mcp.logging.message",
45
+ "mcp.prompt.result.description",
46
+ "mcp.request.argument",
47
+ "mcp.logging.logger",
48
+ "mcp.logging.level",
49
+ "mcp.client.name",
50
+ "mcp.client.title",
51
+ "mcp.client.version"
52
+ ]);
53
+ /** Remove MCP correlation and unbounded client fields before Sentry export. */
54
+ function sanitizeSentryMcpSpan(span) {
55
+ if (!span.data) return span;
56
+ let sanitizedData;
57
+ for (const key of SENTRY_PROHIBITED_MCP_ATTRIBUTES) {
58
+ if (!(key in span.data)) continue;
59
+ sanitizedData ??= { ...span.data };
60
+ delete sanitizedData[key];
61
+ }
62
+ return sanitizedData ? {
63
+ ...span,
64
+ data: sanitizedData
65
+ } : span;
66
+ }
67
+ //#endregion
31
68
  //#region src/utils/telemetry.ts
32
69
  /**
33
70
  * Centralized telemetry initialization.
@@ -40,10 +77,10 @@ import semver from "semver";
40
77
  * OTel Collector → Honeycomb: performance traces, metrics
41
78
  */
42
79
  const name$1 = "hevy-mcp";
43
- const version$1 = "3.3.0";
80
+ const version$1 = "3.4.1";
44
81
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
45
82
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
46
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@3.3.0`;
83
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@3.4.1`;
47
84
  const resource = resourceFromAttributes({
48
85
  "service.name": name$1,
49
86
  "service.version": version$1
@@ -52,6 +89,7 @@ const rawDsn = process.env.SENTRY_DSN ?? "https://ce696d8333b507acbf5203eb877bce
52
89
  const isValidDsn = typeof rawDsn === "string" && rawDsn.length > 0 && !rawDsn.startsWith("*");
53
90
  const sentryClient = Sentry.init({
54
91
  dsn: isValidDsn ? rawDsn : void 0,
92
+ beforeSendSpan: sanitizeSentryMcpSpan,
55
93
  release: sentryRelease,
56
94
  tracesSampleRate: 1,
57
95
  sendDefaultPii: false,
@@ -82,21 +120,36 @@ tracerProvider.register({
82
120
  propagator: new SentryPropagator(),
83
121
  contextManager: new Sentry.SentryContextManager()
84
122
  });
85
- {
86
- const meterProvider = new MeterProvider({
87
- resource,
88
- readers: [new PeriodicExportingMetricReader({
89
- exporter: new OTLPMetricExporter({
90
- url: `${COLLECTOR_ENDPOINT}/metrics`,
91
- headers: { Authorization: `Bearer ${collectorToken}` }
92
- }),
93
- exportIntervalMillis: 1e4
94
- })]
95
- });
96
- metrics.setGlobalMeterProvider(meterProvider);
97
- }
123
+ let meterProvider;
124
+ meterProvider = new MeterProvider({
125
+ resource,
126
+ readers: [new PeriodicExportingMetricReader({
127
+ exporter: new OTLPMetricExporter({
128
+ url: `${COLLECTOR_ENDPOINT}/metrics`,
129
+ headers: { Authorization: `Bearer ${collectorToken}` }
130
+ }),
131
+ exportIntervalMillis: 1e4
132
+ })]
133
+ });
134
+ metrics.setGlobalMeterProvider(meterProvider);
98
135
  trace.setGlobalTracerProvider(tracerProvider);
99
136
  Sentry.validateOpenTelemetrySetup();
137
+ async function flushTelemetry(timeoutMs = 1e3) {
138
+ const flushPromise = Promise.allSettled([
139
+ tracerProvider.forceFlush(),
140
+ ...meterProvider ? [meterProvider.forceFlush()] : [],
141
+ Sentry.flush(timeoutMs)
142
+ ]);
143
+ let timeout;
144
+ const timeoutPromise = new Promise((resolve) => {
145
+ timeout = setTimeout(resolve, timeoutMs);
146
+ });
147
+ try {
148
+ await Promise.race([flushPromise, timeoutPromise]);
149
+ } finally {
150
+ clearTimeout(timeout);
151
+ }
152
+ }
100
153
  const tracer = trace.getTracer(name$1);
101
154
  const meter = metrics.getMeter(name$1);
102
155
  function setCurrentUserHash(hash) {
@@ -111,9 +164,15 @@ function setCurrentUserHash(hash) {
111
164
  * for use throughout the codebase. All instruments are sent to Honeycomb
112
165
  * via the OTLP metrics exporter configured in telemetry.ts.
113
166
  */
114
- /** Total MCP tool invocations, grouped by tool name. */
167
+ /** Total MCP tool invocations grouped by name and bounded taxonomy. */
115
168
  const toolInvocations = meter.createCounter("mcp.tool.invocations", { description: "Total MCP tool invocations" });
116
- /** Total MCP tool errors, grouped by tool name and error type. */
169
+ /** Total MCP tool outcomes, grouped by bounded taxonomy and outcome. */
170
+ const toolOutcomes = meter.createCounter("mcp.tool.outcomes", { description: "Total MCP tool outcomes" });
171
+ /** Total MCP sessions started, grouped by sanitized client metadata. */
172
+ const sessionStarted = meter.createCounter("mcp.session.started", { description: "Total MCP sessions started" });
173
+ /** Total MCP sessions ended, grouped by bounded termination metadata. */
174
+ const sessionEnded = meter.createCounter("mcp.session.ended", { description: "Total MCP sessions ended" });
175
+ /** Total thrown MCP tool errors grouped by name, taxonomy, and error type. */
117
176
  const toolErrors = meter.createCounter("mcp.tool.errors", { description: "Total MCP tool errors" });
118
177
  /** MCP tool execution duration in milliseconds. */
119
178
  const toolDuration = meter.createHistogram("mcp.tool.duration_ms", {
@@ -547,6 +606,22 @@ function resolveErrorPolicy(error, defaultMessage, notInitializedMessage) {
547
606
  };
548
607
  }
549
608
  //#endregion
609
+ //#region src/utils/result-telemetry.ts
610
+ function bucketCount(value) {
611
+ if (!Number.isFinite(value) || value <= 0) return "0";
612
+ if (value === 1) return "1";
613
+ if (value <= 10) return "2-10";
614
+ if (value <= 50) return "11-50";
615
+ return "51+";
616
+ }
617
+ const resultTelemetry = /* @__PURE__ */ new WeakMap();
618
+ function attachResultTelemetry(result, telemetry) {
619
+ if (telemetry) resultTelemetry.set(result, telemetry);
620
+ }
621
+ function getResultTelemetry(result) {
622
+ return resultTelemetry.get(result);
623
+ }
624
+ //#endregion
550
625
  //#region src/utils/response-formatter.ts
551
626
  function jsonText(data) {
552
627
  return JSON.stringify(data, null, 2) ?? "null";
@@ -564,17 +639,21 @@ function defineStructuredResponseContract(definition) {
564
639
  const structuredContent = z.object(definition.outputSchema).parse(definition.normalize(data));
565
640
  const text = definition.text?.(data, structuredContent) ?? jsonText(definition.legacyJson(structuredContent));
566
641
  const additionalText = definition.additionalText?.(data, structuredContent) ?? [];
567
- return {
642
+ const response = {
568
643
  content: [textContent(text), ...additionalText.map(textContent)],
569
644
  structuredContent
570
645
  };
646
+ attachResultTelemetry(response, definition.telemetry?.(data));
647
+ return response;
571
648
  }
572
649
  };
573
650
  }
574
- function defineJsonResponseContract(present) {
651
+ function defineJsonResponseContract(present, telemetry) {
575
652
  return { render(data) {
576
653
  const presentation = present(data);
577
- return { content: [textContent(presentation.text ?? jsonText(presentation.json)), ...(presentation.additionalText ?? []).map(textContent)] };
654
+ const response = { content: [textContent(presentation.text ?? jsonText(presentation.json)), ...(presentation.additionalText ?? []).map(textContent)] };
655
+ attachResultTelemetry(response, telemetry?.(data));
656
+ return response;
578
657
  } };
579
658
  }
580
659
  /** The only public success-response entry point used by tool handlers. */
@@ -955,125 +1034,183 @@ function formatWorkoutEvent(event) {
955
1034
  };
956
1035
  throw new Error(`Unsupported workout event type: ${event.type}`);
957
1036
  }
1037
+ function exerciseSetCountTelemetry(exercises) {
1038
+ const setCount = exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0);
1039
+ return {
1040
+ exerciseCountBucket: bucketCount(exercises.length),
1041
+ setCountBucket: bucketCount(setCount)
1042
+ };
1043
+ }
1044
+ function workoutResultTelemetry(workout) {
1045
+ const exercises = workout?.exercises ?? [];
1046
+ return {
1047
+ itemCountBucket: bucketCount(workout ? 1 : 0),
1048
+ ...exerciseSetCountTelemetry(exercises)
1049
+ };
1050
+ }
1051
+ function routineResultTelemetry(routine) {
1052
+ const exercises = routine?.exercises ?? [];
1053
+ return {
1054
+ itemCountBucket: bucketCount(routine ? 1 : 0),
1055
+ ...exerciseSetCountTelemetry(exercises)
1056
+ };
1057
+ }
1058
+ const SAFE_WORKFLOW_NAMES = {
1059
+ "training-summary": "training-summary",
1060
+ "routine-discovery": "routine-discovery"
1061
+ };
1062
+ function workflowResultTelemetry(workflow) {
1063
+ const name = SAFE_WORKFLOW_NAMES[workflow.name];
1064
+ if (!name) return void 0;
1065
+ return {
1066
+ name,
1067
+ pagination: workflow.pagination,
1068
+ cacheStatus: workflow.cacheStatus,
1069
+ itemsScanned: workflow.itemsScanned
1070
+ };
1071
+ }
958
1072
  const workoutsResponse = defineStructuredResponseContract({
959
1073
  outputSchema: workoutsOutputSchema,
960
1074
  normalize: (workouts) => ({ workouts: workouts?.map(formatWorkout) ?? [] }),
961
1075
  legacyJson: ({ workouts }) => workouts,
962
- text: (_data, { workouts }) => workouts.length === 0 ? "No workouts found for the specified parameters" : void 0
1076
+ text: (_data, { workouts }) => workouts.length === 0 ? "No workouts found for the specified parameters" : void 0,
1077
+ telemetry: (workouts) => ({ itemCountBucket: bucketCount(workouts?.length ?? 0) })
963
1078
  });
964
1079
  const workoutResponse = defineStructuredResponseContract({
965
1080
  outputSchema: workoutOutputSchema,
966
1081
  normalize: (data) => ({ workout: data.workout ? formatWorkout(data.workout) : null }),
967
1082
  legacyJson: ({ workout }) => workout,
968
- text: ({ workoutId }, { workout }) => workout === null ? `Workout with ID ${workoutId} not found` : void 0
1083
+ text: ({ workoutId }, { workout }) => workout === null ? `Workout with ID ${workoutId} not found` : void 0,
1084
+ telemetry: ({ workout }) => workoutResultTelemetry(workout)
969
1085
  });
970
1086
  const workoutCountResponse = defineStructuredResponseContract({
971
1087
  outputSchema: workoutCountOutputSchema,
972
1088
  normalize: (count) => ({ count }),
973
- legacyJson: (output) => output
1089
+ legacyJson: (output) => output,
1090
+ telemetry: (count) => ({ itemCountBucket: bucketCount(count) })
974
1091
  });
975
1092
  const workoutEventsResponse = defineStructuredResponseContract({
976
1093
  outputSchema: workoutEventsOutputSchema,
977
1094
  normalize: (data) => ({ events: data.events?.map(formatWorkoutEvent) ?? [] }),
978
1095
  legacyJson: ({ events }) => events,
979
- text: ({ since }, { events }) => events.length === 0 ? `No workout events found for the specified parameters since ${since}` : void 0
1096
+ text: ({ since }, { events }) => events.length === 0 ? `No workout events found for the specified parameters since ${since}` : void 0,
1097
+ telemetry: (data) => ({ itemCountBucket: bucketCount(data.events?.length ?? 0) })
980
1098
  });
981
1099
  const routinesResponse = defineStructuredResponseContract({
982
1100
  outputSchema: routinesOutputSchema,
983
1101
  normalize: (routines) => ({ routines: routines?.map(formatRoutine) ?? [] }),
984
1102
  legacyJson: ({ routines }) => routines,
985
- text: (_data, { routines }) => routines.length === 0 ? "No routines found for the specified parameters" : void 0
1103
+ text: (_data, { routines }) => routines.length === 0 ? "No routines found for the specified parameters" : void 0,
1104
+ telemetry: (routines) => ({ itemCountBucket: bucketCount(routines?.length ?? 0) })
986
1105
  });
987
1106
  const routineResponse = defineStructuredResponseContract({
988
1107
  outputSchema: routineOutputSchema,
989
1108
  normalize: (data) => ({ routine: data.routine ? formatRoutine(data.routine) : null }),
990
1109
  legacyJson: ({ routine }) => routine,
991
- text: ({ routineId }, { routine }) => routine === null ? `Routine with ID ${routineId} not found` : void 0
1110
+ text: ({ routineId }, { routine }) => routine === null ? `Routine with ID ${routineId} not found` : void 0,
1111
+ telemetry: ({ routine }) => routineResultTelemetry(routine)
992
1112
  });
993
1113
  const exerciseTemplatesResponse = defineStructuredResponseContract({
994
1114
  outputSchema: exerciseTemplatesOutputSchema,
995
1115
  normalize: (templates) => ({ exerciseTemplates: templates?.map(formatExerciseTemplate) ?? [] }),
996
1116
  legacyJson: ({ exerciseTemplates }) => exerciseTemplates,
997
- text: (_data, { exerciseTemplates }) => exerciseTemplates.length === 0 ? "No exercise templates found for the specified parameters" : void 0
1117
+ text: (_data, { exerciseTemplates }) => exerciseTemplates.length === 0 ? "No exercise templates found for the specified parameters" : void 0,
1118
+ telemetry: (templates) => ({ itemCountBucket: bucketCount(templates?.length ?? 0) })
998
1119
  });
999
1120
  const exerciseTemplateResponse = defineStructuredResponseContract({
1000
1121
  outputSchema: exerciseTemplateOutputSchema,
1001
1122
  normalize: (data) => ({ exerciseTemplate: data.exerciseTemplate ? formatExerciseTemplate(data.exerciseTemplate) : null }),
1002
1123
  legacyJson: ({ exerciseTemplate }) => exerciseTemplate,
1003
- text: ({ exerciseTemplateId }, { exerciseTemplate }) => exerciseTemplate === null ? `Exercise template with ID ${exerciseTemplateId} not found` : void 0
1124
+ text: ({ exerciseTemplateId }, { exerciseTemplate }) => exerciseTemplate === null ? `Exercise template with ID ${exerciseTemplateId} not found` : void 0,
1125
+ telemetry: ({ exerciseTemplate }) => ({ itemCountBucket: bucketCount(exerciseTemplate ? 1 : 0) })
1004
1126
  });
1005
1127
  const exerciseHistoryResponse = defineStructuredResponseContract({
1006
1128
  outputSchema: exerciseHistoryOutputSchema,
1007
1129
  normalize: (data) => ({ exerciseHistory: data.history?.map(formatExerciseHistoryEntry) ?? [] }),
1008
1130
  legacyJson: ({ exerciseHistory }) => exerciseHistory,
1009
- text: ({ exerciseTemplateId }, { exerciseHistory }) => exerciseHistory.length === 0 ? `No exercise history found for template ${exerciseTemplateId}` : void 0
1131
+ text: ({ exerciseTemplateId }, { exerciseHistory }) => exerciseHistory.length === 0 ? `No exercise history found for template ${exerciseTemplateId}` : void 0,
1132
+ telemetry: (data) => ({ itemCountBucket: bucketCount(data.history?.length ?? 0) })
1010
1133
  });
1011
1134
  const searchExerciseTemplatesResponse = defineStructuredResponseContract({
1012
1135
  outputSchema: exerciseTemplatesOutputSchema,
1013
1136
  normalize: (data) => ({ exerciseTemplates: data.results.map(formatExerciseTemplate) }),
1014
1137
  legacyJson: ({ exerciseTemplates }) => exerciseTemplates,
1015
- text: ({ query, primaryMuscleGroup }, { exerciseTemplates }) => exerciseTemplates.length === 0 ? `No exercise templates found matching "${query}"${primaryMuscleGroup ? ` with primary muscle group "${primaryMuscleGroup}"` : ""}` : void 0
1138
+ text: ({ query, primaryMuscleGroup }, { exerciseTemplates }) => exerciseTemplates.length === 0 ? `No exercise templates found matching "${query}"${primaryMuscleGroup ? ` with primary muscle group "${primaryMuscleGroup}"` : ""}` : void 0,
1139
+ telemetry: (data) => ({ itemCountBucket: bucketCount(data.results.length) })
1016
1140
  });
1017
1141
  const routineFoldersResponse = defineStructuredResponseContract({
1018
1142
  outputSchema: routineFoldersOutputSchema,
1019
1143
  normalize: (folders) => ({ routineFolders: folders?.map(formatRoutineFolder) ?? [] }),
1020
1144
  legacyJson: ({ routineFolders }) => routineFolders,
1021
- text: (_data, { routineFolders }) => routineFolders.length === 0 ? "No routine folders found for the specified parameters" : void 0
1145
+ text: (_data, { routineFolders }) => routineFolders.length === 0 ? "No routine folders found for the specified parameters" : void 0,
1146
+ telemetry: (folders) => ({ itemCountBucket: bucketCount(folders?.length ?? 0) })
1022
1147
  });
1023
1148
  const routineFolderResponse = defineStructuredResponseContract({
1024
1149
  outputSchema: routineFolderOutputSchema,
1025
1150
  normalize: (data) => ({ routineFolder: data.routineFolder ? formatRoutineFolder(data.routineFolder) : null }),
1026
1151
  legacyJson: ({ routineFolder }) => routineFolder,
1027
- text: ({ folderId }, { routineFolder }) => routineFolder === null ? `Routine folder with ID ${folderId} not found` : void 0
1152
+ text: ({ folderId }, { routineFolder }) => routineFolder === null ? `Routine folder with ID ${folderId} not found` : void 0,
1153
+ telemetry: ({ routineFolder }) => ({ itemCountBucket: bucketCount(routineFolder ? 1 : 0) })
1028
1154
  });
1029
1155
  const bodyMeasurementsResponse = defineStructuredResponseContract({
1030
1156
  outputSchema: bodyMeasurementsOutputSchema,
1031
1157
  normalize: (measurements) => ({ bodyMeasurements: measurements?.map(formatBodyMeasurement) ?? [] }),
1032
1158
  legacyJson: ({ bodyMeasurements }) => bodyMeasurements,
1033
- text: (_data, { bodyMeasurements }) => bodyMeasurements.length === 0 ? "No body measurements found for the specified parameters" : void 0
1159
+ text: (_data, { bodyMeasurements }) => bodyMeasurements.length === 0 ? "No body measurements found for the specified parameters" : void 0,
1160
+ telemetry: (measurements) => ({ itemCountBucket: bucketCount(measurements?.length ?? 0) })
1034
1161
  });
1035
1162
  const bodyMeasurementResponse = defineStructuredResponseContract({
1036
1163
  outputSchema: bodyMeasurementOutputSchema,
1037
1164
  normalize: (data) => ({ bodyMeasurement: data.bodyMeasurement ? formatBodyMeasurement(data.bodyMeasurement) : null }),
1038
1165
  legacyJson: ({ bodyMeasurement }) => bodyMeasurement,
1039
- text: ({ date }, { bodyMeasurement }) => bodyMeasurement === null ? `No body measurement found for date ${date}` : void 0
1166
+ text: ({ date }, { bodyMeasurement }) => bodyMeasurement === null ? `No body measurement found for date ${date}` : void 0,
1167
+ telemetry: ({ bodyMeasurement }) => ({ itemCountBucket: bucketCount(bodyMeasurement ? 1 : 0) })
1040
1168
  });
1041
1169
  const userResponse = defineStructuredResponseContract({
1042
1170
  outputSchema: userOutputSchema,
1043
1171
  normalize: (user) => ({ user: user ?? null }),
1044
1172
  legacyJson: ({ user }) => user,
1045
- text: (_data, { user }) => user === null ? "No user info found for the authenticated user" : void 0
1173
+ text: (_data, { user }) => user === null ? "No user info found for the authenticated user" : void 0,
1174
+ telemetry: (user) => ({ itemCountBucket: bucketCount(user ? 1 : 0) })
1046
1175
  });
1047
1176
  const trainingSummaryResponse = defineStructuredResponseContract({
1048
1177
  outputSchema: trainingSummaryOutputSchema,
1049
1178
  normalize: (data) => data,
1050
1179
  legacyJson: (output) => output,
1051
- text: (data) => data.workouts.count === 0 && data.bodyMeasurements.count === 0 ? "No workouts or body measurements found for the specified period" : void 0
1180
+ text: (data) => data.workouts.count === 0 && data.bodyMeasurements.count === 0 ? "No workouts or body measurements found for the specified period" : void 0,
1181
+ telemetry: (data) => ({
1182
+ itemCountBucket: bucketCount(data.workouts.count + data.bodyMeasurements.count),
1183
+ workflow: workflowResultTelemetry(data.workflow)
1184
+ })
1052
1185
  });
1053
1186
  const compactRoutinesResponse = defineStructuredResponseContract({
1054
1187
  outputSchema: compactRoutinesOutputSchema,
1055
1188
  normalize: (data) => data,
1056
1189
  legacyJson: ({ routines }) => routines,
1057
- text: (_data, { routines }) => routines.length === 0 ? "No routines found matching the query" : void 0
1190
+ text: (_data, { routines }) => routines.length === 0 ? "No routines found matching the query" : void 0,
1191
+ telemetry: (data) => ({
1192
+ itemCountBucket: bucketCount(data.routines.length),
1193
+ workflow: workflowResultTelemetry(data.workflow)
1194
+ })
1058
1195
  });
1059
- const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json: formatWorkout(workout) } : { text: "Failed to create workout: Server returned no data" });
1060
- const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json: formatWorkout(data.workout) } : { text: `Failed to update workout with ID ${data.workoutId}` });
1196
+ const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json: formatWorkout(workout) } : { text: "Failed to create workout: Server returned no data" }, (workout) => workoutResultTelemetry(workout));
1197
+ const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json: formatWorkout(data.workout) } : { text: `Failed to update workout with ID ${data.workoutId}` }, (data) => workoutResultTelemetry(data.workout));
1061
1198
  const repRangeDisplayWarningText = "Note: Hevy's public API stores rep ranges (rep_range), but the Hevy apps may not display them because they rely on an internal-only exercise field (input_modifier). See https://github.com/chrisdoc/hevy-mcp/issues/261 for details/workarounds.";
1062
1199
  const createRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
1063
1200
  json: formatRoutine(data.routine),
1064
1201
  additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
1065
- } : { text: "Failed to create routine: Server returned no data" });
1202
+ } : { text: "Failed to create routine: Server returned no data" }, (data) => routineResultTelemetry(data.routine));
1066
1203
  const updateRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
1067
1204
  json: formatRoutine(data.routine),
1068
1205
  additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
1069
- } : { text: `Failed to update routine with ID ${data.routineId}` });
1206
+ } : { text: `Failed to update routine with ID ${data.routineId}` }, (data) => routineResultTelemetry(data.routine));
1070
1207
  const createExerciseTemplateResponse = defineJsonResponseContract((response) => ({ json: {
1071
1208
  id: response?.id,
1072
1209
  message: "Exercise template created successfully"
1073
1210
  } }));
1074
- const createRoutineFolderResponse = defineJsonResponseContract((folder) => folder ? { json: formatRoutineFolder(folder) } : { text: "Failed to create routine folder: Server returned no data" });
1075
- const createBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} created successfully.` }));
1076
- const updateBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} updated successfully.` }));
1211
+ const createRoutineFolderResponse = defineJsonResponseContract((folder) => folder ? { json: formatRoutineFolder(folder) } : { text: "Failed to create routine folder: Server returned no data" }, (folder) => ({ itemCountBucket: bucketCount(folder ? 1 : 0) }));
1212
+ const createBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} created successfully.` }), () => ({ itemCountBucket: "1" }));
1213
+ const updateBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} updated successfully.` }), () => ({ itemCountBucket: "1" }));
1077
1214
  //#endregion
1078
1215
  //#region src/resources/hevy.ts
1079
1216
  const JSON_MIME_TYPE = "application/json";
@@ -1126,7 +1263,7 @@ function registerHevyResources(server, runtime) {
1126
1263
  //#endregion
1127
1264
  //#region src/server-metadata.ts
1128
1265
  const SERVER_NAME = "hevy-mcp";
1129
- const SERVER_VERSION = "3.3.0";
1266
+ const SERVER_VERSION = "3.4.1";
1130
1267
  const SERVER_INSTRUCTIONS = [
1131
1268
  "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.",
1132
1269
  "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.",
@@ -1181,8 +1318,11 @@ function createTypedToolHandler(schema, handler) {
1181
1318
  //#region src/tools/define-tool.ts
1182
1319
  function registerToolDefinition(server, runtime, definition) {
1183
1320
  const directHandler = createTypedToolHandler(definition.inputSchema, async (args) => respond(definition.responseContract, await definition.execute(runtime, args)));
1184
- const handler = runtime.wrapHandler(directHandler, definition.name);
1185
- const callback = (args) => handler(args);
1321
+ const callback = runtime.wrapHandler(directHandler, definition.name, {
1322
+ feature: definition.feature,
1323
+ kind: definition.kind,
1324
+ operation: definition.operation
1325
+ });
1186
1326
  if (definition.kind === "read") {
1187
1327
  server.registerTool(definition.name, {
1188
1328
  description: definition.description,
@@ -1486,6 +1626,8 @@ const updateBodyMeasurementSchema = {
1486
1626
  const bodyMeasurementToolDefinitions = [
1487
1627
  {
1488
1628
  name: "get-body-measurements",
1629
+ feature: "measurements",
1630
+ operation: "list",
1489
1631
  description: describeTool({
1490
1632
  summary: "Read-only. Lists dated body measurements for the account.",
1491
1633
  aliases: [
@@ -1511,6 +1653,8 @@ const bodyMeasurementToolDefinitions = [
1511
1653
  },
1512
1654
  {
1513
1655
  name: "get-body-measurement",
1656
+ feature: "measurements",
1657
+ operation: "get",
1514
1658
  description: describeTool({
1515
1659
  summary: "Read-only. Retrieves the body measurement entry for one date.",
1516
1660
  aliases: [
@@ -1536,6 +1680,8 @@ const bodyMeasurementToolDefinitions = [
1536
1680
  },
1537
1681
  {
1538
1682
  name: "create-body-measurement",
1683
+ feature: "measurements",
1684
+ operation: "create",
1539
1685
  description: describeTool({
1540
1686
  summary: "Writes to the Hevy account by creating a dated body measurement.",
1541
1687
  aliases: [
@@ -1561,6 +1707,8 @@ const bodyMeasurementToolDefinitions = [
1561
1707
  },
1562
1708
  {
1563
1709
  name: "update-body-measurement",
1710
+ feature: "measurements",
1711
+ operation: "update",
1564
1712
  description: describeTool({
1565
1713
  summary: "Mutates the Hevy account by updating a body measurement for a date.",
1566
1714
  aliases: [
@@ -1595,6 +1743,8 @@ const createRoutineFolderSchema = { name: z.string().min(1) };
1595
1743
  const folderToolDefinitions = [
1596
1744
  {
1597
1745
  name: "get-routine-folders",
1746
+ feature: "folders",
1747
+ operation: "list",
1598
1748
  description: describeTool({
1599
1749
  summary: "Read-only. Lists default and custom routine folders.",
1600
1750
  aliases: [
@@ -1620,6 +1770,8 @@ const folderToolDefinitions = [
1620
1770
  },
1621
1771
  {
1622
1772
  name: "get-routine-folder",
1773
+ feature: "folders",
1774
+ operation: "get",
1623
1775
  description: describeTool({
1624
1776
  summary: "Read-only. Retrieves one routine folder's metadata by ID.",
1625
1777
  aliases: [
@@ -1645,6 +1797,8 @@ const folderToolDefinitions = [
1645
1797
  },
1646
1798
  {
1647
1799
  name: "create-routine-folder",
1800
+ feature: "folders",
1801
+ operation: "create",
1648
1802
  description: describeTool({
1649
1803
  summary: "Writes to the Hevy account by creating a new routine folder.",
1650
1804
  aliases: [
@@ -1673,6 +1827,8 @@ const getRoutinesSchema = paginationShape({
1673
1827
  });
1674
1828
  const getRoutinesDefinition = {
1675
1829
  name: "get-routines",
1830
+ feature: "routines",
1831
+ operation: "list",
1676
1832
  description: describeTool({
1677
1833
  summary: "Read-only. Lists custom and default workout routines.",
1678
1834
  aliases: [
@@ -1698,6 +1854,8 @@ const getRoutinesDefinition = {
1698
1854
  const getRoutineSchema = { routineId: nonEmptyId };
1699
1855
  const getRoutineDefinition = {
1700
1856
  name: "get-routine",
1857
+ feature: "routines",
1858
+ operation: "get",
1701
1859
  description: describeTool({
1702
1860
  summary: "Read-only. Retrieves one routine and its exercise configuration by ID.",
1703
1861
  aliases: [
@@ -1724,6 +1882,8 @@ const routineExercisesSchema = z.preprocess(parseJsonArray, z.array(z.object(rou
1724
1882
  const createRoutineSchema = routinePayloadShape;
1725
1883
  const createRoutineDefinition = {
1726
1884
  name: "create-routine",
1885
+ feature: "routines",
1886
+ operation: "create",
1727
1887
  description: describeTool({
1728
1888
  summary: "Writes to the Hevy account by creating a new workout routine.",
1729
1889
  aliases: [
@@ -1758,6 +1918,8 @@ const routineToolDefinitions = [
1758
1918
  createRoutineDefinition,
1759
1919
  {
1760
1920
  name: "update-routine",
1921
+ feature: "routines",
1922
+ operation: "update",
1761
1923
  description: describeTool({
1762
1924
  summary: "Mutates the Hevy account by replacing an existing routine's content.",
1763
1925
  aliases: [
@@ -1811,6 +1973,8 @@ const searchExerciseTemplatesSchema = {
1811
1973
  const templateToolDefinitions = [
1812
1974
  {
1813
1975
  name: "get-exercise-templates",
1976
+ feature: "templates",
1977
+ operation: "list",
1814
1978
  description: describeTool({
1815
1979
  summary: "Read-only. Lists default and custom exercise templates with equipment and muscle metadata.",
1816
1980
  aliases: [
@@ -1836,6 +2000,8 @@ const templateToolDefinitions = [
1836
2000
  },
1837
2001
  {
1838
2002
  name: "get-exercise-template",
2003
+ feature: "templates",
2004
+ operation: "get",
1839
2005
  description: describeTool({
1840
2006
  summary: "Read-only. Retrieves complete metadata for one exercise template by ID.",
1841
2007
  aliases: [
@@ -1861,6 +2027,8 @@ const templateToolDefinitions = [
1861
2027
  },
1862
2028
  {
1863
2029
  name: "get-exercise-history",
2030
+ feature: "templates",
2031
+ operation: "get",
1864
2032
  description: describeTool({
1865
2033
  summary: "Read-only. Retrieves past performed sets for one exercise template.",
1866
2034
  aliases: [
@@ -1889,6 +2057,8 @@ const templateToolDefinitions = [
1889
2057
  },
1890
2058
  {
1891
2059
  name: "create-exercise-template",
2060
+ feature: "templates",
2061
+ operation: "create",
1892
2062
  description: describeTool({
1893
2063
  summary: "Writes to the Hevy account by creating a custom exercise template.",
1894
2064
  aliases: [
@@ -1916,6 +2086,8 @@ const templateToolDefinitions = [
1916
2086
  },
1917
2087
  {
1918
2088
  name: "search-exercise-templates",
2089
+ feature: "templates",
2090
+ operation: "search",
1919
2091
  description: describeTool({
1920
2092
  summary: "Read-only for the Hevy account. Searches the full exercise template catalog by title substring.",
1921
2093
  aliases: [
@@ -1965,6 +2137,8 @@ const templateToolDefinitions = [
1965
2137
  ];
1966
2138
  const userToolDefinitions = [{
1967
2139
  name: "get-user-info",
2140
+ feature: "profile",
2141
+ operation: "get",
1968
2142
  description: describeTool({
1969
2143
  summary: "Read-only. Returns the authenticated account's user ID, display name, and public profile URL.",
1970
2144
  aliases: [
@@ -2033,6 +2207,8 @@ async function discoverRoutines(runtime, { query, limit }) {
2033
2207
  }
2034
2208
  const routineDiscoveryToolDefinitions = [{
2035
2209
  name: "search-routines",
2210
+ feature: "workflows",
2211
+ operation: "search",
2036
2212
  description: describeTool({
2037
2213
  summary: "Read-only. Discovers routines by title and returns compact metadata without full set payloads.",
2038
2214
  aliases: [
@@ -2194,6 +2370,8 @@ async function getTrainingSummary(runtime, weeks) {
2194
2370
  }
2195
2371
  const workflowToolDefinitions = [{
2196
2372
  name: "get-training-summary",
2373
+ feature: "workflows",
2374
+ operation: "get",
2197
2375
  description: describeTool({
2198
2376
  summary: "Read-only. Summarizes recent workout activity and body-measurement trends in one call.",
2199
2377
  aliases: [
@@ -2237,6 +2415,8 @@ const hevyToolDefinitions = [
2237
2415
  ...[
2238
2416
  {
2239
2417
  name: "get-workouts",
2418
+ feature: "workouts",
2419
+ operation: "list",
2240
2420
  description: describeTool({
2241
2421
  summary: "Read-only. Lists workouts from newest to oldest with exercise and timing details.",
2242
2422
  aliases: [
@@ -2261,6 +2441,8 @@ const hevyToolDefinitions = [
2261
2441
  },
2262
2442
  {
2263
2443
  name: "get-workout",
2444
+ feature: "workouts",
2445
+ operation: "get",
2264
2446
  description: describeTool({
2265
2447
  summary: "Read-only. Retrieves complete details for one workout by its ID.",
2266
2448
  aliases: [
@@ -2285,6 +2467,8 @@ const hevyToolDefinitions = [
2285
2467
  },
2286
2468
  {
2287
2469
  name: "get-workout-count",
2470
+ feature: "workouts",
2471
+ operation: "count",
2288
2472
  description: describeTool({
2289
2473
  summary: "Read-only. Returns the total workout count for the account.",
2290
2474
  aliases: [
@@ -2306,6 +2490,8 @@ const hevyToolDefinitions = [
2306
2490
  },
2307
2491
  {
2308
2492
  name: "get-workout-events",
2493
+ feature: "workouts",
2494
+ operation: "sync",
2309
2495
  description: describeTool({
2310
2496
  summary: "Read-only. Lists workout update and delete events since a timestamp, newest first.",
2311
2497
  aliases: [
@@ -2334,6 +2520,8 @@ const hevyToolDefinitions = [
2334
2520
  },
2335
2521
  {
2336
2522
  name: "create-workout",
2523
+ feature: "workouts",
2524
+ operation: "create",
2337
2525
  description: describeTool({
2338
2526
  summary: "Writes to the Hevy account by creating a new workout.",
2339
2527
  aliases: [
@@ -2354,6 +2542,8 @@ const hevyToolDefinitions = [
2354
2542
  },
2355
2543
  {
2356
2544
  name: "update-workout",
2545
+ feature: "workouts",
2546
+ operation: "update",
2357
2547
  description: describeTool({
2358
2548
  summary: "Mutates the Hevy account by replacing an existing workout.",
2359
2549
  aliases: [
@@ -2389,70 +2579,6 @@ function registerHevyTools(server, runtime) {
2389
2579
  for (const definition of hevyToolDefinitions) registerToolDefinition(server, runtime, definition);
2390
2580
  }
2391
2581
  //#endregion
2392
- //#region src/utils/error-handler.ts
2393
- /**
2394
- * Centralized error handling utility for MCP tools
2395
- */
2396
- /**
2397
- * Create a standardized error response for MCP tools
2398
- *
2399
- * @param error - The error object or message
2400
- * @param context - Optional context information about where the error occurred
2401
- * @returns A formatted MCP tool response with error information
2402
- */
2403
- function createErrorResponse(error, context) {
2404
- const policy = resolveErrorPolicy(error, "The request failed unexpectedly. Please try again.", HEVY_CLIENT_NOT_INITIALIZED_ERROR);
2405
- const { diagnostic } = policy;
2406
- const axiosErrorContext = diagnostic.status !== void 0 || diagnostic.method !== void 0 || diagnostic.endpoint !== void 0 ? {
2407
- status: diagnostic.status,
2408
- method: diagnostic.method,
2409
- url: diagnostic.endpoint
2410
- } : null;
2411
- const errorContext = {
2412
- sourceContext: context,
2413
- originalErrorMessage: `${diagnostic.category} occurred`,
2414
- errorCode: diagnostic.code,
2415
- errorType: policy.type,
2416
- axios: axiosErrorContext ?? void 0
2417
- };
2418
- const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${policy.message}`;
2419
- console.error("MCP tool failure", diagnostic);
2420
- return {
2421
- content: [{
2422
- type: "text",
2423
- text: formattedMessage
2424
- }],
2425
- isError: true,
2426
- errorContext
2427
- };
2428
- }
2429
- /**
2430
- * Wrap an async function with standardized error handling
2431
- *
2432
- * This function preserves the parameter types of the wrapped function while
2433
- * providing error handling. The returned function accepts Record<string, unknown>
2434
- * (as required by MCP SDK) but internally casts to the original parameter type.
2435
- *
2436
- * @param fn - The async function to wrap
2437
- * @param context - Context information for error messages
2438
- * @returns A function that catches errors and returns standardized error responses
2439
- */
2440
- function withErrorHandling(fn, context, onError) {
2441
- return async (rawArgs) => {
2442
- const args = rawArgs ?? {};
2443
- try {
2444
- return await fn(args);
2445
- } catch (error) {
2446
- try {
2447
- onError?.(error, context, Object.keys(args).length);
2448
- } catch {
2449
- console.error("MCP error observer failure", { category: "ObserverError" });
2450
- }
2451
- return createErrorResponse(error, context);
2452
- }
2453
- };
2454
- }
2455
- //#endregion
2456
2582
  //#region src/utils/cache.ts
2457
2583
  /**
2458
2584
  * Shared in-memory cache for async fetches with TTL, LRU eviction,
@@ -3315,7 +3441,8 @@ function createNativeClient(apiKey, baseUrl, options) {
3315
3441
  method,
3316
3442
  endpoint,
3317
3443
  status: response.status,
3318
- durationMs: Date.now() - startedAt
3444
+ durationMs: Date.now() - startedAt,
3445
+ retryCount
3319
3446
  });
3320
3447
  return {
3321
3448
  data,
@@ -3330,14 +3457,21 @@ function createNativeClient(apiKey, baseUrl, options) {
3330
3457
  code: normalized.signal?.aborted ? HEVY_REQUEST_ABORTED_ERROR_CODE : getNetworkCode(cause),
3331
3458
  cause
3332
3459
  });
3460
+ const canRetry = method === "GET" && isRetryable(error);
3461
+ if (canRetry && retryCount >= maxGetRetries) {
3462
+ error.hevyRetryExhausted = true;
3463
+ error.hevyRetryCount = retryCount;
3464
+ error.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
3465
+ }
3333
3466
  options.onRequestComplete?.({
3334
3467
  method,
3335
3468
  endpoint,
3336
3469
  status: error.status ?? 0,
3337
3470
  durationMs: Date.now() - startedAt,
3471
+ retryCount,
3338
3472
  error
3339
3473
  });
3340
- if (!(method === "GET" && isRetryable(error))) {
3474
+ if (!canRetry) {
3341
3475
  emitClientLog(options.logger, {
3342
3476
  level: "error",
3343
3477
  logger: "hevy-api",
@@ -3350,12 +3484,7 @@ function createNativeClient(apiKey, baseUrl, options) {
3350
3484
  });
3351
3485
  throw error;
3352
3486
  }
3353
- if (retryCount >= maxGetRetries) {
3354
- error.hevyRetryExhausted = true;
3355
- error.hevyRetryCount = retryCount;
3356
- error.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
3357
- throw error;
3358
- }
3487
+ if (error.hevyRetryExhausted) throw error;
3359
3488
  retryCount += 1;
3360
3489
  const delayMs = getRetryDelayMs(error, retryCount);
3361
3490
  emitClientLog(options.logger, {
@@ -3449,8 +3578,69 @@ function createMcpClientLogger(server) {
3449
3578
  };
3450
3579
  }
3451
3580
  //#endregion
3581
+ //#region src/utils/error-handler.ts
3582
+ /**
3583
+ * Centralized error handling utility for MCP tools
3584
+ */
3585
+ /**
3586
+ * Create a standardized error response for MCP tools
3587
+ *
3588
+ * @param error - The error object or message
3589
+ * @param context - Optional context information about where the error occurred
3590
+ * @returns A formatted MCP tool response with error information
3591
+ */
3592
+ function createErrorResponse(error, context) {
3593
+ const policy = resolveErrorPolicy(error, "The request failed unexpectedly. Please try again.", HEVY_CLIENT_NOT_INITIALIZED_ERROR);
3594
+ const { diagnostic } = policy;
3595
+ const axiosErrorContext = diagnostic.status !== void 0 || diagnostic.method !== void 0 || diagnostic.endpoint !== void 0 ? {
3596
+ status: diagnostic.status,
3597
+ method: diagnostic.method,
3598
+ url: diagnostic.endpoint
3599
+ } : null;
3600
+ const errorContext = {
3601
+ sourceContext: context,
3602
+ originalErrorMessage: `${diagnostic.category} occurred`,
3603
+ errorCode: diagnostic.code,
3604
+ errorType: policy.type,
3605
+ axios: axiosErrorContext ?? void 0
3606
+ };
3607
+ const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${policy.message}`;
3608
+ console.error("MCP tool failure", diagnostic);
3609
+ return {
3610
+ content: [{
3611
+ type: "text",
3612
+ text: formattedMessage
3613
+ }],
3614
+ isError: true,
3615
+ errorContext
3616
+ };
3617
+ }
3618
+ /**
3619
+ * Wrap an async function with standardized error handling
3620
+ *
3621
+ * @param fn - The async function to wrap
3622
+ * @param context - Context information for error messages
3623
+ * @returns A function that catches errors and returns standardized error responses
3624
+ */
3625
+ function withErrorHandling(fn, context, onError) {
3626
+ return async (rawArgs) => {
3627
+ const normalizedArgs = rawArgs ?? {};
3628
+ try {
3629
+ return await fn(normalizedArgs);
3630
+ } catch (error) {
3631
+ try {
3632
+ onError?.(error, context, Object.keys(normalizedArgs).length);
3633
+ } catch {
3634
+ console.error("MCP error observer failure", { category: "ObserverError" });
3635
+ }
3636
+ return createErrorResponse(error, context);
3637
+ }
3638
+ };
3639
+ }
3640
+ //#endregion
3452
3641
  //#region src/tools/tool-runtime.ts
3453
- function createToolRuntime({ client, catalog, logger, wrapHandler = withErrorHandling }) {
3642
+ const defaultToolHandlerWrapper = (fn, context) => withErrorHandling(fn, context);
3643
+ function createToolRuntime({ client, catalog, logger, wrapHandler = defaultToolHandlerWrapper }) {
3454
3644
  return {
3455
3645
  client,
3456
3646
  catalog,
@@ -3502,7 +3692,7 @@ function createSharedMcpServer(options) {
3502
3692
  client: hevyClient,
3503
3693
  catalog: createExerciseTemplateCatalog(hevyClient),
3504
3694
  logger,
3505
- wrapHandler: options.wrapHandler ?? withErrorHandling
3695
+ wrapHandler: options.wrapHandler ?? defaultToolHandlerWrapper
3506
3696
  });
3507
3697
  const counting = createToolCountingServer(server);
3508
3698
  registerHevyTools(counting.server, runtime);
@@ -3541,10 +3731,24 @@ function flushStdout(stdout = process.stdout) {
3541
3731
  }
3542
3732
  });
3543
3733
  }
3544
- function installGracefulShutdown({ target, process: processLike = process, logError = console.error, flush = flushStdout, forcedExitTimeoutMs = FORCED_EXIT_TIMEOUT_MS, scheduleForcedExit = setTimeout }) {
3734
+ function installGracefulShutdown({ target, process: processLike = process, logError = console.error, flush = flushStdout, forcedExitTimeoutMs = FORCED_EXIT_TIMEOUT_MS, scheduleForcedExit = setTimeout, onComplete }) {
3545
3735
  let listenersInstalled = true;
3546
3736
  let shutdownSettled = false;
3547
3737
  let shutdownPromise;
3738
+ let completionReported = false;
3739
+ const reportCompletion = (succeeded) => {
3740
+ if (completionReported) return void 0;
3741
+ completionReported = true;
3742
+ try {
3743
+ const completion = onComplete?.(succeeded);
3744
+ return completion ? Promise.resolve(completion).catch(() => {
3745
+ logError("Graceful shutdown completion observer failed");
3746
+ }) : void 0;
3747
+ } catch {
3748
+ logError("Graceful shutdown completion observer failed");
3749
+ return;
3750
+ }
3751
+ };
3548
3752
  const cleanup = () => {
3549
3753
  if (!listenersInstalled || shutdownPromise && !shutdownSettled) return;
3550
3754
  listenersInstalled = false;
@@ -3557,7 +3761,8 @@ function installGracefulShutdown({ target, process: processLike = process, logEr
3557
3761
  if (shutdownPromise) return;
3558
3762
  if (processLike.exitCode == null) processLike.exitCode = 0;
3559
3763
  scheduleForcedExit(() => {
3560
- processLike.exit(processLike.exitCode ?? 0);
3764
+ reportCompletion(false);
3765
+ processLike.exit(shutdownSettled ? processLike.exitCode ?? 0 : 1);
3561
3766
  }, forcedExitTimeoutMs).unref();
3562
3767
  shutdownPromise = (async () => {
3563
3768
  logError(`Shutting down gracefully after ${signal}`);
@@ -3582,6 +3787,7 @@ function installGracefulShutdown({ target, process: processLike = process, logEr
3582
3787
  return;
3583
3788
  }
3584
3789
  } finally {
3790
+ await reportCompletion(!shutdownFailed);
3585
3791
  shutdownSettled = true;
3586
3792
  cleanup();
3587
3793
  }
@@ -3694,135 +3900,304 @@ function createNodeHevyClientOptions() {
3694
3900
  return {
3695
3901
  ...timeoutMs ? { timeoutMs } : {},
3696
3902
  onRequestComplete(observation) {
3903
+ const retryCountBucket = bucketCount(observation.retryCount);
3904
+ const diagnostic = observation.error ? createSafeErrorDiagnostic(observation.error) : void 0;
3905
+ const safeErrorAttributes = diagnostic ? {
3906
+ error_category: diagnostic.category,
3907
+ ...diagnostic.code ? { error_code: diagnostic.code } : {}
3908
+ } : {};
3697
3909
  const span = tracer.startSpan(`hevy.api.${observation.method}`, { attributes: {
3698
3910
  "http.method": observation.method,
3699
3911
  "http.status_code": observation.status,
3912
+ "hevy.api.retry_count_bucket": retryCountBucket,
3700
3913
  "hevy.api.endpoint": observation.endpoint
3701
3914
  } });
3702
3915
  span.setStatus({ code: observation.error ? SpanStatusCode.ERROR : SpanStatusCode.OK });
3703
- if (observation.error) {
3704
- const diagnostic = createSafeErrorDiagnostic(observation.error);
3705
- span.addEvent("hevy.api.failure", {
3706
- "error.category": diagnostic.category,
3707
- ...diagnostic.code ? { "error.code": diagnostic.code } : {}
3708
- });
3709
- }
3916
+ if (diagnostic) span.addEvent("hevy.api.failure", {
3917
+ "error.category": diagnostic.category,
3918
+ ...diagnostic.code ? { "error.code": diagnostic.code } : {}
3919
+ });
3710
3920
  span.end();
3711
3921
  apiCalls.add(1, {
3712
3922
  method: observation.method,
3713
3923
  endpoint: observation.endpoint,
3714
- status_code: observation.status
3924
+ status_code: observation.status,
3925
+ retry_count_bucket: retryCountBucket,
3926
+ ...safeErrorAttributes
3715
3927
  });
3716
3928
  apiDuration.record(observation.durationMs, {
3717
3929
  method: observation.method,
3718
- endpoint: observation.endpoint
3930
+ endpoint: observation.endpoint,
3931
+ retry_count_bucket: retryCountBucket,
3932
+ ...safeErrorAttributes
3719
3933
  });
3720
3934
  debugLog("api_response", {
3721
3935
  method: observation.method,
3722
3936
  endpoint: observation.endpoint,
3723
3937
  durationMs: observation.durationMs,
3724
- status: observation.status || null
3938
+ status: observation.status || null,
3939
+ retryCountBucket,
3940
+ ...safeErrorAttributes
3725
3941
  });
3726
3942
  }
3727
3943
  };
3728
3944
  }
3729
3945
  //#endregion
3946
+ //#region src/utils/mcp-session-observability.ts
3947
+ const UNKNOWN_METADATA = "unknown";
3948
+ const MAX_METADATA_LENGTH = 64;
3949
+ const SAFE_METADATA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/@-]{0,63}$/u;
3950
+ let currentSession;
3951
+ function isRecord(value) {
3952
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3953
+ }
3954
+ function normalizeMetadata(value) {
3955
+ if (typeof value !== "string") return UNKNOWN_METADATA;
3956
+ const normalized = value.trim();
3957
+ if (normalized.length === 0 || normalized.length > MAX_METADATA_LENGTH || !SAFE_METADATA_PATTERN.test(normalized)) return UNKNOWN_METADATA;
3958
+ return normalized;
3959
+ }
3960
+ function getInitializeParams(message) {
3961
+ if (!isRecord(message) || message.method !== "initialize") return {};
3962
+ return isRecord(message.params) ? message.params : {};
3963
+ }
3964
+ function extractMcpClientMetadata(message) {
3965
+ const params = getInitializeParams(message);
3966
+ const clientInfo = isRecord(params.clientInfo) ? params.clientInfo : {};
3967
+ return {
3968
+ name: normalizeMetadata(clientInfo.name),
3969
+ version: normalizeMetadata(clientInfo.version),
3970
+ protocolVersion: normalizeMetadata(params.protocolVersion)
3971
+ };
3972
+ }
3973
+ function metadataAttributes(metadata) {
3974
+ return {
3975
+ client_name: metadata.name,
3976
+ client_version: metadata.version,
3977
+ protocol_version: metadata.protocolVersion,
3978
+ transport: "stdio"
3979
+ };
3980
+ }
3981
+ function durationBucket(durationMs) {
3982
+ if (durationMs < 1e3) return "<1s";
3983
+ if (durationMs < 1e4) return "1-10s";
3984
+ if (durationMs < 6e4) return "10-60s";
3985
+ if (durationMs < 3e5) return "1-5m";
3986
+ return "5m+";
3987
+ }
3988
+ function recordMcpSessionStart(message) {
3989
+ const metadata = extractMcpClientMetadata(message);
3990
+ currentSession = {
3991
+ metadata,
3992
+ startedAt: Date.now(),
3993
+ toolCalls: 0,
3994
+ hadToolFailure: false
3995
+ };
3996
+ const attributes = metadataAttributes(metadata);
3997
+ sessionStarted.add(1, attributes);
3998
+ return metadata;
3999
+ }
4000
+ function recordMcpToolInvocation() {
4001
+ if (currentSession) currentSession.toolCalls += 1;
4002
+ return metadataAttributes(currentSession?.metadata ?? {
4003
+ name: UNKNOWN_METADATA,
4004
+ version: UNKNOWN_METADATA,
4005
+ protocolVersion: UNKNOWN_METADATA
4006
+ });
4007
+ }
4008
+ function recordMcpToolFailure() {
4009
+ if (currentSession) currentSession.hadToolFailure = true;
4010
+ }
4011
+ function getCurrentMcpClientMetadata() {
4012
+ return currentSession?.metadata ?? {
4013
+ name: UNKNOWN_METADATA,
4014
+ version: UNKNOWN_METADATA,
4015
+ protocolVersion: UNKNOWN_METADATA
4016
+ };
4017
+ }
4018
+ function recordMcpSessionTermination(category) {
4019
+ const session = currentSession;
4020
+ const durationMs = session ? Math.max(0, Date.now() - session.startedAt) : 0;
4021
+ const attributes = {
4022
+ ...metadataAttributes(session?.metadata ?? {
4023
+ name: UNKNOWN_METADATA,
4024
+ version: UNKNOWN_METADATA,
4025
+ protocolVersion: UNKNOWN_METADATA
4026
+ }),
4027
+ termination_category: category,
4028
+ session_duration_bucket: durationBucket(durationMs),
4029
+ tool_calls_bucket: bucketCount(session?.toolCalls ?? 0)
4030
+ };
4031
+ sessionEnded.add(1, attributes);
4032
+ currentSession = void 0;
4033
+ }
4034
+ function resolveSessionTerminationCategory(shutdownSucceeded) {
4035
+ if (!shutdownSucceeded) return "unknown";
4036
+ return currentSession?.hadToolFailure ? "tool_failure" : "clean";
4037
+ }
4038
+ //#endregion
3730
4039
  //#region src/utils/telemetry-wrapper.ts
3731
- /** Whitelist of safe argument keys that can be logged without exposing PII. */
3732
- const ARGUMENT_WHITELIST = /* @__PURE__ */ new Set([
3733
- "page",
3734
- "pageSize",
3735
- "since",
3736
- "workoutId",
3737
- "routineId",
3738
- "folderId",
3739
- "exerciseTemplateId",
3740
- "date",
3741
- "startDate",
3742
- "endDate",
3743
- "updatedSince",
3744
- "includeCustom",
3745
- "limit",
3746
- "offset",
3747
- "refresh",
3748
- "query",
3749
- "primaryMuscleGroup"
3750
- ]);
4040
+ const STRUCTURAL_ARGUMENT_KEYS = {
4041
+ page: true,
4042
+ pageSize: true,
4043
+ since: true,
4044
+ workoutId: true,
4045
+ routineId: true,
4046
+ folderId: true,
4047
+ exerciseTemplateId: true,
4048
+ date: true,
4049
+ startDate: true,
4050
+ endDate: true,
4051
+ updatedSince: true,
4052
+ includeCustom: true,
4053
+ limit: true,
4054
+ offset: true,
4055
+ refresh: true,
4056
+ query: true,
4057
+ primaryMuscleGroup: true
4058
+ };
4059
+ const BUCKETED_ARGUMENT_KEYS = {
4060
+ page: true,
4061
+ pageSize: true,
4062
+ limit: true,
4063
+ offset: true
4064
+ };
4065
+ const PRESENCE_ONLY_ARGUMENT_KEYS = {
4066
+ since: true,
4067
+ workoutId: true,
4068
+ routineId: true,
4069
+ folderId: true,
4070
+ exerciseTemplateId: true,
4071
+ date: true,
4072
+ startDate: true,
4073
+ endDate: true,
4074
+ updatedSince: true,
4075
+ query: true,
4076
+ primaryMuscleGroup: true
4077
+ };
3751
4078
  function extractSafeArgs(args) {
3752
4079
  const attributes = {};
4080
+ const keys = [];
3753
4081
  for (const [key, value] of Object.entries(args)) {
3754
- if (!ARGUMENT_WHITELIST.has(key)) continue;
3755
- if (typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") continue;
3756
- attributes[`mcp.tool.args.${key}`] = key === "query" && typeof value === "string" && value.length > 100 ? `${value.slice(0, 100)}...` : value;
4082
+ if (STRUCTURAL_ARGUMENT_KEYS[key] !== true) continue;
4083
+ keys.push(key);
4084
+ if (PRESENCE_ONLY_ARGUMENT_KEYS[key] === true) {
4085
+ if (value !== void 0 && value !== null) attributes[`mcp.tool.args.${key}.present`] = true;
4086
+ continue;
4087
+ }
4088
+ if (BUCKETED_ARGUMENT_KEYS[key] === true && typeof value === "number") {
4089
+ attributes[`mcp.tool.args.${key}.bucket`] = bucketCount(value);
4090
+ continue;
4091
+ }
4092
+ if (typeof value === "boolean") attributes[`mcp.tool.args.${key}`] = value;
3757
4093
  }
4094
+ return {
4095
+ attributes,
4096
+ keys
4097
+ };
4098
+ }
4099
+ function setWorkflowAttributes(span, workflow) {
4100
+ if (!workflow) return;
4101
+ span.setAttribute("workflow.name", workflow.name);
4102
+ span.setAttribute("workflow.cache_status", workflow.cacheStatus);
4103
+ span.setAttribute("workflow.items_scanned", workflow.itemsScanned);
4104
+ const allowedResources = {
4105
+ workouts: true,
4106
+ bodyMeasurements: true,
4107
+ routines: true
4108
+ };
4109
+ for (const [resource, pageCount] of Object.entries(workflow.pagination ?? {})) if (allowedResources[resource] === true && Number.isSafeInteger(pageCount) && pageCount >= 0) span.setAttribute(`workflow.pagination.${resource}.pages`, pageCount);
4110
+ }
4111
+ function setResultAttributes(span, result) {
4112
+ span.setAttribute("mcp.tool.result.is_error", Boolean(result.isError));
4113
+ span.setAttribute("mcp.tool.result.has_structured_content", "structuredContent" in result && result.structuredContent !== void 0);
4114
+ if (Array.isArray(result.content)) span.setAttribute("mcp.tool.result.content_count", result.content.length);
4115
+ const telemetry = getResultTelemetry(result);
4116
+ if (!telemetry) return;
4117
+ if (telemetry.itemCountBucket) span.setAttribute("mcp.tool.result.item_count_bucket", telemetry.itemCountBucket);
4118
+ if (telemetry.exerciseCountBucket) span.setAttribute("mcp.tool.result.exercise_count_bucket", telemetry.exerciseCountBucket);
4119
+ if (telemetry.setCountBucket) span.setAttribute("mcp.tool.result.set_count_bucket", telemetry.setCountBucket);
4120
+ setWorkflowAttributes(span, telemetry.workflow);
4121
+ }
4122
+ function resultMetricAttributes(result) {
4123
+ const attributes = { "mcp.tool.result.has_structured_content": "structuredContent" in result && result.structuredContent !== void 0 };
4124
+ const telemetry = getResultTelemetry(result);
4125
+ if (!telemetry) return attributes;
4126
+ if (telemetry.itemCountBucket) attributes["mcp.tool.result.item_count_bucket"] = telemetry.itemCountBucket;
4127
+ if (telemetry.exerciseCountBucket) attributes["mcp.tool.result.exercise_count_bucket"] = telemetry.exerciseCountBucket;
4128
+ if (telemetry.setCountBucket) attributes["mcp.tool.result.set_count_bucket"] = telemetry.setCountBucket;
3758
4129
  return attributes;
3759
4130
  }
3760
- function isRecord(value) {
3761
- return value !== null && typeof value === "object" && !Array.isArray(value);
4131
+ function taxonomyAttributes(metadata) {
4132
+ return metadata ? {
4133
+ "hevy.feature": metadata.feature,
4134
+ "mcp.tool.kind": metadata.kind,
4135
+ "mcp.tool.operation": metadata.operation
4136
+ } : {};
3762
4137
  }
3763
- function getWorkflowTelemetry(result) {
3764
- const structuredContent = result.structuredContent;
3765
- if (!isRecord(structuredContent)) return null;
3766
- const workflow = structuredContent.workflow;
3767
- if (!isRecord(workflow)) return null;
3768
- const name = workflow.name;
3769
- const paginationValue = workflow.pagination;
3770
- const cacheStatus = workflow.cacheStatus;
3771
- const itemsScanned = workflow.itemsScanned;
3772
- if (typeof name !== "string" || !isRecord(paginationValue) || typeof cacheStatus !== "string" || typeof itemsScanned !== "number" || !Number.isSafeInteger(itemsScanned) || itemsScanned < 0) return null;
3773
- const pagination = {};
3774
- for (const [key, value] of Object.entries(paginationValue)) if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) pagination[key] = value;
4138
+ function metricAttributes(context, metadata, clientAttributes) {
3775
4139
  return {
3776
- name,
3777
- pagination,
3778
- cacheStatus,
3779
- itemsScanned
4140
+ tool_name: context,
4141
+ ...taxonomyAttributes(metadata),
4142
+ ...clientAttributes
3780
4143
  };
3781
4144
  }
3782
4145
  /**
3783
- * Wrap an MCP tool handler with its existing OpenTelemetry span and metrics.
4146
+ * Privacy contract: this wrapper records only bounded tool taxonomy, structural
4147
+ * argument presence/buckets, safe result-shape metadata, and allowlisted error
4148
+ * diagnostics. It MUST NOT read or emit prompt text, tool arguments, result
4149
+ * bodies, identifiers, dates, titles, notes, descriptions, or measurements.
3784
4150
  */
3785
- function withTelemetry(fn, context) {
4151
+ function withTelemetry(fn, context, metadata) {
3786
4152
  return async (rawArgs) => {
3787
4153
  const args = rawArgs ?? {};
3788
4154
  if (isDebugEnabled()) debugLog("tool_invocation", {
3789
4155
  tool: context,
3790
4156
  params: redactToolArgs(args)
3791
4157
  });
4158
+ const { attributes: safeArgs, keys: safeArgumentKeys } = extractSafeArgs(args);
4159
+ const clientAttributes = recordMcpToolInvocation();
4160
+ let resultMetrics = {};
4161
+ const clientMetadata = getCurrentMcpClientMetadata();
4162
+ const metrics = metricAttributes(context, metadata, clientAttributes);
3792
4163
  const argumentKeyCount = Object.keys(args).length;
3793
4164
  const startTime = Date.now();
3794
- let isError = false;
3795
- toolInvocations.add(1, { tool_name: context });
3796
- const safeArgs = extractSafeArgs(args);
3797
- const whitelistedKeys = Object.keys(safeArgs).map((key) => key.replace("mcp.tool.args.", ""));
4165
+ let outcome = "success";
4166
+ toolInvocations.add(1, metrics);
3798
4167
  return tracer.startActiveSpan(`mcp.tool.${context}`, { attributes: {
3799
4168
  "mcp.tool.name": context,
3800
- "workflow.name": context,
3801
- "mcp.tool.args.key_count": argumentKeyCount,
3802
- "mcp.tool.args.keys": whitelistedKeys.join(","),
4169
+ ...taxonomyAttributes(metadata),
4170
+ "mcp.client.name": clientMetadata.name,
4171
+ "mcp.client.version": clientMetadata.version,
4172
+ "mcp.protocol.version": clientMetadata.protocolVersion,
4173
+ "mcp.transport": "stdio",
4174
+ "mcp.tool.args.key_count_bucket": bucketCount(argumentKeyCount),
4175
+ "mcp.tool.args.keys": safeArgumentKeys.join(","),
3803
4176
  ...safeArgs
3804
4177
  } }, async (span) => {
3805
4178
  try {
3806
4179
  const result = await fn(args);
3807
- isError = Boolean(result.isError);
3808
- span.setStatus({ code: isError ? SpanStatusCode.ERROR : SpanStatusCode.OK });
3809
- span.setAttribute("mcp.tool.result.is_error", isError);
3810
- if (result.content) {
3811
- span.setAttribute("mcp.tool.result.content_count", result.content.length);
3812
- const textLength = result.content.reduce((sum, item) => sum + (item.text?.length ?? 0), 0);
3813
- span.setAttribute("mcp.tool.result.text_length", textLength);
3814
- }
3815
- const workflow = getWorkflowTelemetry(result);
3816
- if (workflow) {
3817
- span.setAttribute("workflow.name", workflow.name);
3818
- span.setAttribute("workflow.cache_status", workflow.cacheStatus);
3819
- span.setAttribute("workflow.items_scanned", workflow.itemsScanned);
3820
- for (const [resource, pageCount] of Object.entries(workflow.pagination)) span.setAttribute(`workflow.pagination.${resource}.pages`, pageCount);
3821
- }
4180
+ const isError = Boolean(result.isError);
4181
+ if (isError) try {
4182
+ recordMcpToolFailure();
4183
+ } catch {}
4184
+ outcome = isError ? "returned_error" : "success";
4185
+ try {
4186
+ span.setStatus({ code: isError ? SpanStatusCode.ERROR : SpanStatusCode.OK });
4187
+ setResultAttributes(span, result);
4188
+ resultMetrics = resultMetricAttributes(result);
4189
+ span.setAttribute("mcp.tool.outcome", outcome);
4190
+ toolOutcomes.add(1, {
4191
+ ...metrics,
4192
+ outcome
4193
+ });
4194
+ } catch {}
3822
4195
  return result;
3823
4196
  } catch (error) {
3824
- isError = true;
4197
+ outcome = "thrown_error";
4198
+ recordMcpToolFailure();
3825
4199
  span.setStatus({ code: SpanStatusCode.ERROR });
4200
+ span.setAttribute("mcp.tool.outcome", outcome);
3826
4201
  const policy = resolveErrorPolicy(error, "");
3827
4202
  const { diagnostic } = policy;
3828
4203
  span.addEvent("mcp.tool.failure", {
@@ -3835,14 +4210,20 @@ function withTelemetry(fn, context) {
3835
4210
  const errorType = policy.type;
3836
4211
  span.setAttribute("error.type", errorType);
3837
4212
  toolErrors.add(1, {
3838
- tool_name: context,
4213
+ ...metrics,
3839
4214
  error_type: errorType
3840
4215
  });
4216
+ toolOutcomes.add(1, {
4217
+ ...metrics,
4218
+ outcome
4219
+ });
3841
4220
  throw error;
3842
4221
  } finally {
3843
4222
  toolDuration.record(Date.now() - startTime, {
3844
- tool_name: context,
3845
- is_error: String(isError)
4223
+ ...metrics,
4224
+ ...resultMetrics,
4225
+ is_error: String(outcome !== "success"),
4226
+ outcome
3846
4227
  });
3847
4228
  span.end();
3848
4229
  }
@@ -3851,18 +4232,29 @@ function withTelemetry(fn, context) {
3851
4232
  }
3852
4233
  //#endregion
3853
4234
  //#region src/utils/observability-wrapper.ts
3854
- /**
3855
- * Wrap an MCP tool handler with telemetry inside error response handling.
3856
- */
3857
- function withObservability(fn, context) {
3858
- return withErrorHandling(withTelemetry(fn, context), context, (error, _toolContext, argumentKeyCount) => {
4235
+ /** Wrap an MCP tool handler with telemetry inside error response handling. */
4236
+ function withObservability(fn, context, metadata) {
4237
+ return withErrorHandling(withTelemetry(fn, context, metadata), context, (error, toolContext, argumentKeyCount) => {
3859
4238
  const { diagnostic } = resolveErrorPolicy(error, "");
3860
4239
  Sentry.withScope((scope) => {
4240
+ scope.setTag("mcp.tool.context", toolContext);
3861
4241
  scope.setTag("error.category", diagnostic.category);
3862
4242
  if (diagnostic.code) scope.setTag("error.code", diagnostic.code);
3863
4243
  if (diagnostic.status !== void 0) scope.setTag("http.status_code", String(diagnostic.status));
3864
- scope.setContext("mcpTool", { argumentKeyCount });
4244
+ if (diagnostic.endpoint) scope.setTag("hevy.api.endpoint", diagnostic.endpoint);
4245
+ scope.setContext("mcpTool", {
4246
+ context: toolContext,
4247
+ argumentKeyCountBucket: bucketCount(argumentKeyCount)
4248
+ });
3865
4249
  scope.setContext("safeError", { ...diagnostic });
4250
+ scope.setFingerprint([
4251
+ "mcp-tool-failure",
4252
+ toolContext,
4253
+ diagnostic.category,
4254
+ diagnostic.code ?? "none",
4255
+ String(diagnostic.status ?? "none"),
4256
+ diagnostic.endpoint ?? "none"
4257
+ ]);
3866
4258
  Sentry.captureMessage("MCP tool failure", "error");
3867
4259
  });
3868
4260
  });
@@ -3872,6 +4264,18 @@ function withObservability(fn, context) {
3872
4264
  const UTF8_BOM = "";
3873
4265
  /** Maximum escaped characters included in a malformed stdin shape preview. */
3874
4266
  const STDIN_PARSE_SHAPE_PREVIEW_MAX_LENGTH = 200;
4267
+ const SAFE_MCP_METHODS = {
4268
+ initialize: true,
4269
+ "notifications/initialized": true,
4270
+ "notifications/cancelled": true,
4271
+ ping: true,
4272
+ "tools/call": true,
4273
+ "tools/list": true,
4274
+ "resources/read": true,
4275
+ "resources/list": true,
4276
+ "prompts/get": true,
4277
+ "prompts/list": true
4278
+ };
3875
4279
  const REDACTED_CONTENT_MARKER = "[REDACTED]";
3876
4280
  /**
3877
4281
  * Adapter boundary around MCP SDK stdio internals.
@@ -3999,7 +4403,16 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
3999
4403
  try {
4000
4404
  const message = deserializeMessage(normalizedLine);
4001
4405
  span.setStatus({ code: SpanStatusCode.OK });
4002
- if (message && typeof message === "object" && "method" in message) span.setAttribute("mcp.method", String(message.method));
4406
+ if (message && typeof message === "object" && "method" in message) {
4407
+ const method = message.method;
4408
+ if (typeof method === "string" && SAFE_MCP_METHODS[method] === true) span.setAttribute("mcp.method", method);
4409
+ if (method === "initialize") {
4410
+ const client = recordMcpSessionStart(message);
4411
+ span.setAttribute("mcp.client.name", client.name);
4412
+ span.setAttribute("mcp.client.version", client.version);
4413
+ span.setAttribute("mcp.protocol.version", client.protocolVersion);
4414
+ }
4415
+ }
4003
4416
  return message;
4004
4417
  } catch (error) {
4005
4418
  const diagnostic = createSafeErrorDiagnostic(error);
@@ -4263,7 +4676,10 @@ function buildServer(apiKey) {
4263
4676
  apiKey,
4264
4677
  clientOptions: createNodeHevyClientOptions(),
4265
4678
  wrapHandler: withObservability,
4266
- wrapServer: (baseServer) => Sentry.wrapMcpServerWithSentry(baseServer),
4679
+ wrapServer: (baseServer) => Sentry.wrapMcpServerWithSentry(baseServer, {
4680
+ recordInputs: false,
4681
+ recordOutputs: false
4682
+ }),
4267
4683
  onToolsRegistered: (count) => span.setAttribute("mcp.tools.count", count)
4268
4684
  });
4269
4685
  console.error("Hevy client initialized with API key");
@@ -4302,6 +4718,7 @@ async function runServer() {
4302
4718
  setCurrentUserHash(initialUserHash);
4303
4719
  Sentry.setUser({ id: initialUserHash });
4304
4720
  }
4721
+ let connectAttempted = false;
4305
4722
  await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
4306
4723
  try {
4307
4724
  const apiKey = parseConfig(process.env).apiKey;
@@ -4309,6 +4726,7 @@ async function runServer() {
4309
4726
  const server = await createServer({ config: { apiKey } });
4310
4727
  console.error("Starting MCP server in stdio mode");
4311
4728
  const transport = createInstrumentedStdioTransport(new StdioServerTransport());
4729
+ connectAttempted = true;
4312
4730
  await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
4313
4731
  try {
4314
4732
  await server.connect(transport);
@@ -4324,9 +4742,16 @@ async function runServer() {
4324
4742
  packageName: name$1,
4325
4743
  currentVersion: version$1
4326
4744
  });
4327
- installGracefulShutdown({ target: server });
4745
+ installGracefulShutdown({
4746
+ target: server,
4747
+ onComplete: async (succeeded) => {
4748
+ recordMcpSessionTermination(resolveSessionTerminationCategory(succeeded));
4749
+ await flushTelemetry();
4750
+ }
4751
+ });
4328
4752
  span.setStatus({ code: SpanStatusCode.OK });
4329
4753
  } catch (e) {
4754
+ recordMcpSessionTermination(connectAttempted ? "connect_failure" : "startup_failure");
4330
4755
  span.setStatus({ code: SpanStatusCode.ERROR });
4331
4756
  throw e;
4332
4757
  } finally {
@@ -4335,6 +4760,6 @@ async function runServer() {
4335
4760
  });
4336
4761
  }
4337
4762
  //#endregion
4338
- export { createSafeErrorDiagnostic as i, createServer as n, runServer as r, configSchema as t };
4763
+ export { flushTelemetry as a, createSafeErrorDiagnostic as i, createServer as n, runServer as r, configSchema as t };
4339
4764
 
4340
- //# sourceMappingURL=src-B_mNqS1X.mjs.map
4765
+ //# sourceMappingURL=src-Ch2aS5wP.mjs.map