hevy-mcp 5.0.2 → 5.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{src-DBkGttYB.mjs → src-CJRjsomO.mjs} +637 -102
- package/package.json +1 -1
- package/server.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.4" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
9
|
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "625c4c9c-e20d-40b3-96b1-4e799ef130f3", e._sentryDebugIdIdentifier = "sentry-dbid-625c4c9c-e20d-40b3-96b1-4e799ef130f3");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { a as createSafeErrorDiagnostic, i as MissingHevyApiKeyError, n as runServer, o as flushTelemetry } from "./src-
|
|
12
|
+
import { a as createSafeErrorDiagnostic, i as MissingHevyApiKeyError, n as runServer, o as flushTelemetry } from "./src-CJRjsomO.mjs";
|
|
13
13
|
//#region src/cli.ts
|
|
14
14
|
runServer().catch(async (error) => {
|
|
15
15
|
if (error instanceof MissingHevyApiKeyError) console.error(error.message);
|
package/dist/index.mjs
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.4" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
9
|
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "e9496b31-626b-4ad1-9416-47ba4ffd2995", e._sentryDebugIdIdentifier = "sentry-dbid-e9496b31-626b-4ad1-9416-47ba4ffd2995");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { n as runServer, r as runStdioServer, t as createNodeMcpServer } from "./src-
|
|
12
|
+
import { n as runServer, r as runStdioServer, t as createNodeMcpServer } from "./src-CJRjsomO.mjs";
|
|
13
13
|
export { createNodeMcpServer, runServer, runStdioServer };
|
|
@@ -4,29 +4,29 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.4" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
|
-
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "
|
|
9
|
+
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "72e7b08a-987c-4dba-aabd-6437da54ce2f", e._sentryDebugIdIdentifier = "sentry-dbid-72e7b08a-987c-4dba-aabd-6437da54ce2f");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
12
|
+
import { createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
13
13
|
import * as Sentry from "@sentry/node";
|
|
14
14
|
import { SentryPropagator, SentrySampler, SentrySpanProcessor } from "@sentry/opentelemetry";
|
|
15
|
-
import { SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
15
|
+
import { SpanStatusCode, context, metrics, trace } from "@opentelemetry/api";
|
|
16
16
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
17
|
-
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
17
|
+
import { AggregationTemporalityPreference, OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
18
18
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
19
19
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
20
20
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
21
21
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
22
22
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
23
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
23
24
|
import { ZodError, z } from "zod";
|
|
24
25
|
import { McpServer, deserializeMessage } from "@modelcontextprotocol/server";
|
|
25
26
|
import { z as z$1 } from "zod/v4";
|
|
26
27
|
import { isIP } from "node:net";
|
|
27
28
|
import { createServer } from "node:http";
|
|
28
29
|
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
|
|
29
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
30
30
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
32
|
import { dirname, join } from "node:path";
|
|
@@ -79,15 +79,109 @@ function sanitizeSentryMcpSpan(span) {
|
|
|
79
79
|
* Sentry SDK: error events, performance traces, release tracking
|
|
80
80
|
* OTel Collector → Honeycomb: performance traces, metrics
|
|
81
81
|
*/
|
|
82
|
+
const SAFE_EXCEPTION_TYPES = /* @__PURE__ */ new Set([
|
|
83
|
+
"AggregateError",
|
|
84
|
+
"DOMException",
|
|
85
|
+
"Error",
|
|
86
|
+
"EvalError",
|
|
87
|
+
"HevyHttpError",
|
|
88
|
+
"RangeError",
|
|
89
|
+
"ReferenceError",
|
|
90
|
+
"SyntaxError",
|
|
91
|
+
"TypeError",
|
|
92
|
+
"URIError",
|
|
93
|
+
"ProtocolError",
|
|
94
|
+
"ZodError"
|
|
95
|
+
]);
|
|
96
|
+
const SAFE_EXCEPTION_CODES = /* @__PURE__ */ new Set([
|
|
97
|
+
"EAI_AGAIN",
|
|
98
|
+
"ECONNABORTED",
|
|
99
|
+
"ECONNREFUSED",
|
|
100
|
+
"ECONNRESET",
|
|
101
|
+
"ENETUNREACH",
|
|
102
|
+
"ENOTFOUND",
|
|
103
|
+
"ERR_NETWORK",
|
|
104
|
+
"ERR_SOCKET_TIMEOUT",
|
|
105
|
+
"ETIMEDOUT",
|
|
106
|
+
"HEVY_INVALID_ENDPOINT",
|
|
107
|
+
"HEVY_REQUEST_ABORTED",
|
|
108
|
+
"HEVY_RETRY_EXHAUSTED"
|
|
109
|
+
]);
|
|
110
|
+
function normalizeTelemetryError(error) {
|
|
111
|
+
const candidate = error instanceof Error && typeof error.name === "string" ? error.name : void 0;
|
|
112
|
+
return {
|
|
113
|
+
name: candidate && SAFE_EXCEPTION_TYPES.has(candidate) ? candidate : "UnknownError",
|
|
114
|
+
...error instanceof Error && error.stack ? { stack: error.stack } : {}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function getSafeExceptionCode(error) {
|
|
118
|
+
if (!error || typeof error !== "object" || !("code" in error)) return void 0;
|
|
119
|
+
const code = error.code;
|
|
120
|
+
return typeof code === "string" && SAFE_EXCEPTION_CODES.has(code) ? code : void 0;
|
|
121
|
+
}
|
|
122
|
+
function recordTelemetryException(error, attributes, span) {
|
|
123
|
+
if (!telemetryEnabled) return;
|
|
124
|
+
try {
|
|
125
|
+
const target = span ?? trace.getActiveSpan();
|
|
126
|
+
if (!target) return;
|
|
127
|
+
const normalized = normalizeTelemetryError(error);
|
|
128
|
+
target.recordException(normalized);
|
|
129
|
+
target.setAttribute("exception.type", normalized.name);
|
|
130
|
+
target.setAttribute("error.category", normalized.name);
|
|
131
|
+
if (attributes) target.setAttributes(attributes);
|
|
132
|
+
target.setStatus({ code: SpanStatusCode.ERROR });
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
function installProcessExceptionTracking(processLike = process) {
|
|
136
|
+
if (!telemetryEnabled) return () => {};
|
|
137
|
+
const recordProcessException = (source, error) => {
|
|
138
|
+
try {
|
|
139
|
+
tracer.startActiveSpan(`mcp.process.${source}`, { attributes: { "mcp.span.category": "process" } }, (span) => {
|
|
140
|
+
try {
|
|
141
|
+
const normalized = normalizeTelemetryError(error);
|
|
142
|
+
const code = getSafeExceptionCode(error);
|
|
143
|
+
recordTelemetryException(error, {
|
|
144
|
+
"exception.source": source,
|
|
145
|
+
"error.category": normalized.name,
|
|
146
|
+
...code ? { "error.code": code } : {}
|
|
147
|
+
}, span);
|
|
148
|
+
} finally {
|
|
149
|
+
span.end();
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
} catch {}
|
|
153
|
+
};
|
|
154
|
+
const uncaughtException = (error) => recordProcessException("uncaughtException", error);
|
|
155
|
+
const unhandledRejection = (error) => recordProcessException("unhandledRejection", error);
|
|
156
|
+
processLike.on("uncaughtExceptionMonitor", uncaughtException);
|
|
157
|
+
processLike.on("unhandledRejection", unhandledRejection);
|
|
158
|
+
let cleaned = false;
|
|
159
|
+
return () => {
|
|
160
|
+
if (cleaned) return;
|
|
161
|
+
cleaned = true;
|
|
162
|
+
processLike.removeListener("uncaughtExceptionMonitor", uncaughtException);
|
|
163
|
+
processLike.removeListener("unhandledRejection", unhandledRejection);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
82
166
|
const name$1 = "hevy-mcp";
|
|
83
|
-
const version$1 = "5.0.
|
|
167
|
+
const version$1 = "5.0.4";
|
|
84
168
|
const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
|
|
85
169
|
const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
|
|
86
170
|
const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
|
|
87
|
-
const sentryRelease = process.env.SENTRY_RELEASE ??
|
|
171
|
+
const sentryRelease = process.env.SENTRY_RELEASE ?? `${name$1}@${version$1}`;
|
|
172
|
+
function createServiceInstanceId(generate = randomUUID) {
|
|
173
|
+
try {
|
|
174
|
+
const generated = generate();
|
|
175
|
+
if (generated.length > 0 && generated.length <= 128) return generated;
|
|
176
|
+
} catch {}
|
|
177
|
+
return randomBytes(16).toString("hex");
|
|
178
|
+
}
|
|
88
179
|
const resource = resourceFromAttributes({
|
|
89
180
|
"service.name": name$1,
|
|
90
|
-
"service.version": version$1
|
|
181
|
+
"service.version": version$1,
|
|
182
|
+
"service.instance.id": createServiceInstanceId(),
|
|
183
|
+
"process.runtime.name": process.release.name,
|
|
184
|
+
"process.runtime.version": process.version
|
|
91
185
|
});
|
|
92
186
|
let currentUserHash;
|
|
93
187
|
var UserHashSpanProcessor = class {
|
|
@@ -132,7 +226,8 @@ if (telemetryEnabled) {
|
|
|
132
226
|
readers: [new PeriodicExportingMetricReader({
|
|
133
227
|
exporter: new OTLPMetricExporter({
|
|
134
228
|
url: `${COLLECTOR_ENDPOINT}/metrics`,
|
|
135
|
-
headers: { Authorization: `Bearer ${collectorToken}` }
|
|
229
|
+
headers: { Authorization: `Bearer ${collectorToken}` },
|
|
230
|
+
temporalityPreference: AggregationTemporalityPreference.DELTA
|
|
136
231
|
}),
|
|
137
232
|
exportIntervalMillis: 1e4
|
|
138
233
|
})]
|
|
@@ -160,6 +255,8 @@ async function flushTelemetry(timeoutMs = 1e3) {
|
|
|
160
255
|
}
|
|
161
256
|
const tracer = trace.getTracer(name$1);
|
|
162
257
|
const meter = metrics.getMeter(name$1);
|
|
258
|
+
const serviceName = name$1;
|
|
259
|
+
const serviceVersion = version$1;
|
|
163
260
|
const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
|
|
164
261
|
function setTelemetryUser(apiKey) {
|
|
165
262
|
if (!telemetryEnabled) return;
|
|
@@ -968,6 +1065,35 @@ const SAFE_STATIC_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
|
968
1065
|
"/v1/workouts/count",
|
|
969
1066
|
"/v1/workouts/events"
|
|
970
1067
|
]);
|
|
1068
|
+
const EXPECTED_READ_404_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
1069
|
+
"/v1/body_measurements/:date",
|
|
1070
|
+
"/v1/exercise_history/:exerciseTemplateId",
|
|
1071
|
+
"/v1/exercise_templates/:exerciseTemplateId",
|
|
1072
|
+
"/v1/routine_folders/:folderId",
|
|
1073
|
+
"/v1/routines/:routineId",
|
|
1074
|
+
"/v1/workouts/:workoutId"
|
|
1075
|
+
]);
|
|
1076
|
+
const EXPECTED_LIST_404_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
1077
|
+
"/v1/body_measurements",
|
|
1078
|
+
"/v1/exercise_templates",
|
|
1079
|
+
"/v1/routine_folders",
|
|
1080
|
+
"/v1/routines",
|
|
1081
|
+
"/v1/workouts",
|
|
1082
|
+
"/v1/workouts/events"
|
|
1083
|
+
]);
|
|
1084
|
+
const SAFE_OBSERVATION_CODES = /* @__PURE__ */ new Set([
|
|
1085
|
+
"EAI_AGAIN",
|
|
1086
|
+
"ECONNABORTED",
|
|
1087
|
+
"ECONNREFUSED",
|
|
1088
|
+
"ECONNRESET",
|
|
1089
|
+
"ENETUNREACH",
|
|
1090
|
+
"ENOTFOUND",
|
|
1091
|
+
"ERR_NETWORK",
|
|
1092
|
+
"ERR_SOCKET_TIMEOUT",
|
|
1093
|
+
"ETIMEDOUT",
|
|
1094
|
+
HEVY_REQUEST_ABORTED_ERROR_CODE,
|
|
1095
|
+
HEVY_RETRY_EXHAUSTED_ERROR_CODE
|
|
1096
|
+
]);
|
|
971
1097
|
const SAFE_DYNAMIC_ENDPOINTS = [
|
|
972
1098
|
["/v1/body_measurements/", "/v1/body_measurements/:date"],
|
|
973
1099
|
["/v1/exercise_history/", "/v1/exercise_history/:exerciseTemplateId"],
|
|
@@ -1013,9 +1139,11 @@ function getRequestContext(config) {
|
|
|
1013
1139
|
let endpoint = "unknown";
|
|
1014
1140
|
if (SAFE_STATIC_ENDPOINTS.has(rawEndpoint)) endpoint = rawEndpoint;
|
|
1015
1141
|
else endpoint = SAFE_DYNAMIC_ENDPOINTS.find(([prefix]) => rawEndpoint.startsWith(prefix))?.[1] ?? "unknown";
|
|
1142
|
+
const page = config.params !== null && typeof config.params === "object" && "page" in config.params && typeof config.params.page === "number" ? config.params.page : void 0;
|
|
1016
1143
|
return {
|
|
1017
1144
|
method,
|
|
1018
|
-
endpoint
|
|
1145
|
+
endpoint,
|
|
1146
|
+
page
|
|
1019
1147
|
};
|
|
1020
1148
|
}
|
|
1021
1149
|
function emitClientLog(logger, message) {
|
|
@@ -1025,11 +1153,49 @@ function emitClientLog(logger, message) {
|
|
|
1025
1153
|
console.error("Failed to emit structured Hevy API log");
|
|
1026
1154
|
}
|
|
1027
1155
|
}
|
|
1156
|
+
function emitRequestStart(observer, observation) {
|
|
1157
|
+
try {
|
|
1158
|
+
return observer?.(observation) ?? void 0;
|
|
1159
|
+
} catch {
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function finishRequestObservation(scope, observation) {
|
|
1164
|
+
try {
|
|
1165
|
+
scope?.finish(observation);
|
|
1166
|
+
} catch {}
|
|
1167
|
+
}
|
|
1028
1168
|
function emitRequestObservation(observer, observation) {
|
|
1029
1169
|
try {
|
|
1030
1170
|
observer?.(observation);
|
|
1031
1171
|
} catch {}
|
|
1032
1172
|
}
|
|
1173
|
+
function runRequestObservation(scope, operation) {
|
|
1174
|
+
if (!scope?.run) return operation();
|
|
1175
|
+
let started = false;
|
|
1176
|
+
const trackedOperation = () => {
|
|
1177
|
+
started = true;
|
|
1178
|
+
return operation();
|
|
1179
|
+
};
|
|
1180
|
+
try {
|
|
1181
|
+
return scope.run(trackedOperation);
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
if (started) throw error;
|
|
1184
|
+
return operation();
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
function emitRetryWait(observer, observation) {
|
|
1188
|
+
try {
|
|
1189
|
+
return observer?.(observation) ?? void 0;
|
|
1190
|
+
} catch {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
function finishRetryWait(scope) {
|
|
1195
|
+
try {
|
|
1196
|
+
scope?.finish();
|
|
1197
|
+
} catch {}
|
|
1198
|
+
}
|
|
1033
1199
|
function parseRetryAfterMs(value) {
|
|
1034
1200
|
if (!value) return void 0;
|
|
1035
1201
|
const seconds = Number(value);
|
|
@@ -1086,7 +1252,7 @@ function createNativeClient(apiKey, baseUrl, options) {
|
|
|
1086
1252
|
...clientConfig,
|
|
1087
1253
|
...config
|
|
1088
1254
|
};
|
|
1089
|
-
const { method, endpoint } = getRequestContext(normalized);
|
|
1255
|
+
const { method, endpoint, page } = getRequestContext(normalized);
|
|
1090
1256
|
const url = buildUrl(baseUrl, normalized);
|
|
1091
1257
|
let retryCount = 0;
|
|
1092
1258
|
while (true) {
|
|
@@ -1096,43 +1262,53 @@ function createNativeClient(apiKey, baseUrl, options) {
|
|
|
1096
1262
|
code: HEVY_REQUEST_ABORTED_ERROR_CODE
|
|
1097
1263
|
});
|
|
1098
1264
|
const startedAt = Date.now();
|
|
1265
|
+
const observationScope = emitRequestStart(options.onRequestStart, {
|
|
1266
|
+
method,
|
|
1267
|
+
endpoint,
|
|
1268
|
+
retryCount
|
|
1269
|
+
});
|
|
1099
1270
|
const controller = new AbortController();
|
|
1100
1271
|
const abortFromCaller = () => controller.abort();
|
|
1101
1272
|
normalized.signal?.addEventListener("abort", abortFromCaller, { once: true });
|
|
1102
1273
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
1103
1274
|
try {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1275
|
+
return await runRequestObservation(observationScope, async () => {
|
|
1276
|
+
const headers = new Headers({ "api-key": apiKey });
|
|
1277
|
+
if (normalized.data !== void 0 && !(normalized.data instanceof FormData)) headers.set("content-type", "application/json");
|
|
1278
|
+
const attemptDeadline = Date.now() + timeoutMs;
|
|
1279
|
+
const response = await withTimeout(fetchImplementation(url, {
|
|
1280
|
+
method,
|
|
1281
|
+
headers,
|
|
1282
|
+
redirect: "manual",
|
|
1283
|
+
body: normalized.data instanceof FormData ? normalized.data : normalized.data === void 0 ? void 0 : JSON.stringify(normalized.data),
|
|
1284
|
+
signal: controller.signal
|
|
1285
|
+
}), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
|
|
1286
|
+
const data = await withTimeout(parseResponseData(response), Math.max(0, attemptDeadline - Date.now()), () => controller.abort());
|
|
1287
|
+
if (!response.ok) throw new HevyHttpError(`Hevy API request failed (HTTP ${response.status})`, {
|
|
1288
|
+
status: response.status,
|
|
1289
|
+
statusText: response.statusText,
|
|
1290
|
+
data,
|
|
1291
|
+
headers: response.headers,
|
|
1292
|
+
method,
|
|
1293
|
+
endpoint
|
|
1294
|
+
});
|
|
1295
|
+
const observation = {
|
|
1296
|
+
method,
|
|
1297
|
+
endpoint,
|
|
1298
|
+
status: response.status,
|
|
1299
|
+
durationMs: Date.now() - startedAt,
|
|
1300
|
+
retryCount,
|
|
1301
|
+
outcome: "success"
|
|
1302
|
+
};
|
|
1303
|
+
finishRequestObservation(observationScope, observation);
|
|
1304
|
+
emitRequestObservation(options.onRequestComplete, observation);
|
|
1305
|
+
return {
|
|
1306
|
+
data,
|
|
1307
|
+
status: response.status,
|
|
1308
|
+
statusText: response.statusText,
|
|
1309
|
+
headers: response.headers
|
|
1310
|
+
};
|
|
1129
1311
|
});
|
|
1130
|
-
return {
|
|
1131
|
-
data,
|
|
1132
|
-
status: response.status,
|
|
1133
|
-
statusText: response.statusText,
|
|
1134
|
-
headers: response.headers
|
|
1135
|
-
};
|
|
1136
1312
|
} catch (cause) {
|
|
1137
1313
|
const error = isHevyHttpError(cause) ? cause : new HevyHttpError(normalized.signal?.aborted ? "Hevy API request was canceled" : "Hevy API network request failed", {
|
|
1138
1314
|
method,
|
|
@@ -1140,24 +1316,31 @@ function createNativeClient(apiKey, baseUrl, options) {
|
|
|
1140
1316
|
code: normalized.signal?.aborted ? HEVY_REQUEST_ABORTED_ERROR_CODE : getNetworkCode(cause),
|
|
1141
1317
|
cause
|
|
1142
1318
|
});
|
|
1319
|
+
const expectedReason = error.status === 404 && method === "GET" && EXPECTED_READ_404_ENDPOINTS.has(endpoint) ? "not_found" : error.status === 404 && method === "GET" && page !== void 0 && page > 1 && EXPECTED_LIST_404_ENDPOINTS.has(endpoint) ? "end_of_list" : void 0;
|
|
1143
1320
|
const canRetry = method === "GET" && isRetryable(error);
|
|
1144
|
-
|
|
1321
|
+
const retryExhausted = canRetry && retryCount >= maxGetRetries;
|
|
1322
|
+
if (retryExhausted) {
|
|
1145
1323
|
error.hevyRetryExhausted = true;
|
|
1146
1324
|
error.hevyRetryCount = retryCount;
|
|
1147
1325
|
error.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
|
|
1148
1326
|
}
|
|
1149
|
-
|
|
1327
|
+
const observation = {
|
|
1150
1328
|
method,
|
|
1151
1329
|
endpoint,
|
|
1152
1330
|
status: error.status ?? 0,
|
|
1153
1331
|
durationMs: Date.now() - startedAt,
|
|
1154
1332
|
retryCount,
|
|
1333
|
+
outcome: expectedReason ? "expected" : canRetry && !retryExhausted ? "retryable_failure" : "terminal_failure",
|
|
1334
|
+
...expectedReason ? { expectedReason } : {},
|
|
1155
1335
|
error: {
|
|
1156
1336
|
status: error.status,
|
|
1157
|
-
code: error.code
|
|
1337
|
+
code: typeof error.code === "string" && SAFE_OBSERVATION_CODES.has(error.code) ? error.code : void 0,
|
|
1338
|
+
category: error.status === void 0 ? "NetworkError" : "HevyHttpError"
|
|
1158
1339
|
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1340
|
+
};
|
|
1341
|
+
finishRequestObservation(observationScope, observation);
|
|
1342
|
+
emitRequestObservation(options.onRequestComplete, observation);
|
|
1343
|
+
if (!expectedReason && !canRetry) {
|
|
1161
1344
|
emitClientLog(options.onLog, {
|
|
1162
1345
|
level: "error",
|
|
1163
1346
|
logger: "hevy-api",
|
|
@@ -1170,7 +1353,7 @@ function createNativeClient(apiKey, baseUrl, options) {
|
|
|
1170
1353
|
});
|
|
1171
1354
|
throw error;
|
|
1172
1355
|
}
|
|
1173
|
-
if (error.hevyRetryExhausted) throw error;
|
|
1356
|
+
if (expectedReason || error.hevyRetryExhausted) throw error;
|
|
1174
1357
|
retryCount += 1;
|
|
1175
1358
|
const delayMs = getRetryDelayMs(error, retryCount);
|
|
1176
1359
|
emitClientLog(options.onLog, {
|
|
@@ -1186,7 +1369,19 @@ function createNativeClient(apiKey, baseUrl, options) {
|
|
|
1186
1369
|
endpoint
|
|
1187
1370
|
}
|
|
1188
1371
|
});
|
|
1189
|
-
await
|
|
1372
|
+
await runRequestObservation(observationScope, async () => {
|
|
1373
|
+
const retryWaitScope = emitRetryWait(options.onRetryWait, {
|
|
1374
|
+
method,
|
|
1375
|
+
endpoint,
|
|
1376
|
+
retryCount,
|
|
1377
|
+
delayMs
|
|
1378
|
+
});
|
|
1379
|
+
try {
|
|
1380
|
+
await sleep(delayMs);
|
|
1381
|
+
} finally {
|
|
1382
|
+
finishRetryWait(retryWaitScope);
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1190
1385
|
} finally {
|
|
1191
1386
|
clearTimeout(timeout);
|
|
1192
1387
|
normalized.signal?.removeEventListener("abort", abortFromCaller);
|
|
@@ -2644,7 +2839,7 @@ function registerHevyResources(server, runtime) {
|
|
|
2644
2839
|
//#endregion
|
|
2645
2840
|
//#region ../core/src/server-metadata.ts
|
|
2646
2841
|
const SERVER_NAME = "hevy-mcp";
|
|
2647
|
-
const SERVER_VERSION = "5.0.
|
|
2842
|
+
const SERVER_VERSION = "5.0.4";
|
|
2648
2843
|
const SERVER_INSTRUCTIONS = [
|
|
2649
2844
|
"Hevy MCP connects clients to the authenticated user's Hevy workout-tracking data, including workouts, routines, exercise templates, routine folders, body measurements, and profile information. HEVY_API_KEY must contain a valid Hevy API key for local stdio use.",
|
|
2650
2845
|
"Safety: all get-* and search-* tools are read-only. create-* and update-* tools mutate Hevy data. Creates are additive and non-idempotent, so repeating one can create duplicates. Updates can overwrite existing data. Delete operations are not available.",
|
|
@@ -4007,6 +4202,18 @@ function registerHevyTools(server, runtime) {
|
|
|
4007
4202
|
/**
|
|
4008
4203
|
* Centralized error handling utility for MCP tools
|
|
4009
4204
|
*/
|
|
4205
|
+
function createMcpToolFailureEvent(toolName, errorType, diagnostic) {
|
|
4206
|
+
return {
|
|
4207
|
+
event: "mcp.tool.failure",
|
|
4208
|
+
"mcp.tool.name": toolName,
|
|
4209
|
+
"error.type": errorType,
|
|
4210
|
+
"error.category": diagnostic.category,
|
|
4211
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {},
|
|
4212
|
+
...diagnostic.status !== void 0 ? { "http.status_code": diagnostic.status } : {},
|
|
4213
|
+
...diagnostic.method ? { "http.method": diagnostic.method } : {},
|
|
4214
|
+
...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
|
|
4215
|
+
};
|
|
4216
|
+
}
|
|
4010
4217
|
/**
|
|
4011
4218
|
* Create a standardized error response for MCP tools
|
|
4012
4219
|
*
|
|
@@ -4030,7 +4237,7 @@ function createErrorResponse(error, context) {
|
|
|
4030
4237
|
axios: axiosErrorContext ?? void 0
|
|
4031
4238
|
};
|
|
4032
4239
|
const formattedMessage = `${context ? `[${context}] ` : ""}Error: ${policy.message}`;
|
|
4033
|
-
console.error(
|
|
4240
|
+
console.error(JSON.stringify(createMcpToolFailureEvent(context || "unknown", policy.type, diagnostic)));
|
|
4034
4241
|
return {
|
|
4035
4242
|
content: [{
|
|
4036
4243
|
type: "text",
|
|
@@ -4192,39 +4399,69 @@ var AsyncTtlCache = class {
|
|
|
4192
4399
|
ttlMs;
|
|
4193
4400
|
maxSize;
|
|
4194
4401
|
now;
|
|
4402
|
+
observer;
|
|
4195
4403
|
entries = /* @__PURE__ */ new Map();
|
|
4196
4404
|
inFlight = /* @__PURE__ */ new Map();
|
|
4197
4405
|
requestCounter = 0;
|
|
4198
4406
|
constructor(options, now = Date.now) {
|
|
4199
|
-
const { ttlMs, maxSize } = options;
|
|
4407
|
+
const { ttlMs, maxSize, observer } = options;
|
|
4200
4408
|
if (ttlMs <= 0) throw new Error("Cache ttlMs must be greater than 0.");
|
|
4201
4409
|
if (maxSize <= 0) throw new Error("Cache maxSize must be greater than 0.");
|
|
4202
4410
|
this.ttlMs = ttlMs;
|
|
4203
4411
|
this.maxSize = maxSize;
|
|
4204
4412
|
this.now = now;
|
|
4413
|
+
this.observer = observer;
|
|
4414
|
+
}
|
|
4415
|
+
startObservation(state) {
|
|
4416
|
+
try {
|
|
4417
|
+
return this.observer?.start({ state }) ?? void 0;
|
|
4418
|
+
} catch {
|
|
4419
|
+
return;
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
finishObservation(scope, metadata) {
|
|
4423
|
+
try {
|
|
4424
|
+
scope?.finish(metadata);
|
|
4425
|
+
} catch {}
|
|
4205
4426
|
}
|
|
4206
4427
|
async getOrFetch(key, fetcher, options = {}) {
|
|
4207
|
-
const { refresh = false } = options;
|
|
4428
|
+
const { refresh = false, getObservationMetadata } = options;
|
|
4429
|
+
let observationState = refresh ? "refresh" : "miss";
|
|
4208
4430
|
if (refresh) this.invalidate(key);
|
|
4209
4431
|
else {
|
|
4210
4432
|
const cachedEntry = this.entries.get(key);
|
|
4211
4433
|
if (cachedEntry !== void 0) {
|
|
4212
4434
|
if (cachedEntry.expiresAt > this.now()) {
|
|
4213
4435
|
this.markAsRecentlyUsed(key, cachedEntry);
|
|
4436
|
+
const observationScope = this.startObservation("hit");
|
|
4437
|
+
this.finishObservation(observationScope);
|
|
4214
4438
|
return cachedEntry.value;
|
|
4215
4439
|
}
|
|
4216
4440
|
this.entries.delete(key);
|
|
4441
|
+
observationState = "expired";
|
|
4217
4442
|
}
|
|
4218
4443
|
const inFlightEntry = this.inFlight.get(key);
|
|
4219
|
-
if (inFlightEntry !== void 0)
|
|
4444
|
+
if (inFlightEntry !== void 0) {
|
|
4445
|
+
const observationScope = this.startObservation("inflight_wait");
|
|
4446
|
+
inFlightEntry.promise.then(() => this.finishObservation(observationScope), () => this.finishObservation(observationScope));
|
|
4447
|
+
return inFlightEntry.promise;
|
|
4448
|
+
}
|
|
4220
4449
|
}
|
|
4221
4450
|
const requestId = ++this.requestCounter;
|
|
4451
|
+
const observationScope = this.startObservation(observationState);
|
|
4222
4452
|
const request = (async () => {
|
|
4223
4453
|
try {
|
|
4224
4454
|
const value = await fetcher();
|
|
4225
4455
|
if (this.isCurrentRequest(key, requestId)) this.setValue(key, value);
|
|
4226
4456
|
return value;
|
|
4227
4457
|
} finally {
|
|
4458
|
+
let metadata;
|
|
4459
|
+
try {
|
|
4460
|
+
metadata = getObservationMetadata?.();
|
|
4461
|
+
} catch {
|
|
4462
|
+
metadata = void 0;
|
|
4463
|
+
}
|
|
4464
|
+
this.finishObservation(observationScope, metadata);
|
|
4228
4465
|
if (this.inFlight.get(key)?.requestId === requestId) this.inFlight.delete(key);
|
|
4229
4466
|
}
|
|
4230
4467
|
})();
|
|
@@ -4273,17 +4510,20 @@ var AsyncTtlCache = class {
|
|
|
4273
4510
|
const EXERCISE_TEMPLATE_CATALOG_CACHE_KEY = "exercise-template-catalog";
|
|
4274
4511
|
const EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS = 300 * 1e3;
|
|
4275
4512
|
const EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE = 1;
|
|
4276
|
-
|
|
4277
|
-
function createExerciseTemplateCatalog(hevyClient) {
|
|
4513
|
+
function createExerciseTemplateCatalog(hevyClient, cacheObserver) {
|
|
4278
4514
|
const cache = new AsyncTtlCache({
|
|
4279
4515
|
ttlMs: EXERCISE_TEMPLATE_CATALOG_CACHE_TTL_MS,
|
|
4280
|
-
maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE
|
|
4516
|
+
maxSize: EXERCISE_TEMPLATE_CATALOG_CACHE_MAX_SIZE,
|
|
4517
|
+
observer: cacheObserver
|
|
4281
4518
|
});
|
|
4282
4519
|
return {
|
|
4283
4520
|
get(options = {}) {
|
|
4284
4521
|
const reason = options.refresh ? "explicit-refresh" : cache.size === 0 ? "initial-load" : "ttl-expired";
|
|
4522
|
+
let observationMetadata;
|
|
4523
|
+
let pagesLoaded = 0;
|
|
4285
4524
|
return cache.getOrFetch(EXERCISE_TEMPLATE_CATALOG_CACHE_KEY, async () => {
|
|
4286
4525
|
const catalog = await fetchAllPages(async (page, pageSize) => {
|
|
4526
|
+
pagesLoaded = page;
|
|
4287
4527
|
const data = await hevyClient.getExerciseTemplates({
|
|
4288
4528
|
page,
|
|
4289
4529
|
pageSize
|
|
@@ -4293,9 +4533,17 @@ function createExerciseTemplateCatalog(hevyClient) {
|
|
|
4293
4533
|
pageCount: data?.page_count
|
|
4294
4534
|
};
|
|
4295
4535
|
}, 100);
|
|
4536
|
+
observationMetadata = {
|
|
4537
|
+
refreshReason: reason,
|
|
4538
|
+
pageCountBucket: bucketCount(pagesLoaded),
|
|
4539
|
+
itemCountBucket: bucketCount(catalog.length)
|
|
4540
|
+
};
|
|
4296
4541
|
options.onRefreshed?.(catalog, reason);
|
|
4297
4542
|
return catalog;
|
|
4298
|
-
},
|
|
4543
|
+
}, {
|
|
4544
|
+
refresh: options.refresh,
|
|
4545
|
+
getObservationMetadata: () => observationMetadata
|
|
4546
|
+
});
|
|
4299
4547
|
},
|
|
4300
4548
|
reset() {
|
|
4301
4549
|
cache.clear();
|
|
@@ -4357,7 +4605,7 @@ function createHevyMcpServer(options) {
|
|
|
4357
4605
|
const client = options.createClient({ onLog: (event) => mcpLogger(event) });
|
|
4358
4606
|
const runtime = createToolRuntime({
|
|
4359
4607
|
client,
|
|
4360
|
-
catalog: createExerciseTemplateCatalog(client),
|
|
4608
|
+
catalog: createExerciseTemplateCatalog(client, options.cacheObserver),
|
|
4361
4609
|
logger: mcpLogger,
|
|
4362
4610
|
observer: options.observer
|
|
4363
4611
|
});
|
|
@@ -4482,13 +4730,15 @@ function extractMcpClientMetadata(message) {
|
|
|
4482
4730
|
protocolVersion: normalizeMetadata(params.protocolVersion)
|
|
4483
4731
|
};
|
|
4484
4732
|
}
|
|
4485
|
-
function createMcpSessionContext(message, transport = "stdio") {
|
|
4733
|
+
function createMcpSessionContext(message, transport = "stdio", options = {}) {
|
|
4734
|
+
const normalizedOptions = typeof options === "function" ? { generateTelemetrySessionId: options } : options;
|
|
4486
4735
|
return {
|
|
4487
4736
|
metadata: extractMcpClientMetadata(message),
|
|
4488
|
-
startedAt: Date.now(),
|
|
4737
|
+
startedAt: (normalizedOptions.now ?? Date.now)(),
|
|
4489
4738
|
toolCalls: 0,
|
|
4490
4739
|
hadToolFailure: false,
|
|
4491
|
-
transport
|
|
4740
|
+
transport,
|
|
4741
|
+
telemetrySessionId: normalizedOptions.telemetrySessionId ?? (normalizedOptions.generateTelemetrySessionId ?? randomUUID)()
|
|
4492
4742
|
};
|
|
4493
4743
|
}
|
|
4494
4744
|
function runWithMcpSessionContext(context, callback) {
|
|
@@ -4497,6 +4747,9 @@ function runWithMcpSessionContext(context, callback) {
|
|
|
4497
4747
|
function getSession() {
|
|
4498
4748
|
return contextStorage.getStore() ?? activeStdioSession;
|
|
4499
4749
|
}
|
|
4750
|
+
function getCurrentMcpSessionId() {
|
|
4751
|
+
return getSession()?.telemetrySessionId;
|
|
4752
|
+
}
|
|
4500
4753
|
function metadataAttributes(metadata, transport) {
|
|
4501
4754
|
return {
|
|
4502
4755
|
client_name: metadata.name,
|
|
@@ -4937,6 +5190,38 @@ function debugLog(event, data) {
|
|
|
4937
5190
|
}
|
|
4938
5191
|
//#endregion
|
|
4939
5192
|
//#region src/utils/hevy-client-observability.ts
|
|
5193
|
+
function safeErrorAttributes(observation) {
|
|
5194
|
+
if (!observation.error) return {};
|
|
5195
|
+
const code = observation.error.code;
|
|
5196
|
+
return {
|
|
5197
|
+
error_category: observation.error.category ?? "HevyHttpError",
|
|
5198
|
+
...code && SAFE_OBSERVATION_CODES.has(code) ? { error_code: code } : {}
|
|
5199
|
+
};
|
|
5200
|
+
}
|
|
5201
|
+
function startApiSpan(start) {
|
|
5202
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5203
|
+
return tracer.startSpan(`hevy.api.${start.method}`, { attributes: {
|
|
5204
|
+
"mcp.span.category": "api",
|
|
5205
|
+
"http.request.method": start.method,
|
|
5206
|
+
"hevy.api.endpoint": start.endpoint,
|
|
5207
|
+
"hevy.api.retry_count_bucket": bucketCount(start.retryCount),
|
|
5208
|
+
"mcp.transport": getCurrentMcpTransport(),
|
|
5209
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
5210
|
+
} });
|
|
5211
|
+
}
|
|
5212
|
+
function finishApiSpan(span, observation) {
|
|
5213
|
+
const errorAttributes = safeErrorAttributes(observation);
|
|
5214
|
+
if (observation.status > 0) span.setAttribute("http.response.status_code", observation.status);
|
|
5215
|
+
span.setAttribute("hevy.api.outcome", observation.outcome);
|
|
5216
|
+
if (observation.expectedReason) span.setAttribute("hevy.api.expected_reason", observation.expectedReason);
|
|
5217
|
+
for (const [key, value] of Object.entries(errorAttributes)) span.setAttribute(key, value);
|
|
5218
|
+
span.setStatus({ code: observation.outcome === "success" || observation.outcome === "expected" ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
5219
|
+
if (observation.outcome !== "success" && observation.outcome !== "expected") span.addEvent("hevy.api.failure", {
|
|
5220
|
+
"error.category": errorAttributes.error_category ?? "HevyHttpError",
|
|
5221
|
+
...errorAttributes.error_code ? { "error.code": errorAttributes.error_code } : {}
|
|
5222
|
+
});
|
|
5223
|
+
span.end();
|
|
5224
|
+
}
|
|
4940
5225
|
/** Node-only adapter; the Worker graph never imports telemetry or metrics. */
|
|
4941
5226
|
function createNodeHevyClientOptions() {
|
|
4942
5227
|
const rawTimeout = process.env.HEVY_MCP_API_TIMEOUT;
|
|
@@ -4944,37 +5229,38 @@ function createNodeHevyClientOptions() {
|
|
|
4944
5229
|
const timeoutMs = Number.isFinite(parsedTimeout) && parsedTimeout > 0 ? Math.floor(parsedTimeout) : void 0;
|
|
4945
5230
|
return {
|
|
4946
5231
|
...timeoutMs ? { timeoutMs } : {},
|
|
5232
|
+
onRequestStart(start) {
|
|
5233
|
+
const span = startApiSpan(start);
|
|
5234
|
+
return {
|
|
5235
|
+
run(operation) {
|
|
5236
|
+
return context.with(trace.setSpan(context.active(), span), operation);
|
|
5237
|
+
},
|
|
5238
|
+
finish(observation) {
|
|
5239
|
+
finishApiSpan(span, observation);
|
|
5240
|
+
}
|
|
5241
|
+
};
|
|
5242
|
+
},
|
|
4947
5243
|
onRequestComplete(observation) {
|
|
4948
5244
|
const retryCountBucket = bucketCount(observation.retryCount);
|
|
4949
|
-
const
|
|
4950
|
-
const safeErrorAttributes = diagnostic ? {
|
|
4951
|
-
error_category: diagnostic.category,
|
|
4952
|
-
...diagnostic.code ? { error_code: diagnostic.code } : {}
|
|
4953
|
-
} : {};
|
|
4954
|
-
const span = tracer.startSpan(`hevy.api.${observation.method}`, { attributes: {
|
|
4955
|
-
"http.method": observation.method,
|
|
4956
|
-
"http.status_code": observation.status,
|
|
4957
|
-
"hevy.api.retry_count_bucket": retryCountBucket,
|
|
4958
|
-
"hevy.api.endpoint": observation.endpoint
|
|
4959
|
-
} });
|
|
4960
|
-
span.setStatus({ code: observation.error ? SpanStatusCode.ERROR : SpanStatusCode.OK });
|
|
4961
|
-
if (diagnostic) span.addEvent("hevy.api.failure", {
|
|
4962
|
-
"error.category": diagnostic.category,
|
|
4963
|
-
...diagnostic.code ? { "error.code": diagnostic.code } : {}
|
|
4964
|
-
});
|
|
4965
|
-
span.end();
|
|
5245
|
+
const errorAttributes = safeErrorAttributes(observation);
|
|
4966
5246
|
apiCalls.add(1, {
|
|
4967
5247
|
method: observation.method,
|
|
4968
5248
|
endpoint: observation.endpoint,
|
|
4969
5249
|
status_code: observation.status,
|
|
4970
5250
|
retry_count_bucket: retryCountBucket,
|
|
4971
|
-
|
|
5251
|
+
outcome: observation.outcome,
|
|
5252
|
+
transport: getCurrentMcpTransport(),
|
|
5253
|
+
...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
|
|
5254
|
+
...errorAttributes
|
|
4972
5255
|
});
|
|
4973
5256
|
apiDuration.record(observation.durationMs, {
|
|
4974
5257
|
method: observation.method,
|
|
4975
5258
|
endpoint: observation.endpoint,
|
|
4976
5259
|
retry_count_bucket: retryCountBucket,
|
|
4977
|
-
|
|
5260
|
+
outcome: observation.outcome,
|
|
5261
|
+
transport: getCurrentMcpTransport(),
|
|
5262
|
+
...observation.expectedReason ? { expected_reason: observation.expectedReason } : {},
|
|
5263
|
+
...errorAttributes
|
|
4978
5264
|
});
|
|
4979
5265
|
debugLog("api_response", {
|
|
4980
5266
|
method: observation.method,
|
|
@@ -4982,13 +5268,43 @@ function createNodeHevyClientOptions() {
|
|
|
4982
5268
|
durationMs: observation.durationMs,
|
|
4983
5269
|
status: observation.status || null,
|
|
4984
5270
|
retryCountBucket,
|
|
4985
|
-
|
|
5271
|
+
outcome: observation.outcome,
|
|
5272
|
+
...errorAttributes
|
|
4986
5273
|
});
|
|
5274
|
+
},
|
|
5275
|
+
onRetryWait(observation) {
|
|
5276
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5277
|
+
const span = tracer.startSpan("hevy.api.retry_wait", { attributes: {
|
|
5278
|
+
"mcp.span.category": "api",
|
|
5279
|
+
"http.request.method": observation.method,
|
|
5280
|
+
"hevy.api.retry_count_bucket": bucketCount(observation.retryCount),
|
|
5281
|
+
"hevy.api.retry_wait_ms": Math.max(0, Math.min(5e3, observation.delayMs)),
|
|
5282
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
5283
|
+
} });
|
|
5284
|
+
return { finish: () => span.end() };
|
|
4987
5285
|
}
|
|
4988
5286
|
};
|
|
4989
5287
|
}
|
|
5288
|
+
function createNodeCacheObserver() {
|
|
5289
|
+
return { start(observation) {
|
|
5290
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5291
|
+
const span = tracer.startSpan(`hevy.cache.${observation.state}`, { attributes: {
|
|
5292
|
+
"mcp.span.category": "cache",
|
|
5293
|
+
"hevy.cache.state": observation.state,
|
|
5294
|
+
"mcp.transport": getCurrentMcpTransport(),
|
|
5295
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
5296
|
+
} });
|
|
5297
|
+
return { finish(metadata) {
|
|
5298
|
+
if (metadata?.refreshReason) span.setAttribute("hevy.cache.refresh_reason", metadata.refreshReason);
|
|
5299
|
+
if (metadata?.pageCountBucket) span.setAttribute("hevy.cache.page_count_bucket", metadata.pageCountBucket);
|
|
5300
|
+
if (metadata?.itemCountBucket) span.setAttribute("hevy.cache.item_count_bucket", metadata.itemCountBucket);
|
|
5301
|
+
span.end();
|
|
5302
|
+
} };
|
|
5303
|
+
} };
|
|
5304
|
+
}
|
|
4990
5305
|
//#endregion
|
|
4991
5306
|
//#region src/utils/tool-observer.ts
|
|
5307
|
+
const DISCOVERY_TOOL_NAMES = /* @__PURE__ */ new Set(["search-routines"]);
|
|
4992
5308
|
const WORKFLOW_PAGINATION_RESOURCES = /* @__PURE__ */ new Set([
|
|
4993
5309
|
"workouts",
|
|
4994
5310
|
"bodyMeasurements",
|
|
@@ -5011,8 +5327,11 @@ function metricAttributes(invocation, clientAttributes) {
|
|
|
5011
5327
|
}
|
|
5012
5328
|
function createAttributes(invocation) {
|
|
5013
5329
|
const clientMetadata = getCurrentMcpClientMetadata();
|
|
5330
|
+
const isPrompt = invocation.kind === "prompt";
|
|
5331
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5014
5332
|
const attributes = {
|
|
5015
|
-
|
|
5333
|
+
"mcp.span.category": DISCOVERY_TOOL_NAMES.has(invocation.name) ? "discovery" : "tool",
|
|
5334
|
+
[isPrompt ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
|
|
5016
5335
|
"mcp.operation.kind": invocation.kind ?? "tool",
|
|
5017
5336
|
...taxonomyAttributes(invocation),
|
|
5018
5337
|
"mcp.client.name": clientMetadata.name,
|
|
@@ -5020,7 +5339,8 @@ function createAttributes(invocation) {
|
|
|
5020
5339
|
"mcp.protocol.version": clientMetadata.protocolVersion,
|
|
5021
5340
|
"mcp.transport": getCurrentMcpTransport(),
|
|
5022
5341
|
"mcp.tool.args.key_count_bucket": invocation.argumentKeyCountBucket ?? "unknown",
|
|
5023
|
-
"mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? ""
|
|
5342
|
+
"mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? "",
|
|
5343
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
5024
5344
|
};
|
|
5025
5345
|
for (const key of Object.keys(invocation.argumentPresence ?? {})) attributes[`mcp.tool.args.${key}.present`] = true;
|
|
5026
5346
|
for (const [key, bucket] of Object.entries(invocation.numericArgumentBuckets ?? {})) if (bucket !== void 0) attributes[`mcp.tool.args.${key}.bucket`] = bucket;
|
|
@@ -5058,14 +5378,16 @@ function resultMetricAttributes(completion) {
|
|
|
5058
5378
|
if (summary?.setCountBucket) attributes["mcp.tool.result.set_count_bucket"] = summary.setCountBucket;
|
|
5059
5379
|
return attributes;
|
|
5060
5380
|
}
|
|
5061
|
-
function setSafeErrorAttributes(span, completion) {
|
|
5381
|
+
function setSafeErrorAttributes(span, invocation, completion) {
|
|
5062
5382
|
const diagnostic = completion.error;
|
|
5063
5383
|
const errorType = completion.errorType ?? "UNKNOWN_ERROR";
|
|
5064
5384
|
if (diagnostic) span.addEvent("mcp.tool.failure", {
|
|
5385
|
+
"mcp.tool.name": invocation.name,
|
|
5386
|
+
"error.type": errorType,
|
|
5065
5387
|
"error.category": diagnostic.category,
|
|
5066
5388
|
...diagnostic.code ? { "error.code": diagnostic.code } : {},
|
|
5067
|
-
...diagnostic.status !== void 0 ? { "http.status_code": diagnostic.status } : {},
|
|
5068
|
-
...diagnostic.method ? { "http.method": diagnostic.method } : {},
|
|
5389
|
+
...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
|
|
5390
|
+
...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
|
|
5069
5391
|
...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
|
|
5070
5392
|
});
|
|
5071
5393
|
span.setAttribute("error.type", errorType);
|
|
@@ -5077,11 +5399,13 @@ function captureSafeToolFailure(invocation, completion) {
|
|
|
5077
5399
|
try {
|
|
5078
5400
|
Sentry.withScope((scope) => {
|
|
5079
5401
|
const isPrompt = invocation.kind === "prompt";
|
|
5402
|
+
scope.setTag("mcp.tool.name", invocation.name);
|
|
5080
5403
|
if (isPrompt) scope.setTag("mcp.prompt.name", invocation.name);
|
|
5081
|
-
|
|
5404
|
+
scope.setTag("error.type", completion.errorType ?? "UNKNOWN_ERROR");
|
|
5082
5405
|
scope.setTag("error.category", category);
|
|
5083
5406
|
if (diagnostic?.code) scope.setTag("error.code", diagnostic.code);
|
|
5084
|
-
if (diagnostic?.status !== void 0) scope.setTag("http.status_code", String(diagnostic.status));
|
|
5407
|
+
if (diagnostic?.status !== void 0) scope.setTag("http.response.status_code", String(diagnostic.status));
|
|
5408
|
+
if (diagnostic?.method) scope.setTag("http.request.method", diagnostic.method);
|
|
5085
5409
|
if (diagnostic?.endpoint) scope.setTag("hevy.api.endpoint", diagnostic.endpoint);
|
|
5086
5410
|
scope.setContext(invocation.kind === "prompt" ? "mcpPrompt" : "mcpTool", {
|
|
5087
5411
|
context: invocation.name,
|
|
@@ -5134,7 +5458,36 @@ function createNodeToolObserver() {
|
|
|
5134
5458
|
activeSpan.setStatus({ code: nextCompletion.outcome === "success" && nextCompletion.result?.isError !== true ? SpanStatusCode.OK : SpanStatusCode.ERROR });
|
|
5135
5459
|
activeSpan.setAttribute("mcp.tool.outcome", nextCompletion.outcome);
|
|
5136
5460
|
setResultAttributes(activeSpan, nextCompletion);
|
|
5137
|
-
if (nextCompletion.outcome === "thrown_error")
|
|
5461
|
+
if (nextCompletion.outcome === "thrown_error") {
|
|
5462
|
+
errorType = setSafeErrorAttributes(activeSpan, invocation, nextCompletion);
|
|
5463
|
+
bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? errorType), {
|
|
5464
|
+
[invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
|
|
5465
|
+
"error.type": errorType ?? "UNKNOWN_ERROR",
|
|
5466
|
+
"error.category": nextCompletion.error?.category ?? "UnknownError",
|
|
5467
|
+
...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
|
|
5468
|
+
...nextCompletion.error?.status !== void 0 ? { "http.response.status_code": nextCompletion.error.status } : {},
|
|
5469
|
+
...nextCompletion.error?.method ? { "http.request.method": nextCompletion.error.method } : {},
|
|
5470
|
+
...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
|
|
5471
|
+
}, activeSpan));
|
|
5472
|
+
}
|
|
5473
|
+
if (nextCompletion.outcome === "returned_error") {
|
|
5474
|
+
activeSpan.addEvent("mcp.tool.failure", {
|
|
5475
|
+
"mcp.tool.name": invocation.name,
|
|
5476
|
+
"error.type": "UNKNOWN_ERROR",
|
|
5477
|
+
"error.category": "McpToolReturnedError",
|
|
5478
|
+
"error.code": "MCP_TOOL_RETURNED_ERROR"
|
|
5479
|
+
});
|
|
5480
|
+
activeSpan.setAttribute("error.type", "UNKNOWN_ERROR");
|
|
5481
|
+
bestEffort(() => recordTelemetryException(new Error(nextCompletion.error?.category ?? "UnknownError"), {
|
|
5482
|
+
[invocation.kind === "prompt" ? "mcp.prompt.name" : "mcp.tool.name"]: invocation.name,
|
|
5483
|
+
"error.type": nextCompletion.errorType ?? "UNKNOWN_ERROR",
|
|
5484
|
+
"error.category": nextCompletion.error?.category ?? "UnknownError",
|
|
5485
|
+
...nextCompletion.error?.code ? { "error.code": nextCompletion.error.code } : {},
|
|
5486
|
+
...nextCompletion.error?.status !== void 0 ? { "http.response.status_code": nextCompletion.error.status } : {},
|
|
5487
|
+
...nextCompletion.error?.method ? { "http.request.method": nextCompletion.error.method } : {},
|
|
5488
|
+
...nextCompletion.error?.endpoint ? { "hevy.api.endpoint": nextCompletion.error.endpoint } : {}
|
|
5489
|
+
}, activeSpan));
|
|
5490
|
+
}
|
|
5138
5491
|
}
|
|
5139
5492
|
} catch {} finally {
|
|
5140
5493
|
if (nextCompletion.outcome === "thrown_error") {
|
|
@@ -5315,14 +5668,17 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
|
|
|
5315
5668
|
const lineHadLeadingBom = line.startsWith(UTF8_BOM);
|
|
5316
5669
|
const normalizedLine = lineHadLeadingBom ? line.slice(1) : line;
|
|
5317
5670
|
const lineByteLength = Buffer.byteLength(line);
|
|
5671
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5318
5672
|
return tracer.startActiveSpan("mcp.stdio.deserialize", { attributes: {
|
|
5673
|
+
"mcp.span.category": "protocol",
|
|
5319
5674
|
"mcp.transport": "stdio",
|
|
5320
5675
|
"mcp.stdio.parse.line.char_length": line.length,
|
|
5321
5676
|
"mcp.stdio.parse.line.byte_length": lineByteLength,
|
|
5322
5677
|
"mcp.stdio.parse.line.had_leading_bom": lineHadLeadingBom,
|
|
5323
5678
|
"mcp.stdio.parse.line.bom_stripped": lineHadLeadingBom,
|
|
5324
5679
|
"mcp.stdio.parse.chunk.last_byte_length": chunkSnapshot.lastChunkByteLength,
|
|
5325
|
-
"mcp.stdio.parse.chunk.last_had_utf8_bom": chunkSnapshot.lastChunkStartsWithUtf8Bom
|
|
5680
|
+
"mcp.stdio.parse.chunk.last_had_utf8_bom": chunkSnapshot.lastChunkStartsWithUtf8Bom,
|
|
5681
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
5326
5682
|
} }, (span) => {
|
|
5327
5683
|
try {
|
|
5328
5684
|
const message = deserializeMessage(normalizedLine);
|
|
@@ -5332,6 +5688,8 @@ function deserializeMessageWithObservability(line, chunkSnapshot) {
|
|
|
5332
5688
|
if (typeof method === "string" && SAFE_MCP_METHODS[method] === true) span.setAttribute("mcp.method", method);
|
|
5333
5689
|
if (method === "initialize") {
|
|
5334
5690
|
const client = recordMcpSessionStart(message);
|
|
5691
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5692
|
+
if (sessionId) span.setAttribute("mcp.session.id", sessionId);
|
|
5335
5693
|
span.setAttribute("mcp.client.name", client.name);
|
|
5336
5694
|
span.setAttribute("mcp.client.version", client.version);
|
|
5337
5695
|
span.setAttribute("mcp.protocol.version", client.protocolVersion);
|
|
@@ -5498,8 +5856,8 @@ function scheduleUpdateCheck(options, overrides = {}) {
|
|
|
5498
5856
|
}
|
|
5499
5857
|
//#endregion
|
|
5500
5858
|
//#region src/index.ts
|
|
5501
|
-
const name =
|
|
5502
|
-
const version =
|
|
5859
|
+
const name = serviceName;
|
|
5860
|
+
const version = serviceVersion;
|
|
5503
5861
|
const HELP_TEXT = [
|
|
5504
5862
|
"Usage:",
|
|
5505
5863
|
" hevy-mcp [options]",
|
|
@@ -5559,6 +5917,159 @@ function getSafeValidationDiagnostic(error) {
|
|
|
5559
5917
|
const code = error.code;
|
|
5560
5918
|
return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
|
|
5561
5919
|
}
|
|
5920
|
+
function recordLifecycleFailure(span, error, phase) {
|
|
5921
|
+
const diagnostic = createSafeErrorDiagnostic(error);
|
|
5922
|
+
const attributes = {
|
|
5923
|
+
"mcp.failure.phase": phase,
|
|
5924
|
+
"error.type": diagnostic.category,
|
|
5925
|
+
"error.category": diagnostic.category,
|
|
5926
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {},
|
|
5927
|
+
...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
|
|
5928
|
+
...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
|
|
5929
|
+
...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
|
|
5930
|
+
};
|
|
5931
|
+
span.addEvent("mcp.lifecycle.failure", {
|
|
5932
|
+
"error.type": diagnostic.category,
|
|
5933
|
+
"error.category": diagnostic.category,
|
|
5934
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {}
|
|
5935
|
+
});
|
|
5936
|
+
recordTelemetryException(error, attributes, span);
|
|
5937
|
+
}
|
|
5938
|
+
const sdkToolNameStorage = new AsyncLocalStorage();
|
|
5939
|
+
const SAFE_TOOL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
|
|
5940
|
+
function getSdkToolName(request) {
|
|
5941
|
+
if (!request || typeof request !== "object") return "unknown";
|
|
5942
|
+
const candidate = request.params?.name;
|
|
5943
|
+
return typeof candidate === "string" && SAFE_TOOL_NAME_PATTERN.test(candidate) ? candidate : "unknown";
|
|
5944
|
+
}
|
|
5945
|
+
function markSdkToolFailure(span, error, toolName = sdkToolNameStorage.getStore() ?? "unknown", errorTypeOverride) {
|
|
5946
|
+
const diagnostic = createSafeErrorDiagnostic(error);
|
|
5947
|
+
const errorType = errorTypeOverride ?? diagnostic.category;
|
|
5948
|
+
span.addEvent("mcp.tool.failure", {
|
|
5949
|
+
"mcp.tool.name": toolName,
|
|
5950
|
+
"error.type": errorType,
|
|
5951
|
+
"error.category": diagnostic.category,
|
|
5952
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {},
|
|
5953
|
+
...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {},
|
|
5954
|
+
...diagnostic.method ? { "http.request.method": diagnostic.method } : {},
|
|
5955
|
+
...diagnostic.endpoint ? { "hevy.api.endpoint": diagnostic.endpoint } : {}
|
|
5956
|
+
});
|
|
5957
|
+
span.setAttribute("error.type", errorType);
|
|
5958
|
+
recordTelemetryException(error, {
|
|
5959
|
+
"mcp.failure.phase": "sdk",
|
|
5960
|
+
"error.type": errorType,
|
|
5961
|
+
"error.category": diagnostic.category,
|
|
5962
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {},
|
|
5963
|
+
...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {}
|
|
5964
|
+
}, span);
|
|
5965
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
5966
|
+
}
|
|
5967
|
+
function installSdkErrorTracking(server) {
|
|
5968
|
+
const protocol = server.server;
|
|
5969
|
+
if (!protocol) return;
|
|
5970
|
+
const previous = protocol.onerror;
|
|
5971
|
+
protocol.onerror = (error) => {
|
|
5972
|
+
const diagnostic = createSafeErrorDiagnostic(error);
|
|
5973
|
+
const attributes = {
|
|
5974
|
+
"mcp.failure.phase": "sdk",
|
|
5975
|
+
"error.category": diagnostic.category,
|
|
5976
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {},
|
|
5977
|
+
...diagnostic.status !== void 0 ? { "http.response.status_code": diagnostic.status } : {}
|
|
5978
|
+
};
|
|
5979
|
+
const activeSpan = trace.getActiveSpan();
|
|
5980
|
+
const sessionId = getCurrentMcpSessionId();
|
|
5981
|
+
if (activeSpan) {
|
|
5982
|
+
activeSpan.addEvent("mcp.tool.failure", {
|
|
5983
|
+
"mcp.tool.name": sdkToolNameStorage.getStore() ?? "unknown",
|
|
5984
|
+
"error.type": "UNKNOWN_ERROR",
|
|
5985
|
+
"error.category": diagnostic.category,
|
|
5986
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {}
|
|
5987
|
+
});
|
|
5988
|
+
recordTelemetryException(error, attributes, activeSpan);
|
|
5989
|
+
} else tracer.startActiveSpan("mcp.sdk.failure", { attributes: { ...sessionId ? { "mcp.session.id": sessionId } : {} } }, (span) => {
|
|
5990
|
+
span.addEvent("mcp.tool.failure", {
|
|
5991
|
+
"mcp.tool.name": "unknown",
|
|
5992
|
+
"error.type": "UNKNOWN_ERROR",
|
|
5993
|
+
"error.category": diagnostic.category,
|
|
5994
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {}
|
|
5995
|
+
});
|
|
5996
|
+
recordTelemetryException(error, attributes, span);
|
|
5997
|
+
span.end();
|
|
5998
|
+
});
|
|
5999
|
+
try {
|
|
6000
|
+
previous?.(error);
|
|
6001
|
+
} catch {}
|
|
6002
|
+
};
|
|
6003
|
+
const toolErrorHost = server;
|
|
6004
|
+
const previousCreateToolError = toolErrorHost.createToolError;
|
|
6005
|
+
if (previousCreateToolError) {
|
|
6006
|
+
const createToolError = previousCreateToolError.bind(server);
|
|
6007
|
+
toolErrorHost.createToolError = (message) => {
|
|
6008
|
+
const result = createToolError(message);
|
|
6009
|
+
const activeSpan = trace.getActiveSpan();
|
|
6010
|
+
if (activeSpan) markSdkToolFailure(activeSpan, /* @__PURE__ */ new Error("MCP tool validation failed"), void 0, "VALIDATION_ERROR");
|
|
6011
|
+
return result;
|
|
6012
|
+
};
|
|
6013
|
+
}
|
|
6014
|
+
const handlers = protocol._requestHandlers;
|
|
6015
|
+
if (!(handlers instanceof Map)) return;
|
|
6016
|
+
const toolHandler = handlers.get("tools/call");
|
|
6017
|
+
if (toolHandler) handlers.set("tools/call", (request, extra) => {
|
|
6018
|
+
const sessionId = getCurrentMcpSessionId();
|
|
6019
|
+
const toolName = getSdkToolName(request);
|
|
6020
|
+
return sdkToolNameStorage.run(toolName, () => tracer.startActiveSpan("mcp.sdk.tools.call", { attributes: {
|
|
6021
|
+
"mcp.span.category": "protocol",
|
|
6022
|
+
"mcp.operation.kind": "tool",
|
|
6023
|
+
"mcp.tool.name": toolName,
|
|
6024
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
6025
|
+
} }, async (span) => {
|
|
6026
|
+
try {
|
|
6027
|
+
const result = await toolHandler(request, extra);
|
|
6028
|
+
if (result?.isError === true) {
|
|
6029
|
+
span.setAttribute("mcp.tool.outcome", "returned_error");
|
|
6030
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
6031
|
+
} else span.setStatus({ code: SpanStatusCode.OK });
|
|
6032
|
+
return result;
|
|
6033
|
+
} catch (error) {
|
|
6034
|
+
markSdkToolFailure(span, error, toolName);
|
|
6035
|
+
throw error;
|
|
6036
|
+
} finally {
|
|
6037
|
+
span.end();
|
|
6038
|
+
}
|
|
6039
|
+
}));
|
|
6040
|
+
});
|
|
6041
|
+
const discoveryHandler = handlers.get("server/discover");
|
|
6042
|
+
if (discoveryHandler) handlers.set("server/discover", (request, extra) => {
|
|
6043
|
+
const sessionId = getCurrentMcpSessionId();
|
|
6044
|
+
return tracer.startActiveSpan("mcp.server.discover", { attributes: {
|
|
6045
|
+
"mcp.span.category": "discovery",
|
|
6046
|
+
"mcp.transport": getCurrentMcpTransport(),
|
|
6047
|
+
...sessionId ? { "mcp.session.id": sessionId } : {}
|
|
6048
|
+
} }, async (span) => {
|
|
6049
|
+
try {
|
|
6050
|
+
const result = await discoveryHandler(request, extra);
|
|
6051
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
6052
|
+
return result;
|
|
6053
|
+
} catch (error) {
|
|
6054
|
+
const diagnostic = createSafeErrorDiagnostic(error);
|
|
6055
|
+
span.addEvent("mcp.discovery.failure", {
|
|
6056
|
+
"error.type": diagnostic.category,
|
|
6057
|
+
"error.category": diagnostic.category,
|
|
6058
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {}
|
|
6059
|
+
});
|
|
6060
|
+
recordTelemetryException(error, {
|
|
6061
|
+
"mcp.failure.phase": "discovery",
|
|
6062
|
+
"error.type": diagnostic.category,
|
|
6063
|
+
"error.category": diagnostic.category,
|
|
6064
|
+
...diagnostic.code ? { "error.code": diagnostic.code } : {}
|
|
6065
|
+
}, span);
|
|
6066
|
+
throw error;
|
|
6067
|
+
} finally {
|
|
6068
|
+
span.end();
|
|
6069
|
+
}
|
|
6070
|
+
});
|
|
6071
|
+
});
|
|
6072
|
+
}
|
|
5562
6073
|
async function validateApiKey(apiKey) {
|
|
5563
6074
|
const startupProbeClient = createHevyClient({
|
|
5564
6075
|
apiKey,
|
|
@@ -5577,6 +6088,7 @@ async function validateApiKey(apiKey) {
|
|
|
5577
6088
|
}
|
|
5578
6089
|
function buildServer(apiKey, transport = "stdio") {
|
|
5579
6090
|
return tracer.startActiveSpan("mcp.server.build", { attributes: {
|
|
6091
|
+
"mcp.span.category": "startup",
|
|
5580
6092
|
"mcp.server.name": name,
|
|
5581
6093
|
"mcp.server.version": version,
|
|
5582
6094
|
"mcp.transport": transport
|
|
@@ -5593,12 +6105,15 @@ function buildServer(apiKey, transport = "stdio") {
|
|
|
5593
6105
|
recordOutputs: false
|
|
5594
6106
|
}),
|
|
5595
6107
|
onToolsRegistered: (count) => span.setAttribute("mcp.tools.count", count),
|
|
5596
|
-
observer: createNodeToolObserver()
|
|
6108
|
+
observer: createNodeToolObserver(),
|
|
6109
|
+
cacheObserver: createNodeCacheObserver()
|
|
5597
6110
|
});
|
|
6111
|
+
installSdkErrorTracking(server);
|
|
5598
6112
|
console.error("Hevy client initialized with API key");
|
|
5599
6113
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
5600
6114
|
return server;
|
|
5601
6115
|
} catch (e) {
|
|
6116
|
+
recordLifecycleFailure(span, e, "build");
|
|
5602
6117
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
5603
6118
|
throw e;
|
|
5604
6119
|
} finally {
|
|
@@ -5622,11 +6137,16 @@ async function runStdioServer() {
|
|
|
5622
6137
|
console.log(HELP_TEXT);
|
|
5623
6138
|
return;
|
|
5624
6139
|
}
|
|
6140
|
+
const cleanupProcessExceptionTracking = installProcessExceptionTracking();
|
|
5625
6141
|
serverStartups.add(1, { version });
|
|
5626
6142
|
const configuredApiKey = process.env.HEVY_API_KEY;
|
|
5627
6143
|
if (configuredApiKey) setTelemetryUser(configuredApiKey);
|
|
5628
6144
|
let connectAttempted = false;
|
|
5629
|
-
|
|
6145
|
+
let connectSucceeded = false;
|
|
6146
|
+
await tracer.startActiveSpan("mcp.server.run", { attributes: {
|
|
6147
|
+
"mcp.span.category": "startup",
|
|
6148
|
+
"mcp.transport": "stdio"
|
|
6149
|
+
} }, async (span) => {
|
|
5630
6150
|
try {
|
|
5631
6151
|
const apiKey = parseConfig(process.env).apiKey;
|
|
5632
6152
|
assertApiKey(apiKey);
|
|
@@ -5634,11 +6154,16 @@ async function runStdioServer() {
|
|
|
5634
6154
|
console.error("Starting MCP server in stdio mode");
|
|
5635
6155
|
const transport = createInstrumentedStdioTransport(new StdioServerTransport());
|
|
5636
6156
|
connectAttempted = true;
|
|
5637
|
-
await tracer.startActiveSpan("mcp.server.connect", { attributes: {
|
|
6157
|
+
await tracer.startActiveSpan("mcp.server.connect", { attributes: {
|
|
6158
|
+
"mcp.span.category": "session",
|
|
6159
|
+
"mcp.transport": "stdio"
|
|
6160
|
+
} }, async (connectSpan) => {
|
|
5638
6161
|
try {
|
|
5639
6162
|
await server.connect(transport);
|
|
6163
|
+
connectSucceeded = true;
|
|
5640
6164
|
connectSpan.setStatus({ code: SpanStatusCode.OK });
|
|
5641
6165
|
} catch (e) {
|
|
6166
|
+
recordLifecycleFailure(connectSpan, e, "connect");
|
|
5642
6167
|
connectSpan.setStatus({ code: SpanStatusCode.ERROR });
|
|
5643
6168
|
throw e;
|
|
5644
6169
|
} finally {
|
|
@@ -5646,20 +6171,23 @@ async function runStdioServer() {
|
|
|
5646
6171
|
}
|
|
5647
6172
|
});
|
|
5648
6173
|
scheduleUpdateCheck({
|
|
5649
|
-
packageName:
|
|
5650
|
-
currentVersion:
|
|
6174
|
+
packageName: serviceName,
|
|
6175
|
+
currentVersion: serviceVersion
|
|
5651
6176
|
});
|
|
5652
6177
|
installGracefulShutdown({
|
|
5653
6178
|
target: server,
|
|
5654
6179
|
onComplete: async (succeeded) => {
|
|
5655
6180
|
recordMcpSessionTermination(resolveSessionTerminationCategory(succeeded));
|
|
6181
|
+
cleanupProcessExceptionTracking();
|
|
5656
6182
|
await flushTelemetry();
|
|
5657
6183
|
}
|
|
5658
6184
|
});
|
|
5659
6185
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
5660
6186
|
} catch (e) {
|
|
6187
|
+
if (!connectAttempted || connectSucceeded) recordLifecycleFailure(span, e, "run");
|
|
5661
6188
|
recordMcpSessionTermination(connectAttempted ? "connect_failure" : "startup_failure");
|
|
5662
6189
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
6190
|
+
cleanupProcessExceptionTracking();
|
|
5663
6191
|
throw e;
|
|
5664
6192
|
} finally {
|
|
5665
6193
|
span.end();
|
|
@@ -5682,7 +6210,11 @@ async function runServer() {
|
|
|
5682
6210
|
await runStdioServer();
|
|
5683
6211
|
return;
|
|
5684
6212
|
}
|
|
5685
|
-
|
|
6213
|
+
const cleanupProcessExceptionTracking = installProcessExceptionTracking();
|
|
6214
|
+
await tracer.startActiveSpan("mcp.server.run", { attributes: {
|
|
6215
|
+
"mcp.span.category": "startup",
|
|
6216
|
+
"mcp.transport": "http"
|
|
6217
|
+
} }, async (span) => {
|
|
5686
6218
|
let listening = false;
|
|
5687
6219
|
serverStartups.add(1, { version });
|
|
5688
6220
|
try {
|
|
@@ -5694,19 +6226,22 @@ async function runServer() {
|
|
|
5694
6226
|
listening = true;
|
|
5695
6227
|
console.error(`Starting MCP server in HTTP mode at ${options.host}:${options.port}/mcp`);
|
|
5696
6228
|
scheduleUpdateCheck({
|
|
5697
|
-
packageName:
|
|
5698
|
-
currentVersion:
|
|
6229
|
+
packageName: serviceName,
|
|
6230
|
+
currentVersion: serviceVersion
|
|
5699
6231
|
});
|
|
5700
6232
|
installGracefulShutdown({
|
|
5701
6233
|
target: handle,
|
|
5702
6234
|
onComplete: async () => {
|
|
6235
|
+
cleanupProcessExceptionTracking();
|
|
5703
6236
|
await flushTelemetry();
|
|
5704
6237
|
}
|
|
5705
6238
|
});
|
|
5706
6239
|
span.setStatus({ code: SpanStatusCode.OK });
|
|
5707
6240
|
} catch (error) {
|
|
6241
|
+
recordLifecycleFailure(span, error, "run");
|
|
5708
6242
|
recordMcpSessionTermination(listening ? "unknown" : "startup_failure");
|
|
5709
6243
|
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
6244
|
+
cleanupProcessExceptionTracking();
|
|
5710
6245
|
throw error;
|
|
5711
6246
|
} finally {
|
|
5712
6247
|
span.end();
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/chrisdoc/hevy-mcp",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "5.0.
|
|
10
|
+
"version": "5.0.4",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "hevy-mcp",
|
|
15
|
-
"version": "5.0.
|
|
15
|
+
"version": "5.0.4",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|