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.
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +7 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{src-B_mNqS1X.mjs → src-Ch2aS5wP.mjs} +640 -215
- package/dist/src-Ch2aS5wP.mjs.map +1 -0
- package/package.json +12 -5
- package/server.json +2 -2
- package/dist/src-B_mNqS1X.mjs.map +0 -1
|
@@ -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.
|
|
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] = "
|
|
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.
|
|
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.
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
1185
|
-
|
|
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 (!
|
|
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 (
|
|
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
|
-
|
|
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 ??
|
|
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
|
-
|
|
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 (
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
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
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
|
|
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 (
|
|
3755
|
-
|
|
3756
|
-
|
|
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
|
|
3761
|
-
return
|
|
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
|
|
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
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
itemsScanned
|
|
4140
|
+
tool_name: context,
|
|
4141
|
+
...taxonomyAttributes(metadata),
|
|
4142
|
+
...clientAttributes
|
|
3780
4143
|
};
|
|
3781
4144
|
}
|
|
3782
4145
|
/**
|
|
3783
|
-
*
|
|
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
|
|
3795
|
-
toolInvocations.add(1,
|
|
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
|
-
|
|
3801
|
-
"mcp.
|
|
3802
|
-
"mcp.
|
|
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
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
span.
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3845
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
|
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({
|
|
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-
|
|
4765
|
+
//# sourceMappingURL=src-Ch2aS5wP.mjs.map
|