hevy-mcp 4.1.2 → 5.0.0
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/README.md +9 -8
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{src-Wa4VP4Na.mjs → src-BUmpaxmH.mjs} +877 -973
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -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@
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@5.0.0" };
|
|
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] = "8ea46bdf-fa98-4e73-b4d5-a25044e016df", e._sentryDebugIdIdentifier = "sentry-dbid-8ea46bdf-fa98-4e73-b4d5-a25044e016df");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
12
|
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
@@ -19,15 +19,14 @@ 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
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
22
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
23
23
|
import { z } from "zod";
|
|
24
|
-
import { McpServer } from "@modelcontextprotocol/
|
|
24
|
+
import { McpServer, deserializeMessage } from "@modelcontextprotocol/server";
|
|
25
25
|
import { z as z$1 } from "zod/v4";
|
|
26
26
|
import { isIP } from "node:net";
|
|
27
27
|
import { createServer } from "node:http";
|
|
28
|
-
import {
|
|
28
|
+
import { NodeStreamableHTTPServerTransport } from "@modelcontextprotocol/node";
|
|
29
29
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
30
|
-
import { deserializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
|
|
31
30
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
32
31
|
import { homedir } from "node:os";
|
|
33
32
|
import { dirname, join } from "node:path";
|
|
@@ -81,11 +80,11 @@ function sanitizeSentryMcpSpan(span) {
|
|
|
81
80
|
* OTel Collector → Honeycomb: performance traces, metrics
|
|
82
81
|
*/
|
|
83
82
|
const name$1 = "hevy-mcp";
|
|
84
|
-
const version$1 = "
|
|
83
|
+
const version$1 = "5.0.0";
|
|
85
84
|
const telemetryEnabled = process.env.HEVY_MCP_TELEMETRY !== "0";
|
|
86
85
|
const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
|
|
87
86
|
const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
|
|
88
|
-
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@
|
|
87
|
+
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@5.0.0`;
|
|
89
88
|
const resource = resourceFromAttributes({
|
|
90
89
|
"service.name": name$1,
|
|
91
90
|
"service.version": version$1
|
|
@@ -227,8 +226,8 @@ const repRangeShape = {
|
|
|
227
226
|
function optionalRepRangeSchema(schema) {
|
|
228
227
|
return z.preprocess((value) => value === null ? void 0 : value, schema);
|
|
229
228
|
}
|
|
230
|
-
|
|
231
|
-
optionalRepRangeSchema(z.object(repRangeShape).strict().optional());
|
|
229
|
+
optionalRepRangeSchema(z.object(repRangeShape).optional());
|
|
230
|
+
const zStrictOptionalRepRange = optionalRepRangeSchema(z.object(repRangeShape).strict().optional());
|
|
232
231
|
const setTypeEnum = z.enum([
|
|
233
232
|
"warmup",
|
|
234
233
|
"normal",
|
|
@@ -1469,8 +1468,8 @@ function withPromptObservation(name, observer, handler) {
|
|
|
1469
1468
|
scope = memoizeObservationScope(observer?.start({
|
|
1470
1469
|
name,
|
|
1471
1470
|
kind: "prompt",
|
|
1472
|
-
argumentKeys: Object.keys(args).filter((key) => key === "
|
|
1473
|
-
argumentPresence: args.
|
|
1471
|
+
argumentKeys: Object.keys(args).filter((key) => key === "routine_id"),
|
|
1472
|
+
argumentPresence: args.routine_id ? { routine_id: true } : {},
|
|
1474
1473
|
argumentKeyCountBucket: bucketCount(Object.keys(args).length)
|
|
1475
1474
|
}));
|
|
1476
1475
|
} catch {
|
|
@@ -1507,7 +1506,7 @@ function registerWorkoutPrompts(server, observer) {
|
|
|
1507
1506
|
server.registerPrompt("analyze-workout-progress", {
|
|
1508
1507
|
title: "Analyze Workout Progress",
|
|
1509
1508
|
description: "Analyze recent workout and body-measurement trends.",
|
|
1510
|
-
argsSchema: { weeks: z.coerce.number().int().min(1).max(12).default(4).optional().describe("Number of recent weeks to analyze (1-12).") }
|
|
1509
|
+
argsSchema: z.object({ weeks: z.coerce.number().int().min(1).max(12).default(4).optional().describe("Number of recent weeks to analyze (1-12).") })
|
|
1511
1510
|
}, withPromptObservation("analyze-workout-progress", observer, ({ weeks = 4 }) => ({ messages: [{
|
|
1512
1511
|
role: "user",
|
|
1513
1512
|
content: {
|
|
@@ -1524,23 +1523,23 @@ function registerWorkoutPrompts(server, observer) {
|
|
|
1524
1523
|
server.registerPrompt("create-workout-from-routine", {
|
|
1525
1524
|
title: "Create Workout From Routine",
|
|
1526
1525
|
description: "Create a completed workout from an existing routine.",
|
|
1527
|
-
argsSchema: {
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
}
|
|
1531
|
-
}, withPromptObservation("create-workout-from-routine", observer, ({
|
|
1526
|
+
argsSchema: z.object({
|
|
1527
|
+
routine_id: z.string().min(1).optional().describe("Routine ID to use as a guide."),
|
|
1528
|
+
start_time: utcSecondTimestamp.optional().describe("Workout start time in UTC as YYYY-MM-DDTHH:mm:ssZ.")
|
|
1529
|
+
})
|
|
1530
|
+
}, withPromptObservation("create-workout-from-routine", observer, ({ routine_id, start_time }) => ({ messages: [{
|
|
1532
1531
|
role: "user",
|
|
1533
1532
|
content: {
|
|
1534
1533
|
type: "text",
|
|
1535
|
-
text:
|
|
1536
|
-
`Create a workout from routine ${
|
|
1537
|
-
"First call get-routine with the
|
|
1538
|
-
"Do not copy routine-only
|
|
1539
|
-
"Before calling create-workout, confirm or collect the user's actual completed set data for every set, including applicable
|
|
1540
|
-
"Also collect the required
|
|
1541
|
-
"Never invent completion data. If the actual results or
|
|
1534
|
+
text: routine_id && start_time ? [
|
|
1535
|
+
`Create a workout from routine ${routine_id}, starting at ${start_time}.`,
|
|
1536
|
+
["First call get-routine with the routine_id and map supported plan fields:", "routine title to workout title, plus each exercise_template_id, superset_id, exercise notes, and set type."].join(" "),
|
|
1537
|
+
["Do not copy routine-only rest_seconds or rep_range fields into", "create-workout."].join(" "),
|
|
1538
|
+
["Before calling create-workout, confirm or collect the user's actual completed set data for every set,", "including applicable weight_kg, reps, distance_meters, duration_seconds, rpe, or custom_metric values."].join(" "),
|
|
1539
|
+
["Also collect the required end_time in strict UTC YYYY-MM-DDTHH:mm:ssZ format and confirm", "any other missing required workout fields."].join(" "),
|
|
1540
|
+
["Never invent completion data. If the actual results or end_time are unavailable, ask the user for them", "instead of creating the workout."].join(" "),
|
|
1542
1541
|
"Once confirmed, call create-workout with only fields supported by that tool."
|
|
1543
|
-
].join("\n") : "Provide a
|
|
1542
|
+
].join("\n") : "Provide a routine_id and start_time to generate the full prompt."
|
|
1544
1543
|
}
|
|
1545
1544
|
}] })));
|
|
1546
1545
|
}
|
|
@@ -1846,7 +1845,7 @@ const getV1WorkoutsWorkoutid200Schema = z$1.lazy(() => workoutSchema);
|
|
|
1846
1845
|
z$1.any();
|
|
1847
1846
|
z$1.lazy(() => getV1WorkoutsWorkoutid200Schema);
|
|
1848
1847
|
//#endregion
|
|
1849
|
-
//#region ../core/src/utils/response-
|
|
1848
|
+
//#region ../core/src/utils/response-contracts.ts
|
|
1850
1849
|
function jsonText(data) {
|
|
1851
1850
|
return JSON.stringify(data, null, 2) ?? "null";
|
|
1852
1851
|
}
|
|
@@ -1884,146 +1883,150 @@ function defineJsonResponseContract(present, telemetry) {
|
|
|
1884
1883
|
function respond(contract, data) {
|
|
1885
1884
|
return contract.render(data);
|
|
1886
1885
|
}
|
|
1887
|
-
const
|
|
1886
|
+
const optionalNumber = z.number().optional();
|
|
1888
1887
|
const formattedWorkoutSetSchema = z.object({
|
|
1889
1888
|
index: z.number().optional(),
|
|
1890
1889
|
type: z.string().optional(),
|
|
1891
|
-
|
|
1892
|
-
reps:
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
rpe:
|
|
1896
|
-
|
|
1890
|
+
weight_kg: optionalNumber,
|
|
1891
|
+
reps: optionalNumber,
|
|
1892
|
+
distance_meters: optionalNumber,
|
|
1893
|
+
duration_seconds: optionalNumber,
|
|
1894
|
+
rpe: optionalNumber,
|
|
1895
|
+
custom_metric: optionalNumber
|
|
1897
1896
|
});
|
|
1898
1897
|
const formattedWorkoutExerciseSchema = z.object({
|
|
1899
1898
|
index: z.number().optional(),
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
notes: z.string().
|
|
1903
|
-
|
|
1899
|
+
title: z.string().optional(),
|
|
1900
|
+
exercise_template_id: z.string().optional(),
|
|
1901
|
+
notes: z.string().optional(),
|
|
1902
|
+
supersets_id: optionalNumber,
|
|
1904
1903
|
sets: z.array(formattedWorkoutSetSchema).optional()
|
|
1905
1904
|
});
|
|
1906
1905
|
const formattedWorkoutSchema = z.object({
|
|
1907
1906
|
id: z.string().optional(),
|
|
1907
|
+
routine_id: z.string().optional(),
|
|
1908
1908
|
title: z.string().optional(),
|
|
1909
|
-
description: z.string().
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1909
|
+
description: z.string().optional(),
|
|
1910
|
+
start_time: z.string().optional(),
|
|
1911
|
+
end_time: z.string().optional(),
|
|
1912
|
+
created_at: z.string().optional(),
|
|
1913
|
+
updated_at: z.string().optional(),
|
|
1914
1914
|
duration: z.string(),
|
|
1915
1915
|
exercises: z.array(formattedWorkoutExerciseSchema).optional()
|
|
1916
1916
|
});
|
|
1917
1917
|
const formattedRoutineSetSchema = z.object({
|
|
1918
1918
|
index: z.number().optional(),
|
|
1919
1919
|
type: z.string().optional(),
|
|
1920
|
-
|
|
1921
|
-
reps:
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
start: z.number().
|
|
1927
|
-
end: z.number().
|
|
1928
|
-
}).
|
|
1929
|
-
rpe:
|
|
1920
|
+
weight_kg: optionalNumber,
|
|
1921
|
+
reps: optionalNumber,
|
|
1922
|
+
distance_meters: optionalNumber,
|
|
1923
|
+
duration_seconds: optionalNumber,
|
|
1924
|
+
custom_metric: optionalNumber,
|
|
1925
|
+
rep_range: z.object({
|
|
1926
|
+
start: z.number().optional(),
|
|
1927
|
+
end: z.number().optional()
|
|
1928
|
+
}).optional(),
|
|
1929
|
+
rpe: optionalNumber
|
|
1930
1930
|
});
|
|
1931
1931
|
const formattedRoutineExerciseSchema = z.object({
|
|
1932
|
-
|
|
1932
|
+
title: z.string().optional(),
|
|
1933
1933
|
index: z.number().optional(),
|
|
1934
|
-
|
|
1935
|
-
notes: z.string().
|
|
1936
|
-
|
|
1937
|
-
|
|
1934
|
+
exercise_template_id: z.string().optional(),
|
|
1935
|
+
notes: z.string().optional(),
|
|
1936
|
+
supersets_id: optionalNumber,
|
|
1937
|
+
rest_seconds: z.string().optional(),
|
|
1938
1938
|
sets: z.array(formattedRoutineSetSchema).optional()
|
|
1939
1939
|
});
|
|
1940
1940
|
const formattedRoutineSchema = z.object({
|
|
1941
1941
|
id: z.string().optional(),
|
|
1942
1942
|
title: z.string().optional(),
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1943
|
+
folder_id: z.number().optional(),
|
|
1944
|
+
created_at: z.string().optional(),
|
|
1945
|
+
updated_at: z.string().optional(),
|
|
1946
1946
|
exercises: z.array(formattedRoutineExerciseSchema).optional()
|
|
1947
1947
|
});
|
|
1948
1948
|
const formattedRoutineFolderSchema = z.object({
|
|
1949
1949
|
id: z.number().optional(),
|
|
1950
1950
|
title: z.string().optional(),
|
|
1951
|
-
|
|
1952
|
-
|
|
1951
|
+
created_at: z.string().optional(),
|
|
1952
|
+
updated_at: z.string().optional()
|
|
1953
1953
|
});
|
|
1954
1954
|
const formattedExerciseTemplateSchema = z.object({
|
|
1955
1955
|
id: z.string().optional(),
|
|
1956
1956
|
title: z.string().optional(),
|
|
1957
1957
|
type: z.string().optional(),
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1958
|
+
primary_muscle_group: z.string().optional(),
|
|
1959
|
+
secondary_muscle_groups: z.array(z.string()).optional(),
|
|
1960
|
+
is_custom: z.boolean().optional()
|
|
1961
1961
|
});
|
|
1962
1962
|
const formattedExerciseHistoryEntrySchema = z.object({
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
reps:
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
rpe:
|
|
1973
|
-
|
|
1974
|
-
|
|
1963
|
+
workout_id: z.string().optional(),
|
|
1964
|
+
workout_title: z.string().optional(),
|
|
1965
|
+
workout_start_time: z.string().optional(),
|
|
1966
|
+
workout_end_time: z.string().optional(),
|
|
1967
|
+
exercise_template_id: z.string().optional(),
|
|
1968
|
+
weight_kg: optionalNumber,
|
|
1969
|
+
reps: optionalNumber,
|
|
1970
|
+
distance_meters: optionalNumber,
|
|
1971
|
+
duration_seconds: optionalNumber,
|
|
1972
|
+
rpe: optionalNumber,
|
|
1973
|
+
custom_metric: optionalNumber,
|
|
1974
|
+
set_type: z.string().optional()
|
|
1975
1975
|
});
|
|
1976
1976
|
const formattedBodyMeasurementSchema = z.object({
|
|
1977
1977
|
date: z.string(),
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
abdomen:
|
|
1989
|
-
waist:
|
|
1990
|
-
hips:
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1978
|
+
weight_kg: optionalNumber,
|
|
1979
|
+
lean_mass_kg: optionalNumber,
|
|
1980
|
+
fat_percent: optionalNumber,
|
|
1981
|
+
neck_cm: optionalNumber,
|
|
1982
|
+
shoulder_cm: optionalNumber,
|
|
1983
|
+
chest_cm: optionalNumber,
|
|
1984
|
+
left_bicep_cm: optionalNumber,
|
|
1985
|
+
right_bicep_cm: optionalNumber,
|
|
1986
|
+
left_forearm_cm: optionalNumber,
|
|
1987
|
+
right_forearm_cm: optionalNumber,
|
|
1988
|
+
abdomen: optionalNumber,
|
|
1989
|
+
waist: optionalNumber,
|
|
1990
|
+
hips: optionalNumber,
|
|
1991
|
+
left_thigh_cm: optionalNumber,
|
|
1992
|
+
right_thigh_cm: optionalNumber,
|
|
1993
|
+
left_calf_cm: optionalNumber,
|
|
1994
|
+
right_calf_cm: optionalNumber
|
|
1995
1995
|
});
|
|
1996
|
-
const
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
cacheStatus: z.enum([
|
|
2000
|
-
"hit",
|
|
2001
|
-
"miss",
|
|
2002
|
-
"not-used"
|
|
2003
|
-
]),
|
|
2004
|
-
itemsScanned: z.number().int().nonnegative()
|
|
1996
|
+
const scanSchema = z.object({
|
|
1997
|
+
pages: z.record(z.string(), z.number().int().nonnegative()),
|
|
1998
|
+
items: z.number().int().nonnegative()
|
|
2005
1999
|
});
|
|
2006
2000
|
const trainingSummarySessionSchema = z.object({
|
|
2007
2001
|
id: z.string().optional(),
|
|
2008
2002
|
title: z.string().optional(),
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2003
|
+
start_time: z.string().optional(),
|
|
2004
|
+
end_time: z.string().optional(),
|
|
2005
|
+
duration_seconds: z.number().int().nonnegative().optional(),
|
|
2006
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2007
|
+
set_count: z.number().int().nonnegative()
|
|
2014
2008
|
});
|
|
2015
2009
|
const compactRoutineSchema = z.object({
|
|
2016
2010
|
id: z.string().optional(),
|
|
2017
2011
|
title: z.string().optional(),
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2012
|
+
folder_id: z.number().optional(),
|
|
2013
|
+
updated_at: z.string().optional(),
|
|
2014
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2015
|
+
set_count: z.number().int().nonnegative()
|
|
2016
|
+
});
|
|
2017
|
+
const formattedWorkoutSummarySchema = z.object({
|
|
2018
|
+
id: z.string().optional(),
|
|
2019
|
+
title: z.string().optional(),
|
|
2020
|
+
start_time: z.string().optional(),
|
|
2021
|
+
end_time: z.string().optional(),
|
|
2022
|
+
duration: z.string(),
|
|
2023
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2024
|
+
set_count: z.number().int().nonnegative()
|
|
2022
2025
|
});
|
|
2023
2026
|
const paginationOutputSchema = {
|
|
2024
2027
|
page: z.number().int().positive(),
|
|
2025
|
-
|
|
2026
|
-
|
|
2028
|
+
page_count: z.number().int().nonnegative().optional(),
|
|
2029
|
+
has_next_page: z.boolean().optional()
|
|
2027
2030
|
};
|
|
2028
2031
|
function normalizePaginatedInput(data) {
|
|
2029
2032
|
if (data === void 0) return {
|
|
@@ -2037,11 +2040,11 @@ function normalizePaginatedInput(data) {
|
|
|
2037
2040
|
};
|
|
2038
2041
|
}
|
|
2039
2042
|
const workoutsOutputSchema = {
|
|
2040
|
-
workouts: z.array(
|
|
2043
|
+
workouts: z.array(formattedWorkoutSummarySchema),
|
|
2041
2044
|
...paginationOutputSchema
|
|
2042
2045
|
};
|
|
2043
2046
|
const workoutOutputSchema = { workout: formattedWorkoutSchema.nullable() };
|
|
2044
|
-
const workoutCountOutputSchema = {
|
|
2047
|
+
const workoutCountOutputSchema = { workout_count: z.number().int() };
|
|
2045
2048
|
const formattedUpdatedWorkoutSchema = z.object({
|
|
2046
2049
|
type: z.literal("updated"),
|
|
2047
2050
|
workout: formattedWorkoutSchema
|
|
@@ -2049,70 +2052,70 @@ const formattedUpdatedWorkoutSchema = z.object({
|
|
|
2049
2052
|
const formattedDeletedWorkoutSchema = z.object({
|
|
2050
2053
|
type: z.literal("deleted"),
|
|
2051
2054
|
id: z.string(),
|
|
2052
|
-
|
|
2055
|
+
deleted_at: z.string().optional()
|
|
2053
2056
|
});
|
|
2054
2057
|
const workoutEventsOutputSchema = {
|
|
2055
2058
|
events: z.array(z.union([formattedUpdatedWorkoutSchema, formattedDeletedWorkoutSchema])),
|
|
2056
2059
|
...paginationOutputSchema
|
|
2057
2060
|
};
|
|
2058
2061
|
const routinesOutputSchema = {
|
|
2059
|
-
routines: z.array(
|
|
2062
|
+
routines: z.array(compactRoutineSchema),
|
|
2060
2063
|
...paginationOutputSchema
|
|
2061
2064
|
};
|
|
2062
2065
|
const routineOutputSchema = { routine: formattedRoutineSchema.nullable() };
|
|
2063
2066
|
const exerciseTemplatesOutputSchema = {
|
|
2064
|
-
|
|
2067
|
+
exercise_templates: z.array(formattedExerciseTemplateSchema),
|
|
2065
2068
|
...paginationOutputSchema
|
|
2066
2069
|
};
|
|
2067
|
-
const exerciseTemplateSearchOutputSchema = {
|
|
2068
|
-
const exerciseTemplateOutputSchema = {
|
|
2069
|
-
const exerciseHistoryOutputSchema = {
|
|
2070
|
+
const exerciseTemplateSearchOutputSchema = { exercise_templates: z.array(formattedExerciseTemplateSchema) };
|
|
2071
|
+
const exerciseTemplateOutputSchema = { exercise_template: formattedExerciseTemplateSchema.nullable() };
|
|
2072
|
+
const exerciseHistoryOutputSchema = { exercise_history: z.array(formattedExerciseHistoryEntrySchema) };
|
|
2070
2073
|
const routineFoldersOutputSchema = {
|
|
2071
|
-
|
|
2074
|
+
routine_folders: z.array(formattedRoutineFolderSchema),
|
|
2072
2075
|
...paginationOutputSchema
|
|
2073
2076
|
};
|
|
2074
|
-
const routineFolderOutputSchema = {
|
|
2077
|
+
const routineFolderOutputSchema = { routine_folder: formattedRoutineFolderSchema.nullable() };
|
|
2075
2078
|
const bodyMeasurementsOutputSchema = {
|
|
2076
|
-
|
|
2079
|
+
body_measurements: z.array(formattedBodyMeasurementSchema),
|
|
2077
2080
|
...paginationOutputSchema
|
|
2078
2081
|
};
|
|
2079
|
-
const bodyMeasurementOutputSchema = {
|
|
2082
|
+
const bodyMeasurementOutputSchema = { body_measurement: formattedBodyMeasurementSchema.nullable() };
|
|
2080
2083
|
const userOutputSchema = { user: userInfoSchema.nullable() };
|
|
2081
2084
|
const trainingSummaryOutputSchema = {
|
|
2082
2085
|
period: z.object({
|
|
2083
|
-
|
|
2084
|
-
|
|
2086
|
+
start_date: z.string(),
|
|
2087
|
+
end_date: z.string(),
|
|
2085
2088
|
weeks: z.number().int().positive()
|
|
2086
2089
|
}),
|
|
2087
2090
|
workouts: z.object({
|
|
2088
2091
|
count: z.number().int().nonnegative(),
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2092
|
+
total_duration_seconds: z.number().int().nonnegative(),
|
|
2093
|
+
exercise_count: z.number().int().nonnegative(),
|
|
2094
|
+
set_count: z.number().int().nonnegative(),
|
|
2095
|
+
unique_exercise_template_ids: z.array(z.string()),
|
|
2093
2096
|
sessions: z.array(trainingSummarySessionSchema)
|
|
2094
2097
|
}),
|
|
2095
|
-
|
|
2098
|
+
body_measurements: z.object({
|
|
2096
2099
|
count: z.number().int().nonnegative(),
|
|
2097
2100
|
latest: z.object({
|
|
2098
2101
|
date: z.string(),
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
}).
|
|
2102
|
+
weight_kg: optionalNumber,
|
|
2103
|
+
lean_mass_kg: optionalNumber,
|
|
2104
|
+
fat_percent: optionalNumber
|
|
2105
|
+
}).optional(),
|
|
2103
2106
|
earliest: z.object({
|
|
2104
2107
|
date: z.string(),
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
}).
|
|
2109
|
-
|
|
2108
|
+
weight_kg: optionalNumber,
|
|
2109
|
+
lean_mass_kg: optionalNumber,
|
|
2110
|
+
fat_percent: optionalNumber
|
|
2111
|
+
}).optional(),
|
|
2112
|
+
weight_change_kg: optionalNumber
|
|
2110
2113
|
}),
|
|
2111
|
-
|
|
2114
|
+
scan: scanSchema
|
|
2112
2115
|
};
|
|
2113
2116
|
const compactRoutinesOutputSchema = {
|
|
2114
2117
|
routines: z.array(compactRoutineSchema),
|
|
2115
|
-
|
|
2118
|
+
scan: scanSchema
|
|
2116
2119
|
};
|
|
2117
2120
|
function getSupersetId(exercise) {
|
|
2118
2121
|
if (exercise.superset_id !== void 0) return exercise.superset_id;
|
|
@@ -2120,99 +2123,104 @@ function getSupersetId(exercise) {
|
|
|
2120
2123
|
return null;
|
|
2121
2124
|
}
|
|
2122
2125
|
/**
|
|
2123
|
-
*
|
|
2124
|
-
*
|
|
2125
|
-
* @param workout - The workout object from the API
|
|
2126
|
-
* @returns A formatted workout object with standardized properties
|
|
2126
|
+
* Project a workout into the sparse snake_case MCP contract.
|
|
2127
2127
|
*/
|
|
2128
|
-
function
|
|
2128
|
+
function projectWorkout(workout) {
|
|
2129
2129
|
return {
|
|
2130
|
-
id: workout.id,
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2130
|
+
...workout.id == null ? {} : { id: workout.id },
|
|
2131
|
+
...workout.routine_id == null ? {} : { routine_id: workout.routine_id },
|
|
2132
|
+
...workout.title == null ? {} : { title: workout.title },
|
|
2133
|
+
...workout.description == null ? {} : { description: workout.description },
|
|
2134
|
+
...workout.start_time == null ? {} : { start_time: workout.start_time },
|
|
2135
|
+
...workout.end_time == null ? {} : { end_time: workout.end_time },
|
|
2136
|
+
...workout.created_at == null ? {} : { created_at: workout.created_at },
|
|
2137
|
+
...workout.updated_at == null ? {} : { updated_at: workout.updated_at },
|
|
2137
2138
|
duration: calculateDuration(workout.start_time, workout.end_time),
|
|
2138
|
-
exercises: workout.exercises
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
};
|
|
2156
|
-
})
|
|
2139
|
+
...workout.exercises ? { exercises: workout.exercises.map((exercise) => ({
|
|
2140
|
+
...exercise.index === void 0 ? {} : { index: exercise.index },
|
|
2141
|
+
...exercise.title == null ? {} : { title: exercise.title },
|
|
2142
|
+
...exercise.exercise_template_id == null ? {} : { exercise_template_id: exercise.exercise_template_id },
|
|
2143
|
+
...exercise.notes == null ? {} : { notes: exercise.notes },
|
|
2144
|
+
...getSupersetId(exercise) == null ? {} : { supersets_id: getSupersetId(exercise) ?? void 0 },
|
|
2145
|
+
...exercise.sets ? { sets: exercise.sets.map((set) => ({
|
|
2146
|
+
...set.index === void 0 ? {} : { index: set.index },
|
|
2147
|
+
...set.type == null ? {} : { type: set.type },
|
|
2148
|
+
...set.weight_kg == null ? {} : { weight_kg: set.weight_kg },
|
|
2149
|
+
...set.reps == null ? {} : { reps: set.reps },
|
|
2150
|
+
...set.distance_meters == null ? {} : { distance_meters: set.distance_meters },
|
|
2151
|
+
...set.duration_seconds == null ? {} : { duration_seconds: set.duration_seconds },
|
|
2152
|
+
...set.rpe == null ? {} : { rpe: set.rpe },
|
|
2153
|
+
...set.custom_metric == null ? {} : { custom_metric: set.custom_metric }
|
|
2154
|
+
})) } : {}
|
|
2155
|
+
})) } : {}
|
|
2157
2156
|
};
|
|
2158
2157
|
}
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
*
|
|
2162
|
-
* @param routine - The routine object from the API
|
|
2163
|
-
* @returns A formatted routine object with standardized properties
|
|
2164
|
-
*/
|
|
2165
|
-
function formatRoutine(routine) {
|
|
2158
|
+
function summarizeWorkout(workout) {
|
|
2159
|
+
const exercises = workout.exercises ?? [];
|
|
2166
2160
|
return {
|
|
2167
|
-
id:
|
|
2168
|
-
title:
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
name: exercise.title,
|
|
2175
|
-
index: exercise.index,
|
|
2176
|
-
exerciseTemplateId: exercise.exercise_template_id,
|
|
2177
|
-
notes: exercise.notes,
|
|
2178
|
-
supersetId: getSupersetId(exercise),
|
|
2179
|
-
restSeconds: exercise.rest_seconds,
|
|
2180
|
-
sets: exercise.sets?.map((set) => ({
|
|
2181
|
-
index: set.index,
|
|
2182
|
-
type: set.type,
|
|
2183
|
-
weight: set.weight_kg,
|
|
2184
|
-
reps: set.reps,
|
|
2185
|
-
...set.rep_range !== void 0 && { repRange: set.rep_range },
|
|
2186
|
-
distance: set.distance_meters,
|
|
2187
|
-
duration: set.duration_seconds,
|
|
2188
|
-
...set.rpe !== void 0 && { rpe: set.rpe },
|
|
2189
|
-
customMetric: set.custom_metric
|
|
2190
|
-
}))
|
|
2191
|
-
};
|
|
2192
|
-
})
|
|
2161
|
+
...workout.id == null ? {} : { id: workout.id },
|
|
2162
|
+
...workout.title == null ? {} : { title: workout.title },
|
|
2163
|
+
...workout.start_time == null ? {} : { start_time: workout.start_time },
|
|
2164
|
+
...workout.end_time == null ? {} : { end_time: workout.end_time },
|
|
2165
|
+
duration: calculateDuration(workout.start_time, workout.end_time),
|
|
2166
|
+
exercise_count: exercises.length,
|
|
2167
|
+
set_count: exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0)
|
|
2193
2168
|
};
|
|
2194
2169
|
}
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2170
|
+
function projectRoutine(routine) {
|
|
2171
|
+
return {
|
|
2172
|
+
...routine.id == null ? {} : { id: routine.id },
|
|
2173
|
+
...routine.title == null ? {} : { title: routine.title },
|
|
2174
|
+
...routine.folder_id == null ? {} : { folder_id: routine.folder_id },
|
|
2175
|
+
...routine.created_at == null ? {} : { created_at: routine.created_at },
|
|
2176
|
+
...routine.updated_at == null ? {} : { updated_at: routine.updated_at },
|
|
2177
|
+
...routine.exercises ? { exercises: routine.exercises.map((exercise) => ({
|
|
2178
|
+
...exercise.title == null ? {} : { title: exercise.title },
|
|
2179
|
+
...exercise.index === void 0 ? {} : { index: exercise.index },
|
|
2180
|
+
...exercise.exercise_template_id == null ? {} : { exercise_template_id: exercise.exercise_template_id },
|
|
2181
|
+
...exercise.notes == null ? {} : { notes: exercise.notes },
|
|
2182
|
+
...getSupersetId(exercise) == null ? {} : { supersets_id: getSupersetId(exercise) ?? void 0 },
|
|
2183
|
+
...exercise.rest_seconds == null ? {} : { rest_seconds: exercise.rest_seconds },
|
|
2184
|
+
...exercise.sets ? { sets: exercise.sets.map((set) => {
|
|
2185
|
+
const repRange = set.rep_range;
|
|
2186
|
+
const normalizedRepRange = repRange && (repRange.start != null || repRange.end != null) ? {
|
|
2187
|
+
...repRange.start == null ? {} : { start: repRange.start },
|
|
2188
|
+
...repRange.end == null ? {} : { end: repRange.end }
|
|
2189
|
+
} : void 0;
|
|
2190
|
+
return {
|
|
2191
|
+
...set.index === void 0 ? {} : { index: set.index },
|
|
2192
|
+
...set.type == null ? {} : { type: set.type },
|
|
2193
|
+
...set.weight_kg == null ? {} : { weight_kg: set.weight_kg },
|
|
2194
|
+
...set.reps == null ? {} : { reps: set.reps },
|
|
2195
|
+
...normalizedRepRange ? { rep_range: normalizedRepRange } : {},
|
|
2196
|
+
...set.distance_meters == null ? {} : { distance_meters: set.distance_meters },
|
|
2197
|
+
...set.duration_seconds == null ? {} : { duration_seconds: set.duration_seconds },
|
|
2198
|
+
...set.rpe == null ? {} : { rpe: set.rpe },
|
|
2199
|
+
...set.custom_metric == null ? {} : { custom_metric: set.custom_metric }
|
|
2200
|
+
};
|
|
2201
|
+
}) } : {}
|
|
2202
|
+
})) } : {}
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
function summarizeRoutine(routine) {
|
|
2206
|
+
const exercises = routine.exercises ?? [];
|
|
2207
|
+
return {
|
|
2208
|
+
...routine.id == null ? {} : { id: routine.id },
|
|
2209
|
+
...routine.title == null ? {} : { title: routine.title },
|
|
2210
|
+
...routine.folder_id == null ? {} : { folder_id: routine.folder_id },
|
|
2211
|
+
...routine.updated_at == null ? { updated_at: void 0 } : { updated_at: routine.updated_at },
|
|
2212
|
+
exercise_count: exercises.length,
|
|
2213
|
+
set_count: exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0)
|
|
2214
|
+
};
|
|
2215
|
+
}
|
|
2216
|
+
function projectRoutineFolder(folder) {
|
|
2202
2217
|
return {
|
|
2203
2218
|
id: folder.id,
|
|
2204
2219
|
title: folder.title,
|
|
2205
|
-
|
|
2206
|
-
|
|
2220
|
+
created_at: folder.created_at,
|
|
2221
|
+
updated_at: folder.updated_at
|
|
2207
2222
|
};
|
|
2208
2223
|
}
|
|
2209
|
-
/**
|
|
2210
|
-
* Calculate duration between two ISO timestamp strings
|
|
2211
|
-
*
|
|
2212
|
-
* @param startTime - The start time as ISO string or timestamp
|
|
2213
|
-
* @param endTime - The end time as ISO string or timestamp
|
|
2214
|
-
* @returns A formatted duration string (e.g. "1h 30m 45s") or "Unknown duration" if inputs are invalid
|
|
2215
|
-
*/
|
|
2216
2224
|
function calculateDuration(startTime, endTime) {
|
|
2217
2225
|
if (!startTime || !endTime) return "Unknown duration";
|
|
2218
2226
|
try {
|
|
@@ -2227,69 +2235,53 @@ function calculateDuration(startTime, endTime) {
|
|
|
2227
2235
|
return "Unknown duration";
|
|
2228
2236
|
}
|
|
2229
2237
|
}
|
|
2230
|
-
|
|
2231
|
-
* Format an exercise template object for consistent presentation
|
|
2232
|
-
*
|
|
2233
|
-
* @param template - The exercise template object from the API
|
|
2234
|
-
* @returns A formatted exercise template object with standardized properties
|
|
2235
|
-
*/
|
|
2236
|
-
function formatExerciseTemplate(template) {
|
|
2237
|
-
return {
|
|
2238
|
-
id: template.id,
|
|
2239
|
-
title: template.title,
|
|
2240
|
-
type: template.type,
|
|
2241
|
-
primaryMuscleGroup: template.primary_muscle_group,
|
|
2242
|
-
secondaryMuscleGroups: template.secondary_muscle_groups,
|
|
2243
|
-
isCustom: template.is_custom
|
|
2244
|
-
};
|
|
2245
|
-
}
|
|
2246
|
-
function formatExerciseHistoryEntry(entry) {
|
|
2238
|
+
function normalizeExerciseHistoryEntry(entry) {
|
|
2247
2239
|
return {
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
reps: entry.reps,
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
rpe: entry.rpe,
|
|
2258
|
-
|
|
2259
|
-
|
|
2240
|
+
...entry.workout_id == null ? {} : { workout_id: entry.workout_id },
|
|
2241
|
+
...entry.workout_title == null ? {} : { workout_title: entry.workout_title },
|
|
2242
|
+
...entry.workout_start_time == null ? {} : { workout_start_time: entry.workout_start_time },
|
|
2243
|
+
...entry.workout_end_time == null ? {} : { workout_end_time: entry.workout_end_time },
|
|
2244
|
+
...entry.exercise_template_id == null ? {} : { exercise_template_id: entry.exercise_template_id },
|
|
2245
|
+
...entry.weight_kg == null ? {} : { weight_kg: entry.weight_kg },
|
|
2246
|
+
...entry.reps == null ? {} : { reps: entry.reps },
|
|
2247
|
+
...entry.distance_meters == null ? {} : { distance_meters: entry.distance_meters },
|
|
2248
|
+
...entry.duration_seconds == null ? {} : { duration_seconds: entry.duration_seconds },
|
|
2249
|
+
...entry.rpe == null ? {} : { rpe: entry.rpe },
|
|
2250
|
+
...entry.custom_metric == null ? {} : { custom_metric: entry.custom_metric },
|
|
2251
|
+
...entry.set_type == null ? {} : { set_type: entry.set_type }
|
|
2260
2252
|
};
|
|
2261
2253
|
}
|
|
2262
|
-
function
|
|
2254
|
+
function normalizeBodyMeasurement(measurement) {
|
|
2263
2255
|
return {
|
|
2264
2256
|
date: measurement.date,
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
abdomen: measurement.abdomen
|
|
2276
|
-
waist: measurement.waist
|
|
2277
|
-
hips: measurement.hips
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2257
|
+
...measurement.weight_kg == null ? {} : { weight_kg: measurement.weight_kg },
|
|
2258
|
+
...measurement.lean_mass_kg == null ? {} : { lean_mass_kg: measurement.lean_mass_kg },
|
|
2259
|
+
...measurement.fat_percent == null ? {} : { fat_percent: measurement.fat_percent },
|
|
2260
|
+
...measurement.neck_cm == null ? {} : { neck_cm: measurement.neck_cm },
|
|
2261
|
+
...measurement.shoulder_cm == null ? {} : { shoulder_cm: measurement.shoulder_cm },
|
|
2262
|
+
...measurement.chest_cm == null ? {} : { chest_cm: measurement.chest_cm },
|
|
2263
|
+
...measurement.left_bicep_cm == null ? {} : { left_bicep_cm: measurement.left_bicep_cm },
|
|
2264
|
+
...measurement.right_bicep_cm == null ? {} : { right_bicep_cm: measurement.right_bicep_cm },
|
|
2265
|
+
...measurement.left_forearm_cm == null ? {} : { left_forearm_cm: measurement.left_forearm_cm },
|
|
2266
|
+
...measurement.right_forearm_cm == null ? {} : { right_forearm_cm: measurement.right_forearm_cm },
|
|
2267
|
+
...measurement.abdomen == null ? {} : { abdomen: measurement.abdomen },
|
|
2268
|
+
...measurement.waist == null ? {} : { waist: measurement.waist },
|
|
2269
|
+
...measurement.hips == null ? {} : { hips: measurement.hips },
|
|
2270
|
+
...measurement.left_thigh == null ? {} : { left_thigh_cm: measurement.left_thigh },
|
|
2271
|
+
...measurement.right_thigh == null ? {} : { right_thigh_cm: measurement.right_thigh },
|
|
2272
|
+
...measurement.left_calf == null ? {} : { left_calf_cm: measurement.left_calf },
|
|
2273
|
+
...measurement.right_calf == null ? {} : { right_calf_cm: measurement.right_calf }
|
|
2282
2274
|
};
|
|
2283
2275
|
}
|
|
2284
|
-
function
|
|
2276
|
+
function projectWorkoutEvent(event) {
|
|
2285
2277
|
if (event.type === "updated" && "workout" in event) return {
|
|
2286
2278
|
type: "updated",
|
|
2287
|
-
workout:
|
|
2279
|
+
workout: projectWorkout(event.workout)
|
|
2288
2280
|
};
|
|
2289
2281
|
if (event.type === "deleted" && "id" in event) return {
|
|
2290
2282
|
type: "deleted",
|
|
2291
2283
|
id: event.id,
|
|
2292
|
-
|
|
2284
|
+
deleted_at: event.deleted_at
|
|
2293
2285
|
};
|
|
2294
2286
|
throw new Error(`Unsupported workout event type: ${event.type}`);
|
|
2295
2287
|
}
|
|
@@ -2333,10 +2325,10 @@ const workoutsResponse = defineStructuredResponseContract({
|
|
|
2333
2325
|
normalize: (input) => {
|
|
2334
2326
|
const data = normalizePaginatedInput(input);
|
|
2335
2327
|
return {
|
|
2336
|
-
workouts: data.items.map(
|
|
2328
|
+
workouts: data.items.map(summarizeWorkout),
|
|
2337
2329
|
page: data.page,
|
|
2338
|
-
|
|
2339
|
-
|
|
2330
|
+
page_count: data.pageCount,
|
|
2331
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2340
2332
|
};
|
|
2341
2333
|
},
|
|
2342
2334
|
legacyJson: ({ workouts }) => workouts,
|
|
@@ -2348,9 +2340,9 @@ const workoutsResponse = defineStructuredResponseContract({
|
|
|
2348
2340
|
});
|
|
2349
2341
|
const workoutResponse = defineStructuredResponseContract({
|
|
2350
2342
|
outputSchema: workoutOutputSchema,
|
|
2351
|
-
normalize: (data) => ({ workout: data.workout ?
|
|
2343
|
+
normalize: (data) => ({ workout: data.workout ? projectWorkout(data.workout) : null }),
|
|
2352
2344
|
legacyJson: ({ workout }) => workout,
|
|
2353
|
-
text: ({
|
|
2345
|
+
text: ({ workout_id }, { workout }) => workout === null ? `Workout with ID ${workout_id} not found` : void 0,
|
|
2354
2346
|
telemetry: ({ workout, expected404Outcome }) => ({
|
|
2355
2347
|
...workoutResultTelemetry(workout),
|
|
2356
2348
|
expected404Outcome
|
|
@@ -2358,17 +2350,17 @@ const workoutResponse = defineStructuredResponseContract({
|
|
|
2358
2350
|
});
|
|
2359
2351
|
const workoutCountResponse = defineStructuredResponseContract({
|
|
2360
2352
|
outputSchema: workoutCountOutputSchema,
|
|
2361
|
-
normalize: (count) => ({ count }),
|
|
2353
|
+
normalize: (count) => ({ workout_count: count }),
|
|
2362
2354
|
legacyJson: (output) => output,
|
|
2363
2355
|
telemetry: (count) => ({ itemCountBucket: bucketCount(count) })
|
|
2364
2356
|
});
|
|
2365
2357
|
const workoutEventsResponse = defineStructuredResponseContract({
|
|
2366
2358
|
outputSchema: workoutEventsOutputSchema,
|
|
2367
2359
|
normalize: (data) => ({
|
|
2368
|
-
events: data.events?.map(
|
|
2360
|
+
events: data.events?.map(projectWorkoutEvent) ?? [],
|
|
2369
2361
|
page: data.page,
|
|
2370
|
-
|
|
2371
|
-
|
|
2362
|
+
page_count: data.pageCount,
|
|
2363
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2372
2364
|
}),
|
|
2373
2365
|
legacyJson: ({ events }) => events,
|
|
2374
2366
|
text: ({ since }, { events }) => events.length === 0 ? `No workout events found for the specified parameters since ${since}` : void 0,
|
|
@@ -2382,10 +2374,10 @@ const routinesResponse = defineStructuredResponseContract({
|
|
|
2382
2374
|
normalize: (input) => {
|
|
2383
2375
|
const data = normalizePaginatedInput(input);
|
|
2384
2376
|
return {
|
|
2385
|
-
routines: data.items.map(
|
|
2377
|
+
routines: data.items.map(summarizeRoutine),
|
|
2386
2378
|
page: data.page,
|
|
2387
|
-
|
|
2388
|
-
|
|
2379
|
+
page_count: data.pageCount,
|
|
2380
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2389
2381
|
};
|
|
2390
2382
|
},
|
|
2391
2383
|
legacyJson: ({ routines }) => routines,
|
|
@@ -2397,9 +2389,9 @@ const routinesResponse = defineStructuredResponseContract({
|
|
|
2397
2389
|
});
|
|
2398
2390
|
const routineResponse = defineStructuredResponseContract({
|
|
2399
2391
|
outputSchema: routineOutputSchema,
|
|
2400
|
-
normalize: (data) => ({ routine: data.routine ?
|
|
2392
|
+
normalize: (data) => ({ routine: data.routine ? projectRoutine(data.routine) : null }),
|
|
2401
2393
|
legacyJson: ({ routine }) => routine,
|
|
2402
|
-
text: ({
|
|
2394
|
+
text: ({ routine_id }, { routine }) => routine === null ? `Routine with ID ${routine_id} not found` : void 0,
|
|
2403
2395
|
telemetry: ({ routine, expected404Outcome }) => ({
|
|
2404
2396
|
...routineResultTelemetry(routine),
|
|
2405
2397
|
expected404Outcome
|
|
@@ -2410,14 +2402,14 @@ const exerciseTemplatesResponse = defineStructuredResponseContract({
|
|
|
2410
2402
|
normalize: (input) => {
|
|
2411
2403
|
const data = normalizePaginatedInput(input);
|
|
2412
2404
|
return {
|
|
2413
|
-
|
|
2405
|
+
exercise_templates: data.items,
|
|
2414
2406
|
page: data.page,
|
|
2415
|
-
|
|
2416
|
-
|
|
2407
|
+
page_count: data.pageCount,
|
|
2408
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2417
2409
|
};
|
|
2418
2410
|
},
|
|
2419
|
-
legacyJson: ({
|
|
2420
|
-
text: (_data, {
|
|
2411
|
+
legacyJson: ({ exercise_templates }) => exercise_templates,
|
|
2412
|
+
text: (_data, { exercise_templates }) => exercise_templates.length === 0 ? "No exercise templates found for the specified parameters" : void 0,
|
|
2421
2413
|
telemetry: (templates) => ({
|
|
2422
2414
|
itemCountBucket: bucketCount(normalizePaginatedInput(templates).items.length),
|
|
2423
2415
|
expected404Outcome: normalizePaginatedInput(templates).expected404Outcome
|
|
@@ -2425,26 +2417,26 @@ const exerciseTemplatesResponse = defineStructuredResponseContract({
|
|
|
2425
2417
|
});
|
|
2426
2418
|
const exerciseTemplateResponse = defineStructuredResponseContract({
|
|
2427
2419
|
outputSchema: exerciseTemplateOutputSchema,
|
|
2428
|
-
normalize: (data) => ({
|
|
2429
|
-
legacyJson: ({
|
|
2430
|
-
text: ({
|
|
2431
|
-
telemetry: ({
|
|
2432
|
-
itemCountBucket: bucketCount(
|
|
2420
|
+
normalize: (data) => ({ exercise_template: data.exercise_template ?? null }),
|
|
2421
|
+
legacyJson: ({ exercise_template }) => exercise_template,
|
|
2422
|
+
text: ({ exercise_template_id }, { exercise_template }) => exercise_template === null ? `Exercise template with ID ${exercise_template_id} not found` : void 0,
|
|
2423
|
+
telemetry: ({ exercise_template, expected404Outcome }) => ({
|
|
2424
|
+
itemCountBucket: bucketCount(exercise_template ? 1 : 0),
|
|
2433
2425
|
expected404Outcome
|
|
2434
2426
|
})
|
|
2435
2427
|
});
|
|
2436
2428
|
const exerciseHistoryResponse = defineStructuredResponseContract({
|
|
2437
2429
|
outputSchema: exerciseHistoryOutputSchema,
|
|
2438
|
-
normalize: (data) => ({
|
|
2439
|
-
legacyJson: ({
|
|
2440
|
-
text: ({
|
|
2441
|
-
telemetry: (data) => ({ itemCountBucket: bucketCount(data.
|
|
2430
|
+
normalize: (data) => ({ exercise_history: data.exercise_history?.map(normalizeExerciseHistoryEntry) ?? [] }),
|
|
2431
|
+
legacyJson: ({ exercise_history }) => exercise_history,
|
|
2432
|
+
text: ({ exercise_template_id }, { exercise_history }) => exercise_history.length === 0 ? `No exercise history found for template ${exercise_template_id}` : void 0,
|
|
2433
|
+
telemetry: (data) => ({ itemCountBucket: bucketCount(data.exercise_history?.length ?? 0) })
|
|
2442
2434
|
});
|
|
2443
2435
|
const searchExerciseTemplatesResponse = defineStructuredResponseContract({
|
|
2444
2436
|
outputSchema: exerciseTemplateSearchOutputSchema,
|
|
2445
|
-
normalize: (data) => ({
|
|
2446
|
-
legacyJson: ({
|
|
2447
|
-
text: ({ query,
|
|
2437
|
+
normalize: (data) => ({ exercise_templates: data.results }),
|
|
2438
|
+
legacyJson: ({ exercise_templates }) => exercise_templates,
|
|
2439
|
+
text: ({ query, primary_muscle_group }, { exercise_templates }) => exercise_templates.length === 0 ? `No exercise templates found matching "${query}"${primary_muscle_group ? ` with primary muscle group "${primary_muscle_group}"` : ""}` : void 0,
|
|
2448
2440
|
telemetry: (data) => ({ itemCountBucket: bucketCount(data.results.length) })
|
|
2449
2441
|
});
|
|
2450
2442
|
const routineFoldersResponse = defineStructuredResponseContract({
|
|
@@ -2452,14 +2444,14 @@ const routineFoldersResponse = defineStructuredResponseContract({
|
|
|
2452
2444
|
normalize: (input) => {
|
|
2453
2445
|
const data = normalizePaginatedInput(input);
|
|
2454
2446
|
return {
|
|
2455
|
-
|
|
2447
|
+
routine_folders: data.items.map(projectRoutineFolder),
|
|
2456
2448
|
page: data.page,
|
|
2457
|
-
|
|
2458
|
-
|
|
2449
|
+
page_count: data.pageCount,
|
|
2450
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2459
2451
|
};
|
|
2460
2452
|
},
|
|
2461
|
-
legacyJson: ({
|
|
2462
|
-
text: (_data, {
|
|
2453
|
+
legacyJson: ({ routine_folders }) => routine_folders,
|
|
2454
|
+
text: (_data, { routine_folders }) => routine_folders.length === 0 ? "No routine folders found for the specified parameters" : void 0,
|
|
2463
2455
|
telemetry: (folders) => ({
|
|
2464
2456
|
itemCountBucket: bucketCount(normalizePaginatedInput(folders).items.length),
|
|
2465
2457
|
expected404Outcome: normalizePaginatedInput(folders).expected404Outcome
|
|
@@ -2467,11 +2459,11 @@ const routineFoldersResponse = defineStructuredResponseContract({
|
|
|
2467
2459
|
});
|
|
2468
2460
|
const routineFolderResponse = defineStructuredResponseContract({
|
|
2469
2461
|
outputSchema: routineFolderOutputSchema,
|
|
2470
|
-
normalize: (data) => ({
|
|
2471
|
-
legacyJson: ({
|
|
2472
|
-
text: ({
|
|
2473
|
-
telemetry: ({
|
|
2474
|
-
itemCountBucket: bucketCount(
|
|
2462
|
+
normalize: (data) => ({ routine_folder: data.routine_folder ? projectRoutineFolder(data.routine_folder) : null }),
|
|
2463
|
+
legacyJson: ({ routine_folder }) => routine_folder,
|
|
2464
|
+
text: ({ folder_id }, { routine_folder }) => routine_folder === null ? `Routine folder with ID ${folder_id} not found` : void 0,
|
|
2465
|
+
telemetry: ({ routine_folder, expected404Outcome }) => ({
|
|
2466
|
+
itemCountBucket: bucketCount(routine_folder ? 1 : 0),
|
|
2475
2467
|
expected404Outcome
|
|
2476
2468
|
})
|
|
2477
2469
|
});
|
|
@@ -2480,14 +2472,14 @@ const bodyMeasurementsResponse = defineStructuredResponseContract({
|
|
|
2480
2472
|
normalize: (input) => {
|
|
2481
2473
|
const data = normalizePaginatedInput(input);
|
|
2482
2474
|
return {
|
|
2483
|
-
|
|
2475
|
+
body_measurements: data.items.map(normalizeBodyMeasurement),
|
|
2484
2476
|
page: data.page,
|
|
2485
|
-
|
|
2486
|
-
|
|
2477
|
+
page_count: data.pageCount,
|
|
2478
|
+
has_next_page: data.pageCount === void 0 ? void 0 : data.page < data.pageCount
|
|
2487
2479
|
};
|
|
2488
2480
|
},
|
|
2489
|
-
legacyJson: ({
|
|
2490
|
-
text: (_data, {
|
|
2481
|
+
legacyJson: ({ body_measurements }) => body_measurements,
|
|
2482
|
+
text: (_data, { body_measurements }) => body_measurements.length === 0 ? "No body measurements found for the specified parameters" : void 0,
|
|
2491
2483
|
telemetry: (measurements) => ({
|
|
2492
2484
|
itemCountBucket: bucketCount(normalizePaginatedInput(measurements).items.length),
|
|
2493
2485
|
expected404Outcome: normalizePaginatedInput(measurements).expected404Outcome
|
|
@@ -2495,11 +2487,11 @@ const bodyMeasurementsResponse = defineStructuredResponseContract({
|
|
|
2495
2487
|
});
|
|
2496
2488
|
const bodyMeasurementResponse = defineStructuredResponseContract({
|
|
2497
2489
|
outputSchema: bodyMeasurementOutputSchema,
|
|
2498
|
-
normalize: (data) => ({
|
|
2499
|
-
legacyJson: ({
|
|
2500
|
-
text: ({ date }, {
|
|
2501
|
-
telemetry: ({
|
|
2502
|
-
itemCountBucket: bucketCount(
|
|
2490
|
+
normalize: (data) => ({ body_measurement: data.body_measurement ? normalizeBodyMeasurement(data.body_measurement) : null }),
|
|
2491
|
+
legacyJson: ({ body_measurement }) => body_measurement,
|
|
2492
|
+
text: ({ date }, { body_measurement }) => body_measurement === null ? `No body measurement found for date ${date}` : void 0,
|
|
2493
|
+
telemetry: ({ body_measurement, expected404Outcome }) => ({
|
|
2494
|
+
itemCountBucket: bucketCount(body_measurement ? 1 : 0),
|
|
2503
2495
|
expected404Outcome
|
|
2504
2496
|
})
|
|
2505
2497
|
});
|
|
@@ -2512,17 +2504,46 @@ const userResponse = defineStructuredResponseContract({
|
|
|
2512
2504
|
});
|
|
2513
2505
|
const trainingSummaryResponse = defineStructuredResponseContract({
|
|
2514
2506
|
outputSchema: trainingSummaryOutputSchema,
|
|
2515
|
-
normalize: (data) =>
|
|
2507
|
+
normalize: (data) => {
|
|
2508
|
+
const { workflow, ...result } = data;
|
|
2509
|
+
const { latest, earliest, weight_change_kg, ...body_measurements } = result.body_measurements;
|
|
2510
|
+
return {
|
|
2511
|
+
...result,
|
|
2512
|
+
body_measurements: {
|
|
2513
|
+
...body_measurements,
|
|
2514
|
+
...latest ? { latest } : {},
|
|
2515
|
+
...earliest ? { earliest } : {},
|
|
2516
|
+
...weight_change_kg == null ? {} : { weight_change_kg }
|
|
2517
|
+
},
|
|
2518
|
+
scan: {
|
|
2519
|
+
pages: workflow.pagination,
|
|
2520
|
+
items: workflow.itemsScanned
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
},
|
|
2516
2524
|
legacyJson: (output) => output,
|
|
2517
|
-
text: (data) => data.workouts.count === 0 && data.
|
|
2525
|
+
text: (data) => data.workouts.count === 0 && data.body_measurements.count === 0 ? "No workouts or body measurements found for the specified period" : void 0,
|
|
2518
2526
|
telemetry: (data) => ({
|
|
2519
|
-
itemCountBucket: bucketCount(data.workouts.count + data.
|
|
2527
|
+
itemCountBucket: bucketCount(data.workouts.count + data.body_measurements.count),
|
|
2520
2528
|
workflow: workflowResultTelemetry(data.workflow)
|
|
2521
2529
|
})
|
|
2522
2530
|
});
|
|
2523
2531
|
const compactRoutinesResponse = defineStructuredResponseContract({
|
|
2524
2532
|
outputSchema: compactRoutinesOutputSchema,
|
|
2525
|
-
normalize: (data) =>
|
|
2533
|
+
normalize: (data) => {
|
|
2534
|
+
const { workflow, ...result } = data;
|
|
2535
|
+
return {
|
|
2536
|
+
...result,
|
|
2537
|
+
routines: result.routines.map((routine) => ({
|
|
2538
|
+
...routine,
|
|
2539
|
+
...routine.folder_id == null ? { folder_id: void 0 } : {}
|
|
2540
|
+
})),
|
|
2541
|
+
scan: {
|
|
2542
|
+
pages: workflow.pagination,
|
|
2543
|
+
items: workflow.itemsScanned
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
},
|
|
2526
2547
|
legacyJson: ({ routines }) => routines,
|
|
2527
2548
|
text: (_data, { routines }) => routines.length === 0 ? "No routines found matching the query" : void 0,
|
|
2528
2549
|
telemetry: (data) => ({
|
|
@@ -2530,22 +2551,22 @@ const compactRoutinesResponse = defineStructuredResponseContract({
|
|
|
2530
2551
|
workflow: workflowResultTelemetry(data.workflow)
|
|
2531
2552
|
})
|
|
2532
2553
|
});
|
|
2533
|
-
const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json:
|
|
2534
|
-
const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json:
|
|
2554
|
+
const createWorkoutResponse = defineJsonResponseContract((workout) => workout ? { json: projectWorkout(workout) } : { text: "Failed to create workout: Server returned no data" }, (workout) => workoutResultTelemetry(workout));
|
|
2555
|
+
const updateWorkoutResponse = defineJsonResponseContract((data) => data.workout ? { json: projectWorkout(data.workout) } : { text: `Failed to update workout with ID ${data.workout_id}` }, (data) => workoutResultTelemetry(data.workout));
|
|
2535
2556
|
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.";
|
|
2536
2557
|
const createRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
|
|
2537
|
-
json:
|
|
2558
|
+
json: projectRoutine(data.routine),
|
|
2538
2559
|
additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
|
|
2539
2560
|
} : { text: "Failed to create routine: Server returned no data" }, (data) => routineResultTelemetry(data.routine));
|
|
2540
2561
|
const updateRoutineResponse = defineJsonResponseContract((data) => data.routine ? {
|
|
2541
|
-
json:
|
|
2562
|
+
json: projectRoutine(data.routine),
|
|
2542
2563
|
additionalText: data.usesRepRanges ? [repRangeDisplayWarningText] : []
|
|
2543
|
-
} : { text: `Failed to update routine with ID ${data.
|
|
2564
|
+
} : { text: `Failed to update routine with ID ${data.routine_id}` }, (data) => routineResultTelemetry(data.routine));
|
|
2544
2565
|
const createExerciseTemplateResponse = defineJsonResponseContract((response) => ({ json: {
|
|
2545
2566
|
id: response?.id,
|
|
2546
2567
|
message: "Exercise template created successfully"
|
|
2547
2568
|
} }));
|
|
2548
|
-
const createRoutineFolderResponse = defineJsonResponseContract((folder) => folder ? { json:
|
|
2569
|
+
const createRoutineFolderResponse = defineJsonResponseContract((folder) => folder ? { json: projectRoutineFolder(folder) } : { text: "Failed to create routine folder: Server returned no data" }, (folder) => ({ itemCountBucket: bucketCount(folder ? 1 : 0) }));
|
|
2549
2570
|
const createBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} created successfully.` }), () => ({ itemCountBucket: "1" }));
|
|
2550
2571
|
const updateBodyMeasurementResponse = defineJsonResponseContract((date) => ({ text: `Body measurement for ${date} updated successfully.` }), () => ({ itemCountBucket: "1" }));
|
|
2551
2572
|
//#endregion
|
|
@@ -2582,33 +2603,103 @@ function registerHevyResources(server, runtime) {
|
|
|
2582
2603
|
description: "Total number of workouts in the Hevy account",
|
|
2583
2604
|
mimeType: JSON_MIME_TYPE
|
|
2584
2605
|
}, async (uri) => {
|
|
2585
|
-
return createJsonResourceResult(uri, {
|
|
2606
|
+
return createJsonResourceResult(uri, { workout_count: (await runtime.getClient().getWorkoutCount())?.workout_count ?? 0 });
|
|
2586
2607
|
});
|
|
2587
2608
|
server.registerResource("exercise-templates", "hevy://exercise-templates", {
|
|
2588
2609
|
description: "Full formatted Hevy exercise template catalog",
|
|
2589
2610
|
mimeType: JSON_MIME_TYPE
|
|
2590
2611
|
}, async (uri) => {
|
|
2591
|
-
return createJsonResourceResult(uri,
|
|
2612
|
+
return createJsonResourceResult(uri, await runtime.catalog.get());
|
|
2592
2613
|
});
|
|
2593
2614
|
server.registerResource("routine-folders", "hevy://routine-folders", {
|
|
2594
2615
|
description: "Full formatted list of Hevy routine folders",
|
|
2595
2616
|
mimeType: JSON_MIME_TYPE
|
|
2596
2617
|
}, async (uri) => {
|
|
2597
|
-
return createJsonResourceResult(uri, (await fetchAllRoutineFolders(runtime)).map(
|
|
2618
|
+
return createJsonResourceResult(uri, (await fetchAllRoutineFolders(runtime)).map(projectRoutineFolder));
|
|
2598
2619
|
});
|
|
2599
2620
|
}
|
|
2600
2621
|
//#endregion
|
|
2601
2622
|
//#region ../core/src/server-metadata.ts
|
|
2602
2623
|
const SERVER_NAME = "hevy-mcp";
|
|
2603
|
-
const SERVER_VERSION = "
|
|
2624
|
+
const SERVER_VERSION = "5.0.0";
|
|
2604
2625
|
const SERVER_INSTRUCTIONS = [
|
|
2605
2626
|
"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.",
|
|
2606
2627
|
"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.",
|
|
2607
2628
|
"Workflow: search exercise templates first, then use the returned template IDs when creating workouts or routines. To create a completed workout from a routine, fetch the routine as a plan, then obtain the actual completed sets and end time from the user; never invent completion data.",
|
|
2608
|
-
"Pagination: start at page 1 and fetch only the pages needed. Most list tools allow
|
|
2629
|
+
"Pagination: start at page 1 and fetch only the pages needed. Most list tools allow page_size up to 10; get-exercise-templates allows page_size up to 100.",
|
|
2609
2630
|
"Rate limits and retries: minimize repeated calls. If Hevy returns HTTP 429, follow its retry guidance. Transient read requests retry automatically, but write requests do not; confirm uncertain write outcomes before trying again."
|
|
2610
2631
|
].join("\n\n");
|
|
2611
2632
|
//#endregion
|
|
2633
|
+
//#region ../core/src/utils/compact-json-schema.ts
|
|
2634
|
+
function isRecord$1(value) {
|
|
2635
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2636
|
+
}
|
|
2637
|
+
function isNullSchema(value) {
|
|
2638
|
+
return isRecord$1(value) && value.type === "null" && Object.keys(value).length === 1;
|
|
2639
|
+
}
|
|
2640
|
+
function compactNullableSchema(value) {
|
|
2641
|
+
if (Array.isArray(value)) return value.map(compactNullableSchema);
|
|
2642
|
+
if (!isRecord$1(value)) return value;
|
|
2643
|
+
const anyOf = value.anyOf;
|
|
2644
|
+
if (Array.isArray(anyOf) && anyOf.length === 2) {
|
|
2645
|
+
const nullableBranch = anyOf.find(isNullSchema);
|
|
2646
|
+
const valueBranch = anyOf.find((branch) => !isNullSchema(branch));
|
|
2647
|
+
if (nullableBranch !== void 0 && isRecord$1(valueBranch) && typeof valueBranch.type === "string") {
|
|
2648
|
+
const { anyOf: _anyOf, ...metadata } = value;
|
|
2649
|
+
return compactNullableSchema({
|
|
2650
|
+
...valueBranch,
|
|
2651
|
+
...metadata,
|
|
2652
|
+
type: [valueBranch.type, "null"]
|
|
2653
|
+
});
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
return Object.fromEntries(Object.entries(value).map(([key, nested]) => [key, compactNullableSchema(nested)]));
|
|
2657
|
+
}
|
|
2658
|
+
function omitOutputObjectRestrictions(value) {
|
|
2659
|
+
if (Array.isArray(value)) return value.map(omitOutputObjectRestrictions);
|
|
2660
|
+
if (!isRecord$1(value)) return value;
|
|
2661
|
+
return Object.fromEntries(Object.entries(value).filter(([key, nested]) => key !== "additionalProperties" || nested !== false).map(([key, nested]) => [key, omitOutputObjectRestrictions(nested)]));
|
|
2662
|
+
}
|
|
2663
|
+
function convertSchema(schema, io) {
|
|
2664
|
+
const converted = z.toJSONSchema(schema, {
|
|
2665
|
+
target: "draft-2020-12",
|
|
2666
|
+
io
|
|
2667
|
+
});
|
|
2668
|
+
if (!isRecord$1(converted)) throw new Error("Zod emitted a non-object root schema");
|
|
2669
|
+
const compacted = compactNullableSchema(converted);
|
|
2670
|
+
if (!isRecord$1(compacted)) throw new Error("Nullable schema compaction changed the root schema");
|
|
2671
|
+
const output = io === "output" ? omitOutputObjectRestrictions(compacted) : compacted;
|
|
2672
|
+
if (!isRecord$1(output)) throw new Error("Output schema compaction changed the root schema");
|
|
2673
|
+
delete output.$schema;
|
|
2674
|
+
return output;
|
|
2675
|
+
}
|
|
2676
|
+
/**
|
|
2677
|
+
* Keep Zod validation while advertising a compact JSON Schema.
|
|
2678
|
+
*
|
|
2679
|
+
* Zod emits nullable values as `anyOf`; draft 2020-12 also permits a type
|
|
2680
|
+
* array. The MCP wire envelope does not need a repeated `$schema` marker, and
|
|
2681
|
+
* structured output is validated by the original Zod schema before sending.
|
|
2682
|
+
*/
|
|
2683
|
+
function compactJsonSchema(schema) {
|
|
2684
|
+
let inputSchema;
|
|
2685
|
+
let outputSchema;
|
|
2686
|
+
Object.defineProperty(schema["~standard"], "jsonSchema", {
|
|
2687
|
+
configurable: true,
|
|
2688
|
+
value: {
|
|
2689
|
+
input: () => {
|
|
2690
|
+
inputSchema ??= convertSchema(schema, "input");
|
|
2691
|
+
return inputSchema;
|
|
2692
|
+
},
|
|
2693
|
+
output: () => {
|
|
2694
|
+
outputSchema ??= convertSchema(schema, "output");
|
|
2695
|
+
return outputSchema;
|
|
2696
|
+
}
|
|
2697
|
+
},
|
|
2698
|
+
writable: true
|
|
2699
|
+
});
|
|
2700
|
+
return schema;
|
|
2701
|
+
}
|
|
2702
|
+
//#endregion
|
|
2612
2703
|
//#region ../core/src/utils/tool-helpers.ts
|
|
2613
2704
|
/**
|
|
2614
2705
|
* Type-safe utilities for MCP tool handlers
|
|
@@ -2646,7 +2737,7 @@ function requireClient(client) {
|
|
|
2646
2737
|
* ```
|
|
2647
2738
|
*/
|
|
2648
2739
|
function createTypedToolHandler(schema, handler) {
|
|
2649
|
-
const zodSchema = z.
|
|
2740
|
+
const zodSchema = z.strictObject(schema);
|
|
2650
2741
|
return async (args) => {
|
|
2651
2742
|
return handler(zodSchema.parse(args));
|
|
2652
2743
|
};
|
|
@@ -2660,16 +2751,14 @@ function registerToolDefinition(server, runtime, definition) {
|
|
|
2660
2751
|
kind: definition.kind,
|
|
2661
2752
|
operation: definition.operation
|
|
2662
2753
|
});
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
}
|
|
2672
|
-
server.tool(definition.name, definition.description, definition.inputSchema, definition.annotations, callback);
|
|
2754
|
+
const inputSchema = compactJsonSchema(z.strictObject(definition.inputSchema));
|
|
2755
|
+
const config = {
|
|
2756
|
+
description: definition.description,
|
|
2757
|
+
inputSchema,
|
|
2758
|
+
annotations: definition.annotations,
|
|
2759
|
+
...definition.kind === "read" ? { outputSchema: compactJsonSchema(z.object(definition.outputSchema)) } : {}
|
|
2760
|
+
};
|
|
2761
|
+
server.registerTool(definition.name, config, callback);
|
|
2673
2762
|
}
|
|
2674
2763
|
//#endregion
|
|
2675
2764
|
//#region ../core/src/utils/tool-annotations.ts
|
|
@@ -2705,16 +2794,6 @@ function updateAnnotations(title) {
|
|
|
2705
2794
|
};
|
|
2706
2795
|
}
|
|
2707
2796
|
//#endregion
|
|
2708
|
-
//#region ../core/src/utils/tool-descriptions.ts
|
|
2709
|
-
function describeTool({ summary, aliases, useCase, importantNotes }) {
|
|
2710
|
-
return [
|
|
2711
|
-
summary,
|
|
2712
|
-
`Aliases: ${aliases.join(", ")}.`,
|
|
2713
|
-
`<use_case>${useCase}</use_case>`,
|
|
2714
|
-
`<important_notes>${importantNotes}</important_notes>`
|
|
2715
|
-
].join(" ");
|
|
2716
|
-
}
|
|
2717
|
-
//#endregion
|
|
2718
2797
|
//#region ../core/src/utils/json-parser.ts
|
|
2719
2798
|
/**
|
|
2720
2799
|
* Preprocessor to handle MCP clients that send JSON-stringified arrays
|
|
@@ -2736,143 +2815,156 @@ function parseJsonArray(val) {
|
|
|
2736
2815
|
}
|
|
2737
2816
|
//#endregion
|
|
2738
2817
|
//#region ../core/src/tools/input-schemas.ts
|
|
2739
|
-
/** Build the page and
|
|
2818
|
+
/** Build the page and page_size fields shared by paginated tools. */
|
|
2740
2819
|
function paginationShape({ defaultPageSize, maxPageSize, integerPage = true }) {
|
|
2741
2820
|
const pageNumber = z.coerce.number().gte(1);
|
|
2742
2821
|
return {
|
|
2743
2822
|
page: integerPage ? pageNumber.int() : pageNumber,
|
|
2744
|
-
|
|
2823
|
+
page_size: z.coerce.number().int().gte(1).lte(maxPageSize).default(defaultPageSize)
|
|
2745
2824
|
};
|
|
2746
2825
|
}
|
|
2747
2826
|
const nonEmptyId = z.string().min(1);
|
|
2748
|
-
const
|
|
2827
|
+
const exerciseTemplatePayloadShape = {
|
|
2749
2828
|
title: z.string().min(1),
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2829
|
+
exercise_type: exerciseTypeEnum,
|
|
2830
|
+
equipment_category: equipmentCategoryEnum,
|
|
2831
|
+
muscle_group: muscleGroupEnum,
|
|
2832
|
+
other_muscles: z.array(muscleGroupEnum).default([])
|
|
2754
2833
|
};
|
|
2755
|
-
const
|
|
2834
|
+
const exerciseTemplateInputShape = z.strictObject({ exercise: z.strictObject(exerciseTemplatePayloadShape) }).shape;
|
|
2835
|
+
const routineFolderPayloadShape = { title: z.string().min(1) };
|
|
2836
|
+
const routineFolderInputShape = z.strictObject({ routine_folder: z.strictObject(routineFolderPayloadShape) }).shape;
|
|
2756
2837
|
const CALENDAR_DATE_MESSAGE = "Date must be in YYYY-MM-DD format";
|
|
2757
2838
|
const calendarDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, CALENDAR_DATE_MESSAGE).refine((value) => {
|
|
2758
2839
|
const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
|
|
2759
2840
|
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().startsWith(value);
|
|
2760
2841
|
}, CALENDAR_DATE_MESSAGE);
|
|
2842
|
+
const rpeEnum = z.union([
|
|
2843
|
+
z.literal(6),
|
|
2844
|
+
z.literal(7),
|
|
2845
|
+
z.literal(7.5),
|
|
2846
|
+
z.literal(8),
|
|
2847
|
+
z.literal(8.5),
|
|
2848
|
+
z.literal(9),
|
|
2849
|
+
z.literal(9.5),
|
|
2850
|
+
z.literal(10)
|
|
2851
|
+
]);
|
|
2761
2852
|
const workoutSetShape = {
|
|
2762
2853
|
type: setTypeEnum,
|
|
2763
|
-
|
|
2764
|
-
weightKg: z.coerce.number().optional().nullable(),
|
|
2854
|
+
weight_kg: z.coerce.number().optional().nullable(),
|
|
2765
2855
|
reps: z.coerce.number().int().optional().nullable(),
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
rpe: z.coerce.number().optional().nullable(),
|
|
2771
|
-
customMetric: z.coerce.number().optional().nullable()
|
|
2856
|
+
distance_meters: z.coerce.number().int().optional().nullable(),
|
|
2857
|
+
duration_seconds: z.coerce.number().int().optional().nullable(),
|
|
2858
|
+
rpe: rpeEnum.optional().nullable(),
|
|
2859
|
+
custom_metric: z.coerce.number().optional().nullable()
|
|
2772
2860
|
};
|
|
2861
|
+
const workoutSetSchema = z.strictObject(workoutSetShape);
|
|
2773
2862
|
const workoutExerciseShape = {
|
|
2774
|
-
|
|
2775
|
-
|
|
2863
|
+
exercise_template_id: nonEmptyId,
|
|
2864
|
+
superset_id: z.coerce.number().nullable().optional(),
|
|
2776
2865
|
notes: z.string().optional().nullable(),
|
|
2777
|
-
sets: z.array(
|
|
2866
|
+
sets: z.array(workoutSetSchema)
|
|
2778
2867
|
};
|
|
2779
|
-
const
|
|
2868
|
+
const workoutExerciseSchema = z.strictObject(workoutExerciseShape);
|
|
2869
|
+
const workoutExercisesSchema = z.preprocess(parseJsonArray, z.array(workoutExerciseSchema));
|
|
2870
|
+
const replaceWorkoutPayloadShape = {
|
|
2780
2871
|
title: z.string().min(1),
|
|
2781
2872
|
description: z.string().optional().nullable(),
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
exercises:
|
|
2873
|
+
start_time: utcSecondTimestamp,
|
|
2874
|
+
end_time: utcSecondTimestamp,
|
|
2875
|
+
is_private: z.boolean().default(false),
|
|
2876
|
+
exercises: workoutExercisesSchema
|
|
2786
2877
|
};
|
|
2878
|
+
const replaceWorkoutPayloadSchema = z.strictObject(replaceWorkoutPayloadShape);
|
|
2879
|
+
const workoutInputShape = z.strictObject({ workout: replaceWorkoutPayloadSchema }).shape;
|
|
2880
|
+
z.strictObject({
|
|
2881
|
+
workout_id: nonEmptyId,
|
|
2882
|
+
workout: replaceWorkoutPayloadSchema
|
|
2883
|
+
}).shape;
|
|
2884
|
+
const workoutMetadataPatchSchema = z.strictObject({
|
|
2885
|
+
title: z.string().min(1).optional(),
|
|
2886
|
+
description: z.string().nullable().optional(),
|
|
2887
|
+
start_time: utcSecondTimestamp.optional(),
|
|
2888
|
+
end_time: utcSecondTimestamp.optional(),
|
|
2889
|
+
is_private: z.boolean().optional()
|
|
2890
|
+
}).refine((patch) => Object.values(patch).some((value) => value !== void 0), "Include at least one workout metadata field").meta({ minProperties: 1 });
|
|
2891
|
+
const updateWorkoutInputShape = z.strictObject({
|
|
2892
|
+
workout_id: nonEmptyId,
|
|
2893
|
+
workout: workoutMetadataPatchSchema
|
|
2894
|
+
}).shape;
|
|
2895
|
+
const replaceWorkoutExercisesInputShape = z.strictObject({
|
|
2896
|
+
workout_id: nonEmptyId,
|
|
2897
|
+
workout: z.strictObject({ exercises: workoutExercisesSchema })
|
|
2898
|
+
}).shape;
|
|
2787
2899
|
const routineSetShape = {
|
|
2788
2900
|
type: setTypeEnum,
|
|
2789
|
-
|
|
2790
|
-
weightKg: z.coerce.number().optional(),
|
|
2901
|
+
weight_kg: z.coerce.number().optional(),
|
|
2791
2902
|
reps: zNullableInt,
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
customMetric: z.coerce.number().optional(),
|
|
2797
|
-
repRange: zOptionalRepRange
|
|
2903
|
+
distance_meters: z.coerce.number().int().optional(),
|
|
2904
|
+
duration_seconds: z.coerce.number().int().optional(),
|
|
2905
|
+
rep_range: zStrictOptionalRepRange,
|
|
2906
|
+
custom_metric: z.coerce.number().optional()
|
|
2798
2907
|
};
|
|
2908
|
+
const routineSetSchema = z.strictObject(routineSetShape);
|
|
2799
2909
|
const routineExerciseShape = {
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2910
|
+
exercise_template_id: nonEmptyId,
|
|
2911
|
+
superset_id: z.coerce.number().nullable().optional(),
|
|
2912
|
+
rest_seconds: z.coerce.number().int().min(0).optional(),
|
|
2803
2913
|
notes: z.string().optional(),
|
|
2804
|
-
sets: z.array(
|
|
2914
|
+
sets: z.array(routineSetSchema)
|
|
2805
2915
|
};
|
|
2916
|
+
const routineExerciseSchema = z.strictObject(routineExerciseShape);
|
|
2917
|
+
const routineExercisesSchema = z.preprocess(parseJsonArray, z.array(routineExerciseSchema));
|
|
2806
2918
|
const routinePayloadShape = {
|
|
2807
2919
|
title: z.string().min(1),
|
|
2808
|
-
|
|
2920
|
+
folder_id: z.coerce.number().nullable().optional(),
|
|
2921
|
+
notes: z.string().optional(),
|
|
2922
|
+
exercises: routineExercisesSchema
|
|
2923
|
+
};
|
|
2924
|
+
const routinePayloadSchema = z.strictObject(routinePayloadShape);
|
|
2925
|
+
const createRoutineInputShape = z.strictObject({ routine: routinePayloadSchema }).shape;
|
|
2926
|
+
const routineUpdatePayloadShape = {
|
|
2927
|
+
title: z.string().min(1),
|
|
2809
2928
|
notes: z.string().optional(),
|
|
2810
|
-
exercises:
|
|
2929
|
+
exercises: routineExercisesSchema
|
|
2811
2930
|
};
|
|
2931
|
+
const routineUpdatePayloadSchema = z.strictObject(routineUpdatePayloadShape);
|
|
2932
|
+
const updateRoutineInputShape = z.strictObject({
|
|
2933
|
+
routine_id: nonEmptyId,
|
|
2934
|
+
routine: routineUpdatePayloadSchema
|
|
2935
|
+
}).shape;
|
|
2812
2936
|
const bodyMeasurementFieldsSchema = {
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
abdomen: zNullableNumber.describe("
|
|
2824
|
-
waist: zNullableNumber.describe("
|
|
2825
|
-
hips: zNullableNumber.describe("
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
};
|
|
2831
|
-
const measurementFieldToApiKey = {
|
|
2832
|
-
weightKg: "weight_kg",
|
|
2833
|
-
leanMassKg: "lean_mass_kg",
|
|
2834
|
-
fatPercent: "fat_percent",
|
|
2835
|
-
neckCm: "neck_cm",
|
|
2836
|
-
shoulderCm: "shoulder_cm",
|
|
2837
|
-
chestCm: "chest_cm",
|
|
2838
|
-
leftBicepCm: "left_bicep_cm",
|
|
2839
|
-
rightBicepCm: "right_bicep_cm",
|
|
2840
|
-
leftForearmCm: "left_forearm_cm",
|
|
2841
|
-
rightForearmCm: "right_forearm_cm",
|
|
2842
|
-
abdomen: "abdomen",
|
|
2843
|
-
waist: "waist",
|
|
2844
|
-
hips: "hips",
|
|
2845
|
-
leftThigh: "left_thigh",
|
|
2846
|
-
rightThigh: "right_thigh",
|
|
2847
|
-
leftCalf: "left_calf",
|
|
2848
|
-
rightCalf: "right_calf"
|
|
2937
|
+
weight_kg: zNullableNumber,
|
|
2938
|
+
lean_mass_kg: zNullableNumber,
|
|
2939
|
+
fat_percent: zNullableNumber,
|
|
2940
|
+
neck_cm: zNullableNumber,
|
|
2941
|
+
shoulder_cm: zNullableNumber,
|
|
2942
|
+
chest_cm: zNullableNumber,
|
|
2943
|
+
left_bicep_cm: zNullableNumber,
|
|
2944
|
+
right_bicep_cm: zNullableNumber,
|
|
2945
|
+
left_forearm_cm: zNullableNumber,
|
|
2946
|
+
right_forearm_cm: zNullableNumber,
|
|
2947
|
+
abdomen: zNullableNumber.describe("Circumference in centimeters."),
|
|
2948
|
+
waist: zNullableNumber.describe("Circumference in centimeters."),
|
|
2949
|
+
hips: zNullableNumber.describe("Circumference in centimeters."),
|
|
2950
|
+
left_thigh: zNullableNumber,
|
|
2951
|
+
right_thigh: zNullableNumber,
|
|
2952
|
+
left_calf: zNullableNumber,
|
|
2953
|
+
right_calf: zNullableNumber
|
|
2849
2954
|
};
|
|
2955
|
+
z.strictObject(bodyMeasurementFieldsSchema);
|
|
2956
|
+
const createBodyMeasurementInputSchema = z.strictObject({
|
|
2957
|
+
date: calendarDate,
|
|
2958
|
+
...bodyMeasurementFieldsSchema
|
|
2959
|
+
});
|
|
2960
|
+
const updateBodyMeasurementInputSchema = z.strictObject({
|
|
2961
|
+
date: calendarDate,
|
|
2962
|
+
...bodyMeasurementFieldsSchema
|
|
2963
|
+
});
|
|
2964
|
+
const createBodyMeasurementInputShape = createBodyMeasurementInputSchema.shape;
|
|
2965
|
+
const updateBodyMeasurementInputShape = updateBodyMeasurementInputSchema.shape;
|
|
2850
2966
|
//#endregion
|
|
2851
|
-
//#region ../core/src/tools/
|
|
2852
|
-
/** Map the public camelCase workout input to the API's snake_case payload. */
|
|
2853
|
-
function buildWorkoutPayload(input) {
|
|
2854
|
-
return {
|
|
2855
|
-
title: input.title,
|
|
2856
|
-
description: input.description ?? null,
|
|
2857
|
-
start_time: input.startTime,
|
|
2858
|
-
end_time: input.endTime,
|
|
2859
|
-
is_private: input.isPrivate,
|
|
2860
|
-
exercises: input.exercises.map((exercise) => ({
|
|
2861
|
-
exercise_template_id: exercise.exerciseTemplateId,
|
|
2862
|
-
superset_id: exercise.supersetId ?? null,
|
|
2863
|
-
notes: exercise.notes ?? null,
|
|
2864
|
-
sets: exercise.sets.map((set) => ({
|
|
2865
|
-
type: set.type,
|
|
2866
|
-
weight_kg: set.weight ?? set.weightKg ?? null,
|
|
2867
|
-
reps: set.reps ?? null,
|
|
2868
|
-
distance_meters: set.distance ?? set.distanceMeters ?? null,
|
|
2869
|
-
duration_seconds: set.duration ?? set.durationSeconds ?? null,
|
|
2870
|
-
rpe: set.rpe ?? null,
|
|
2871
|
-
custom_metric: set.customMetric ?? null
|
|
2872
|
-
}))
|
|
2873
|
-
}))
|
|
2874
|
-
};
|
|
2875
|
-
}
|
|
2967
|
+
//#region ../core/src/tools/mutation-semantics.ts
|
|
2876
2968
|
function buildRepRange(repRange) {
|
|
2877
2969
|
if (!repRange) return null;
|
|
2878
2970
|
const start = repRange.start ?? void 0;
|
|
@@ -2892,14 +2984,14 @@ function getFixedRepsFromRepRange(repRange) {
|
|
|
2892
2984
|
}
|
|
2893
2985
|
function buildRoutineSets(sets, mode) {
|
|
2894
2986
|
return sets.map((set) => {
|
|
2895
|
-
const repRange = buildRepRange(set.
|
|
2987
|
+
const repRange = buildRepRange(set.rep_range);
|
|
2896
2988
|
const reps = typeof set.reps === "number" ? set.reps : getFixedRepsFromRepRange(repRange);
|
|
2897
2989
|
const common = {
|
|
2898
|
-
weight_kg: set.
|
|
2990
|
+
weight_kg: set.weight_kg ?? null,
|
|
2899
2991
|
reps: reps ?? null,
|
|
2900
|
-
distance_meters: set.
|
|
2901
|
-
duration_seconds: set.
|
|
2902
|
-
custom_metric: set.
|
|
2992
|
+
distance_meters: set.distance_meters ?? null,
|
|
2993
|
+
duration_seconds: set.duration_seconds ?? null,
|
|
2994
|
+
custom_metric: set.custom_metric ?? null
|
|
2903
2995
|
};
|
|
2904
2996
|
if (mode === "create") return {
|
|
2905
2997
|
...common,
|
|
@@ -2919,9 +3011,9 @@ function buildRoutinePayload(input, mode) {
|
|
|
2919
3011
|
const sets = buildRoutineSets(exercise.sets, mode);
|
|
2920
3012
|
if (sets.some((set) => set.rep_range != null && getFixedRepsFromRepRange(set.rep_range) === null)) usesRepRanges = true;
|
|
2921
3013
|
return {
|
|
2922
|
-
exercise_template_id: exercise.
|
|
2923
|
-
superset_id: exercise.
|
|
2924
|
-
rest_seconds: exercise.
|
|
3014
|
+
exercise_template_id: exercise.exercise_template_id,
|
|
3015
|
+
superset_id: exercise.superset_id ?? null,
|
|
3016
|
+
rest_seconds: exercise.rest_seconds ?? null,
|
|
2925
3017
|
notes: exercise.notes ?? null,
|
|
2926
3018
|
sets
|
|
2927
3019
|
};
|
|
@@ -2929,7 +3021,7 @@ function buildRoutinePayload(input, mode) {
|
|
|
2929
3021
|
if (mode === "create") return {
|
|
2930
3022
|
payload: {
|
|
2931
3023
|
title: input.title,
|
|
2932
|
-
folder_id: input.
|
|
3024
|
+
folder_id: input.folder_id ?? null,
|
|
2933
3025
|
notes: input.notes ?? "",
|
|
2934
3026
|
exercises
|
|
2935
3027
|
},
|
|
@@ -2944,27 +3036,71 @@ function buildRoutinePayload(input, mode) {
|
|
|
2944
3036
|
usesRepRanges
|
|
2945
3037
|
};
|
|
2946
3038
|
}
|
|
3039
|
+
const workoutUpdateMetadataSchema = z.object({
|
|
3040
|
+
title: z.string().min(1),
|
|
3041
|
+
start_time: utcSecondTimestamp,
|
|
3042
|
+
end_time: utcSecondTimestamp
|
|
3043
|
+
});
|
|
3044
|
+
/**
|
|
3045
|
+
* Map fetched API exercises to the update shape without validating legacy data.
|
|
3046
|
+
* Caller-supplied replacement exercises are still validated by their input schema.
|
|
3047
|
+
*/
|
|
3048
|
+
function preserveWorkoutExercises(current) {
|
|
3049
|
+
return current.exercises?.map((exercise) => ({
|
|
3050
|
+
exercise_template_id: exercise.exercise_template_id,
|
|
3051
|
+
superset_id: exercise.supersets_id ?? null,
|
|
3052
|
+
notes: exercise.notes ?? null,
|
|
3053
|
+
sets: exercise.sets?.map((set) => ({
|
|
3054
|
+
...set.type === void 0 ? {} : { type: set.type },
|
|
3055
|
+
weight_kg: set.weight_kg ?? null,
|
|
3056
|
+
reps: set.reps ?? null,
|
|
3057
|
+
distance_meters: set.distance_meters ?? null,
|
|
3058
|
+
duration_seconds: set.duration_seconds ?? null,
|
|
3059
|
+
rpe: set.rpe,
|
|
3060
|
+
custom_metric: set.custom_metric ?? null
|
|
3061
|
+
})) ?? []
|
|
3062
|
+
})) ?? [];
|
|
3063
|
+
}
|
|
3064
|
+
function buildWorkoutUpdatePayload(current, patch, replacementExercises) {
|
|
3065
|
+
return {
|
|
3066
|
+
...workoutUpdateMetadataSchema.parse({
|
|
3067
|
+
title: patch.title !== void 0 ? patch.title : current.title,
|
|
3068
|
+
start_time: patch.start_time !== void 0 ? patch.start_time : current.start_time,
|
|
3069
|
+
end_time: patch.end_time !== void 0 ? patch.end_time : current.end_time
|
|
3070
|
+
}),
|
|
3071
|
+
description: patch.description !== void 0 ? patch.description : current.description ?? null,
|
|
3072
|
+
exercises: replacementExercises === void 0 ? preserveWorkoutExercises(current) : replacementExercises,
|
|
3073
|
+
...patch.is_private !== void 0 ? { is_private: patch.is_private } : {}
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
3076
|
+
const measurementKeys = [
|
|
3077
|
+
"weight_kg",
|
|
3078
|
+
"lean_mass_kg",
|
|
3079
|
+
"fat_percent",
|
|
3080
|
+
"neck_cm",
|
|
3081
|
+
"shoulder_cm",
|
|
3082
|
+
"chest_cm",
|
|
3083
|
+
"left_bicep_cm",
|
|
3084
|
+
"right_bicep_cm",
|
|
3085
|
+
"left_forearm_cm",
|
|
3086
|
+
"right_forearm_cm",
|
|
3087
|
+
"abdomen",
|
|
3088
|
+
"waist",
|
|
3089
|
+
"hips",
|
|
3090
|
+
"left_thigh",
|
|
3091
|
+
"right_thigh",
|
|
3092
|
+
"left_calf",
|
|
3093
|
+
"right_calf"
|
|
3094
|
+
];
|
|
2947
3095
|
/** Omit nullish measurement values because the API rejects explicit nulls. */
|
|
2948
3096
|
function buildMeasurementPayload(fields) {
|
|
2949
3097
|
const payload = {};
|
|
2950
|
-
for (const
|
|
2951
|
-
const value = fields[
|
|
2952
|
-
if (value != null) payload[
|
|
3098
|
+
for (const key of measurementKeys) {
|
|
3099
|
+
const value = fields[key];
|
|
3100
|
+
if (value != null) payload[key] = value;
|
|
2953
3101
|
}
|
|
2954
3102
|
return payload;
|
|
2955
3103
|
}
|
|
2956
|
-
function buildExerciseTemplateRequest(input) {
|
|
2957
|
-
return { exercise: {
|
|
2958
|
-
title: input.title,
|
|
2959
|
-
exercise_type: input.exerciseType,
|
|
2960
|
-
equipment_category: input.equipmentCategory,
|
|
2961
|
-
muscle_group: input.muscleGroup,
|
|
2962
|
-
other_muscles: input.otherMuscles
|
|
2963
|
-
} };
|
|
2964
|
-
}
|
|
2965
|
-
function buildRoutineFolderRequest(input) {
|
|
2966
|
-
return { routine_folder: { title: input.name } };
|
|
2967
|
-
}
|
|
2968
3104
|
//#endregion
|
|
2969
3105
|
//#region ../core/src/utils/hevy-error-policy.ts
|
|
2970
3106
|
const READ_RESOURCE_ENDPOINTS = /* @__PURE__ */ new Set([
|
|
@@ -2996,41 +3132,26 @@ const getBodyMeasurementsSchema = { ...paginationShape({
|
|
|
2996
3132
|
defaultPageSize: 10,
|
|
2997
3133
|
maxPageSize: 10
|
|
2998
3134
|
}) };
|
|
2999
|
-
const getBodyMeasurementSchema = { date: calendarDate
|
|
3000
|
-
const createBodyMeasurementSchema =
|
|
3001
|
-
|
|
3002
|
-
...bodyMeasurementFieldsSchema
|
|
3003
|
-
};
|
|
3004
|
-
const updateBodyMeasurementSchema = {
|
|
3005
|
-
date: calendarDate.describe("The date of the body measurement to update (YYYY-MM-DD). Must already exist — returns 404 otherwise."),
|
|
3006
|
-
...bodyMeasurementFieldsSchema
|
|
3007
|
-
};
|
|
3135
|
+
const getBodyMeasurementSchema = { date: calendarDate };
|
|
3136
|
+
const createBodyMeasurementSchema = createBodyMeasurementInputShape;
|
|
3137
|
+
const updateBodyMeasurementSchema = updateBodyMeasurementInputShape;
|
|
3008
3138
|
const bodyMeasurementToolDefinitions = [
|
|
3009
3139
|
{
|
|
3010
3140
|
name: "get-body-measurements",
|
|
3011
3141
|
feature: "measurements",
|
|
3012
3142
|
operation: "list",
|
|
3013
|
-
description:
|
|
3014
|
-
summary: "Read-only. Lists dated body measurements for the account.",
|
|
3015
|
-
aliases: [
|
|
3016
|
-
"body stats history",
|
|
3017
|
-
"list weigh-ins",
|
|
3018
|
-
"measurement log"
|
|
3019
|
-
],
|
|
3020
|
-
useCase: "Use to browse measurement history; use get-body-measurement for one exact date.",
|
|
3021
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
|
|
3022
|
-
}),
|
|
3143
|
+
description: "Read-only. Lists dated body measurements; results are paginated. Use get-body-measurement for one date.",
|
|
3023
3144
|
inputSchema: getBodyMeasurementsSchema,
|
|
3024
3145
|
outputSchema: bodyMeasurementsResponse.outputSchema,
|
|
3025
3146
|
annotations: readOnlyAnnotations("Get Body Measurements"),
|
|
3026
3147
|
kind: "read",
|
|
3027
3148
|
responseContract: bodyMeasurementsResponse,
|
|
3028
3149
|
execute: async (runtime, args) => {
|
|
3029
|
-
const { page,
|
|
3150
|
+
const { page, page_size } = args;
|
|
3030
3151
|
try {
|
|
3031
3152
|
const data = await runtime.getClient().getBodyMeasurements({
|
|
3032
3153
|
page,
|
|
3033
|
-
pageSize
|
|
3154
|
+
pageSize: page_size
|
|
3034
3155
|
});
|
|
3035
3156
|
return {
|
|
3036
3157
|
items: data?.body_measurements ?? [],
|
|
@@ -3051,16 +3172,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3051
3172
|
name: "get-body-measurement",
|
|
3052
3173
|
feature: "measurements",
|
|
3053
3174
|
operation: "get",
|
|
3054
|
-
description:
|
|
3055
|
-
summary: "Read-only. Retrieves the body measurement entry for one date.",
|
|
3056
|
-
aliases: [
|
|
3057
|
-
"get weigh-in",
|
|
3058
|
-
"show body stats",
|
|
3059
|
-
"measurement by date"
|
|
3060
|
-
],
|
|
3061
|
-
useCase: "Use when the exact measurement date is known; use get-body-measurements to browse dates.",
|
|
3062
|
-
importantNotes: "date must use YYYY-MM-DD; at most one entry exists per date."
|
|
3063
|
-
}),
|
|
3175
|
+
description: "Read-only. Gets the body measurement for one YYYY-MM-DD date. Use get-body-measurements to browse dates.",
|
|
3064
3176
|
inputSchema: getBodyMeasurementSchema,
|
|
3065
3177
|
outputSchema: bodyMeasurementResponse.outputSchema,
|
|
3066
3178
|
annotations: readOnlyAnnotations("Get Body Measurement"),
|
|
@@ -3070,12 +3182,12 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3070
3182
|
const { date } = args;
|
|
3071
3183
|
try {
|
|
3072
3184
|
return {
|
|
3073
|
-
|
|
3185
|
+
body_measurement: await runtime.getClient().getBodyMeasurement(date),
|
|
3074
3186
|
date
|
|
3075
3187
|
};
|
|
3076
3188
|
} catch (error) {
|
|
3077
3189
|
if (isExpectedReadNotFound(error)) return {
|
|
3078
|
-
|
|
3190
|
+
body_measurement: null,
|
|
3079
3191
|
date,
|
|
3080
3192
|
expected404Outcome: "not_found"
|
|
3081
3193
|
};
|
|
@@ -3087,16 +3199,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3087
3199
|
name: "create-body-measurement",
|
|
3088
3200
|
feature: "measurements",
|
|
3089
3201
|
operation: "create",
|
|
3090
|
-
description:
|
|
3091
|
-
summary: "Writes to the Hevy account by creating a dated body measurement.",
|
|
3092
|
-
aliases: [
|
|
3093
|
-
"log weigh-in",
|
|
3094
|
-
"add body stats",
|
|
3095
|
-
"record measurements"
|
|
3096
|
-
],
|
|
3097
|
-
useCase: "Use for a date without an entry; use update-body-measurement when that date already exists.",
|
|
3098
|
-
importantNotes: "date must use YYYY-MM-DD and be unique. Null fields are omitted and cannot clear values; an existing date returns 409."
|
|
3099
|
-
}),
|
|
3202
|
+
description: "Writes a body measurement for a new YYYY-MM-DD date. Existing dates conflict; retries are not idempotent.",
|
|
3100
3203
|
inputSchema: createBodyMeasurementSchema,
|
|
3101
3204
|
annotations: createAnnotations("Create Body Measurement"),
|
|
3102
3205
|
kind: "write",
|
|
@@ -3114,16 +3217,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3114
3217
|
name: "update-body-measurement",
|
|
3115
3218
|
feature: "measurements",
|
|
3116
3219
|
operation: "update",
|
|
3117
|
-
description:
|
|
3118
|
-
summary: "Mutates the Hevy account by updating a body measurement for a date.",
|
|
3119
|
-
aliases: [
|
|
3120
|
-
"edit weigh-in",
|
|
3121
|
-
"correct body stats",
|
|
3122
|
-
"change measurements"
|
|
3123
|
-
],
|
|
3124
|
-
useCase: "Use to change fields on an existing date; use create-body-measurement for a new date.",
|
|
3125
|
-
importantNotes: "date must use YYYY-MM-DD and already exist. Provide at least one numeric field; nulls are omitted and cannot clear stored values."
|
|
3126
|
-
}),
|
|
3220
|
+
description: "Mutates numeric fields on an existing YYYY-MM-DD measurement. Omitted fields remain unchanged; values cannot be cleared.",
|
|
3127
3221
|
inputSchema: updateBodyMeasurementSchema,
|
|
3128
3222
|
annotations: updateAnnotations("Update Body Measurement"),
|
|
3129
3223
|
kind: "write",
|
|
@@ -3131,7 +3225,7 @@ const bodyMeasurementToolDefinitions = [
|
|
|
3131
3225
|
execute: async (runtime, args) => {
|
|
3132
3226
|
const { date, ...fields } = args;
|
|
3133
3227
|
const payload = buildMeasurementPayload(fields);
|
|
3134
|
-
if (Object.keys(payload).length === 0) throw new Error("No measurement fields provided. Include at least one numeric measurement field (e.g.
|
|
3228
|
+
if (Object.keys(payload).length === 0) throw new Error("No measurement fields provided. Include at least one numeric measurement field (e.g. weight_kg) to update.");
|
|
3135
3229
|
await runtime.getClient().updateBodyMeasurement(date, payload);
|
|
3136
3230
|
return date;
|
|
3137
3231
|
}
|
|
@@ -3143,34 +3237,25 @@ const getRoutineFoldersSchema = paginationShape({
|
|
|
3143
3237
|
defaultPageSize: 5,
|
|
3144
3238
|
maxPageSize: 10
|
|
3145
3239
|
});
|
|
3146
|
-
const getRoutineFolderSchema = {
|
|
3240
|
+
const getRoutineFolderSchema = { folder_id: nonEmptyId };
|
|
3147
3241
|
const createRoutineFolderSchema = routineFolderInputShape;
|
|
3148
3242
|
const folderToolDefinitions = [
|
|
3149
3243
|
{
|
|
3150
3244
|
name: "get-routine-folders",
|
|
3151
3245
|
feature: "folders",
|
|
3152
3246
|
operation: "list",
|
|
3153
|
-
description:
|
|
3154
|
-
summary: "Read-only. Lists default and custom routine folders.",
|
|
3155
|
-
aliases: [
|
|
3156
|
-
"list folders",
|
|
3157
|
-
"browse routine groups",
|
|
3158
|
-
"show plan folders"
|
|
3159
|
-
],
|
|
3160
|
-
useCase: "Use to browse folder organization or discover folder IDs for routine creation.",
|
|
3161
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
|
|
3162
|
-
}),
|
|
3247
|
+
description: "Read-only. Lists routine folders and IDs; results are paginated.",
|
|
3163
3248
|
inputSchema: getRoutineFoldersSchema,
|
|
3164
3249
|
outputSchema: routineFoldersResponse.outputSchema,
|
|
3165
3250
|
annotations: readOnlyAnnotations("Get Routine Folders"),
|
|
3166
3251
|
kind: "read",
|
|
3167
3252
|
responseContract: routineFoldersResponse,
|
|
3168
3253
|
execute: async (runtime, args) => {
|
|
3169
|
-
const { page,
|
|
3254
|
+
const { page, page_size } = args;
|
|
3170
3255
|
try {
|
|
3171
3256
|
const data = await runtime.getClient().getRoutineFolders({
|
|
3172
3257
|
page,
|
|
3173
|
-
pageSize
|
|
3258
|
+
pageSize: page_size
|
|
3174
3259
|
});
|
|
3175
3260
|
return {
|
|
3176
3261
|
items: data?.routine_folders ?? [],
|
|
@@ -3191,32 +3276,23 @@ const folderToolDefinitions = [
|
|
|
3191
3276
|
name: "get-routine-folder",
|
|
3192
3277
|
feature: "folders",
|
|
3193
3278
|
operation: "get",
|
|
3194
|
-
description:
|
|
3195
|
-
summary: "Read-only. Retrieves one routine folder's metadata by ID.",
|
|
3196
|
-
aliases: [
|
|
3197
|
-
"show folder",
|
|
3198
|
-
"folder details",
|
|
3199
|
-
"routine folder metadata"
|
|
3200
|
-
],
|
|
3201
|
-
useCase: "Use for one known folder; use get-routine-folders to browse or discover folder IDs.",
|
|
3202
|
-
importantNotes: "Requires a folderId from get-routine-folders or a prior create response."
|
|
3203
|
-
}),
|
|
3279
|
+
description: "Read-only. Gets one routine folder by folder_id. Use get-routine-folders to discover IDs.",
|
|
3204
3280
|
inputSchema: getRoutineFolderSchema,
|
|
3205
3281
|
outputSchema: routineFolderResponse.outputSchema,
|
|
3206
3282
|
annotations: readOnlyAnnotations("Get Routine Folder"),
|
|
3207
3283
|
kind: "read",
|
|
3208
3284
|
responseContract: routineFolderResponse,
|
|
3209
3285
|
execute: async (runtime, args) => {
|
|
3210
|
-
const {
|
|
3286
|
+
const { folder_id } = args;
|
|
3211
3287
|
try {
|
|
3212
3288
|
return {
|
|
3213
|
-
|
|
3214
|
-
|
|
3289
|
+
routine_folder: await runtime.getClient().getRoutineFolder(folder_id),
|
|
3290
|
+
folder_id
|
|
3215
3291
|
};
|
|
3216
3292
|
} catch (error) {
|
|
3217
3293
|
if (isExpectedReadNotFound(error)) return {
|
|
3218
|
-
|
|
3219
|
-
|
|
3294
|
+
routine_folder: null,
|
|
3295
|
+
folder_id,
|
|
3220
3296
|
expected404Outcome: "not_found"
|
|
3221
3297
|
};
|
|
3222
3298
|
throw error;
|
|
@@ -3227,169 +3303,109 @@ const folderToolDefinitions = [
|
|
|
3227
3303
|
name: "create-routine-folder",
|
|
3228
3304
|
feature: "folders",
|
|
3229
3305
|
operation: "create",
|
|
3230
|
-
description:
|
|
3231
|
-
summary: "Writes to the Hevy account by creating a new routine folder.",
|
|
3232
|
-
aliases: [
|
|
3233
|
-
"add folder",
|
|
3234
|
-
"organize routines",
|
|
3235
|
-
"create plan group"
|
|
3236
|
-
],
|
|
3237
|
-
useCase: "Use to create an organizational destination before assigning new routines to a folderId.",
|
|
3238
|
-
importantNotes: "Requires a non-empty name. Retrying or reusing a name can create duplicate folders."
|
|
3239
|
-
}),
|
|
3306
|
+
description: "Writes a routine folder. Retries or reused titles can create duplicates.",
|
|
3240
3307
|
inputSchema: createRoutineFolderSchema,
|
|
3241
3308
|
annotations: createAnnotations("Create Routine Folder"),
|
|
3242
3309
|
kind: "write",
|
|
3243
3310
|
responseContract: createRoutineFolderResponse,
|
|
3244
3311
|
execute: async (runtime, args) => {
|
|
3245
|
-
return runtime.getClient().createRoutineFolder(
|
|
3312
|
+
return runtime.getClient().createRoutineFolder(args);
|
|
3246
3313
|
}
|
|
3247
3314
|
}
|
|
3248
3315
|
];
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
page,
|
|
3283
|
-
pageCount: data?.page_count
|
|
3284
|
-
};
|
|
3285
|
-
} catch (error) {
|
|
3286
|
-
if (isExpectedListPageNotFound(error, page)) return {
|
|
3287
|
-
items: [],
|
|
3288
|
-
page,
|
|
3289
|
-
expected404Outcome: "end_of_list"
|
|
3290
|
-
};
|
|
3291
|
-
throw error;
|
|
3316
|
+
const routineToolDefinitions = [
|
|
3317
|
+
{
|
|
3318
|
+
name: "get-routines",
|
|
3319
|
+
feature: "routines",
|
|
3320
|
+
operation: "list",
|
|
3321
|
+
description: "Read-only. Lists compact routine summaries. Use get-routine for exercises and sets; results are paginated.",
|
|
3322
|
+
inputSchema: paginationShape({
|
|
3323
|
+
defaultPageSize: 5,
|
|
3324
|
+
maxPageSize: 10
|
|
3325
|
+
}),
|
|
3326
|
+
kind: "read",
|
|
3327
|
+
outputSchema: routinesResponse.outputSchema,
|
|
3328
|
+
annotations: readOnlyAnnotations("Get Routines"),
|
|
3329
|
+
responseContract: routinesResponse,
|
|
3330
|
+
execute: async (runtime, { page, page_size }) => {
|
|
3331
|
+
try {
|
|
3332
|
+
const data = await runtime.getClient().getRoutines({
|
|
3333
|
+
page,
|
|
3334
|
+
pageSize: page_size
|
|
3335
|
+
});
|
|
3336
|
+
return {
|
|
3337
|
+
items: data?.routines ?? [],
|
|
3338
|
+
page,
|
|
3339
|
+
pageCount: data?.page_count
|
|
3340
|
+
};
|
|
3341
|
+
} catch (error) {
|
|
3342
|
+
if (isExpectedListPageNotFound(error, page)) return {
|
|
3343
|
+
items: [],
|
|
3344
|
+
page,
|
|
3345
|
+
expected404Outcome: "end_of_list"
|
|
3346
|
+
};
|
|
3347
|
+
throw error;
|
|
3348
|
+
}
|
|
3292
3349
|
}
|
|
3293
|
-
}
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3350
|
+
},
|
|
3351
|
+
{
|
|
3352
|
+
name: "get-routine",
|
|
3353
|
+
feature: "routines",
|
|
3354
|
+
operation: "get",
|
|
3355
|
+
description: "Read-only. Gets one routine with exercises and sets by routine_id. Use search-routines to discover IDs.",
|
|
3356
|
+
inputSchema: { routine_id: nonEmptyId },
|
|
3357
|
+
kind: "read",
|
|
3358
|
+
outputSchema: routineResponse.outputSchema,
|
|
3359
|
+
annotations: readOnlyAnnotations("Get Routine"),
|
|
3360
|
+
responseContract: routineResponse,
|
|
3361
|
+
execute: async (runtime, { routine_id }) => {
|
|
3362
|
+
try {
|
|
3363
|
+
return {
|
|
3364
|
+
routine: (await runtime.getClient().getRoutineById(String(routine_id)))?.routine,
|
|
3365
|
+
routine_id
|
|
3366
|
+
};
|
|
3367
|
+
} catch (error) {
|
|
3368
|
+
if (isExpectedReadNotFound(error)) return {
|
|
3369
|
+
routine: null,
|
|
3370
|
+
routine_id,
|
|
3371
|
+
expected404Outcome: "not_found"
|
|
3372
|
+
};
|
|
3373
|
+
throw error;
|
|
3374
|
+
}
|
|
3375
|
+
}
|
|
3376
|
+
},
|
|
3377
|
+
{
|
|
3378
|
+
name: "create-routine",
|
|
3379
|
+
feature: "routines",
|
|
3380
|
+
operation: "create",
|
|
3381
|
+
description: "Writes a reusable routine; use create-workout for completed sessions. Retries can create duplicates.",
|
|
3382
|
+
inputSchema: createRoutineInputShape,
|
|
3383
|
+
kind: "write",
|
|
3384
|
+
annotations: createAnnotations("Create Routine"),
|
|
3385
|
+
responseContract: createRoutineResponse,
|
|
3386
|
+
execute: async (runtime, args) => {
|
|
3387
|
+
const { payload, usesRepRanges } = buildRoutinePayload(args.routine, "create");
|
|
3317
3388
|
return {
|
|
3318
|
-
routine:
|
|
3319
|
-
|
|
3320
|
-
};
|
|
3321
|
-
} catch (error) {
|
|
3322
|
-
if (isExpectedReadNotFound(error)) return {
|
|
3323
|
-
routine: null,
|
|
3324
|
-
routineId,
|
|
3325
|
-
expected404Outcome: "not_found"
|
|
3389
|
+
routine: await runtime.getClient().createRoutine({ routine: payload }),
|
|
3390
|
+
usesRepRanges
|
|
3326
3391
|
};
|
|
3327
|
-
throw error;
|
|
3328
3392
|
}
|
|
3329
|
-
}
|
|
3330
|
-
};
|
|
3331
|
-
const routineExercisesSchema = z.preprocess(parseJsonArray, z.array(z.object(routineExerciseShape)));
|
|
3332
|
-
const createRoutineSchema = routinePayloadShape;
|
|
3333
|
-
const createRoutineDefinition = {
|
|
3334
|
-
name: "create-routine",
|
|
3335
|
-
feature: "routines",
|
|
3336
|
-
operation: "create",
|
|
3337
|
-
description: describeTool({
|
|
3338
|
-
summary: "Writes to the Hevy account by creating a new workout routine.",
|
|
3339
|
-
aliases: [
|
|
3340
|
-
"add routine",
|
|
3341
|
-
"build workout plan",
|
|
3342
|
-
"save training template"
|
|
3343
|
-
],
|
|
3344
|
-
useCase: "Use to create a reusable plan; use create-workout to log a completed session.",
|
|
3345
|
-
importantNotes: "Requires exercise template IDs; folderId is optional. Retrying can create duplicates, and non-fixed rep ranges may not display in Hevy apps."
|
|
3346
|
-
}),
|
|
3347
|
-
inputSchema: createRoutineSchema,
|
|
3348
|
-
kind: "write",
|
|
3349
|
-
annotations: createAnnotations("Create Routine"),
|
|
3350
|
-
responseContract: createRoutineResponse,
|
|
3351
|
-
execute: async (runtime, args) => {
|
|
3352
|
-
const { payload, usesRepRanges } = buildRoutinePayload(args, "create");
|
|
3353
|
-
return {
|
|
3354
|
-
routine: await runtime.getClient().createRoutine({ routine: payload }),
|
|
3355
|
-
usesRepRanges
|
|
3356
|
-
};
|
|
3357
|
-
}
|
|
3358
|
-
};
|
|
3359
|
-
const updateRoutineSchema = {
|
|
3360
|
-
routineId: nonEmptyId,
|
|
3361
|
-
title: z.string().min(1),
|
|
3362
|
-
notes: z.string().optional(),
|
|
3363
|
-
exercises: routineExercisesSchema
|
|
3364
|
-
};
|
|
3365
|
-
const routineToolDefinitions = [
|
|
3366
|
-
getRoutinesDefinition,
|
|
3367
|
-
getRoutineDefinition,
|
|
3368
|
-
createRoutineDefinition,
|
|
3393
|
+
},
|
|
3369
3394
|
{
|
|
3370
3395
|
name: "update-routine",
|
|
3371
3396
|
feature: "routines",
|
|
3372
3397
|
operation: "update",
|
|
3373
|
-
description:
|
|
3374
|
-
|
|
3375
|
-
aliases: [
|
|
3376
|
-
"edit routine",
|
|
3377
|
-
"revise workout plan",
|
|
3378
|
-
"replace routine exercises"
|
|
3379
|
-
],
|
|
3380
|
-
useCase: "Use to change a known routine; use create-routine for a separate new plan.",
|
|
3381
|
-
importantNotes: "Requires routineId and the complete title and exercises payload; omitted exercises are removed. Non-fixed rep ranges may not display in Hevy apps."
|
|
3382
|
-
}),
|
|
3383
|
-
inputSchema: updateRoutineSchema,
|
|
3398
|
+
description: "Mutates a routine by replacing its title and exercises. Omitted exercises are removed.",
|
|
3399
|
+
inputSchema: updateRoutineInputShape,
|
|
3384
3400
|
kind: "write",
|
|
3385
3401
|
annotations: updateAnnotations("Update Routine"),
|
|
3386
3402
|
responseContract: updateRoutineResponse,
|
|
3387
3403
|
execute: async (runtime, args) => {
|
|
3388
|
-
const {
|
|
3389
|
-
const { payload, usesRepRanges } = buildRoutinePayload(args, "update");
|
|
3404
|
+
const { routine_id } = args;
|
|
3405
|
+
const { payload, usesRepRanges } = buildRoutinePayload(args.routine, "update");
|
|
3390
3406
|
return {
|
|
3391
|
-
routine: await runtime.getClient().updateRoutine(
|
|
3392
|
-
|
|
3407
|
+
routine: await runtime.getClient().updateRoutine(routine_id, { routine: payload }),
|
|
3408
|
+
routine_id,
|
|
3393
3409
|
usesRepRanges
|
|
3394
3410
|
};
|
|
3395
3411
|
}
|
|
@@ -3401,17 +3417,19 @@ const getExerciseTemplatesSchema = paginationShape({
|
|
|
3401
3417
|
defaultPageSize: 5,
|
|
3402
3418
|
maxPageSize: 100
|
|
3403
3419
|
});
|
|
3404
|
-
const getExerciseTemplateSchema = {
|
|
3420
|
+
const getExerciseTemplateSchema = { exercise_template_id: nonEmptyId };
|
|
3421
|
+
const isoDateTimeBase = z.iso.datetime({ offset: true });
|
|
3422
|
+
const isoDateTimeWithOffset = z.string().refine((value) => isoDateTimeBase.safeParse(value).success, "Must be an ISO 8601 timestamp with an offset").meta({ format: "date-time" });
|
|
3405
3423
|
const getExerciseHistorySchema = {
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3424
|
+
exercise_template_id: nonEmptyId,
|
|
3425
|
+
start_date: isoDateTimeWithOffset.optional(),
|
|
3426
|
+
end_date: isoDateTimeWithOffset.optional()
|
|
3409
3427
|
};
|
|
3410
3428
|
const createExerciseTemplateSchema = exerciseTemplateInputShape;
|
|
3411
3429
|
const searchExerciseTemplatesSchema = {
|
|
3412
|
-
query: z.string().min(1)
|
|
3413
|
-
|
|
3414
|
-
refresh: z.boolean().optional().default(false)
|
|
3430
|
+
query: z.string().min(1),
|
|
3431
|
+
primary_muscle_group: muscleGroupEnum.optional(),
|
|
3432
|
+
refresh: z.boolean().optional().default(false)
|
|
3415
3433
|
};
|
|
3416
3434
|
/** Ordered exercise-template tools for composition by the shared server. */
|
|
3417
3435
|
const templateToolDefinitions = [
|
|
@@ -3419,27 +3437,18 @@ const templateToolDefinitions = [
|
|
|
3419
3437
|
name: "get-exercise-templates",
|
|
3420
3438
|
feature: "templates",
|
|
3421
3439
|
operation: "list",
|
|
3422
|
-
description:
|
|
3423
|
-
summary: "Read-only. Lists default and custom exercise templates with equipment and muscle metadata.",
|
|
3424
|
-
aliases: [
|
|
3425
|
-
"browse exercises",
|
|
3426
|
-
"list exercise catalog",
|
|
3427
|
-
"show movements"
|
|
3428
|
-
],
|
|
3429
|
-
useCase: "Use for page-by-page catalog browsing; use search-exercise-templates for a name lookup across the full catalog.",
|
|
3430
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 100."
|
|
3431
|
-
}),
|
|
3440
|
+
description: "Read-only. Pages through exercise templates. Use search-exercise-templates when a title is known.",
|
|
3432
3441
|
inputSchema: getExerciseTemplatesSchema,
|
|
3433
3442
|
outputSchema: exerciseTemplatesResponse.outputSchema,
|
|
3434
3443
|
annotations: readOnlyAnnotations("Get Exercise Templates"),
|
|
3435
3444
|
kind: "read",
|
|
3436
3445
|
responseContract: exerciseTemplatesResponse,
|
|
3437
3446
|
execute: async (runtime, args) => {
|
|
3438
|
-
const { page,
|
|
3447
|
+
const { page, page_size } = args;
|
|
3439
3448
|
try {
|
|
3440
3449
|
const data = await runtime.getClient().getExerciseTemplates({
|
|
3441
3450
|
page,
|
|
3442
|
-
pageSize
|
|
3451
|
+
pageSize: page_size
|
|
3443
3452
|
});
|
|
3444
3453
|
return {
|
|
3445
3454
|
items: data?.exercise_templates ?? [],
|
|
@@ -3460,32 +3469,23 @@ const templateToolDefinitions = [
|
|
|
3460
3469
|
name: "get-exercise-template",
|
|
3461
3470
|
feature: "templates",
|
|
3462
3471
|
operation: "get",
|
|
3463
|
-
description:
|
|
3464
|
-
summary: "Read-only. Retrieves complete metadata for one exercise template by ID.",
|
|
3465
|
-
aliases: [
|
|
3466
|
-
"show exercise details",
|
|
3467
|
-
"fetch movement",
|
|
3468
|
-
"exercise template info"
|
|
3469
|
-
],
|
|
3470
|
-
useCase: "Use after locating an exact template; use search-exercise-templates when only a name is known.",
|
|
3471
|
-
importantNotes: "Requires an exerciseTemplateId from a template list, search, routine, or workout."
|
|
3472
|
-
}),
|
|
3472
|
+
description: "Read-only. Gets one exercise template by exercise_template_id. Use search-exercise-templates to discover IDs.",
|
|
3473
3473
|
inputSchema: getExerciseTemplateSchema,
|
|
3474
3474
|
outputSchema: exerciseTemplateResponse.outputSchema,
|
|
3475
3475
|
annotations: readOnlyAnnotations("Get Exercise Template"),
|
|
3476
3476
|
kind: "read",
|
|
3477
3477
|
responseContract: exerciseTemplateResponse,
|
|
3478
3478
|
execute: async (runtime, args) => {
|
|
3479
|
-
const {
|
|
3479
|
+
const { exercise_template_id } = args;
|
|
3480
3480
|
try {
|
|
3481
3481
|
return {
|
|
3482
|
-
|
|
3483
|
-
|
|
3482
|
+
exercise_template: await runtime.getClient().getExerciseTemplate(exercise_template_id),
|
|
3483
|
+
exercise_template_id
|
|
3484
3484
|
};
|
|
3485
3485
|
} catch (error) {
|
|
3486
3486
|
if (isExpectedReadNotFound(error)) return {
|
|
3487
|
-
|
|
3488
|
-
|
|
3487
|
+
exercise_template: null,
|
|
3488
|
+
exercise_template_id,
|
|
3489
3489
|
expected404Outcome: "not_found"
|
|
3490
3490
|
};
|
|
3491
3491
|
throw error;
|
|
@@ -3496,29 +3496,20 @@ const templateToolDefinitions = [
|
|
|
3496
3496
|
name: "get-exercise-history",
|
|
3497
3497
|
feature: "templates",
|
|
3498
3498
|
operation: "get",
|
|
3499
|
-
description:
|
|
3500
|
-
summary: "Read-only. Retrieves past performed sets for one exercise template.",
|
|
3501
|
-
aliases: [
|
|
3502
|
-
"exercise progress",
|
|
3503
|
-
"past sets",
|
|
3504
|
-
"movement history"
|
|
3505
|
-
],
|
|
3506
|
-
useCase: "Use to analyze performance for one movement; use get-workouts for complete sessions.",
|
|
3507
|
-
importantNotes: "Requires an exerciseTemplateId. Optional startDate and endDate must be ISO 8601 datetimes with an offset."
|
|
3508
|
-
}),
|
|
3499
|
+
description: "Read-only. Returns performed sets for one exercise-template ID, optionally bounded by ISO 8601 timestamps.",
|
|
3509
3500
|
inputSchema: getExerciseHistorySchema,
|
|
3510
3501
|
outputSchema: exerciseHistoryResponse.outputSchema,
|
|
3511
3502
|
annotations: readOnlyAnnotations("Get Exercise History"),
|
|
3512
3503
|
kind: "read",
|
|
3513
3504
|
responseContract: exerciseHistoryResponse,
|
|
3514
3505
|
execute: async (runtime, args) => {
|
|
3515
|
-
const {
|
|
3506
|
+
const { exercise_template_id, start_date, end_date } = args;
|
|
3516
3507
|
return {
|
|
3517
|
-
|
|
3518
|
-
...
|
|
3519
|
-
...
|
|
3508
|
+
exercise_history: (await runtime.getClient().getExerciseHistory(exercise_template_id, {
|
|
3509
|
+
...start_date ? { start_date } : {},
|
|
3510
|
+
...end_date ? { end_date } : {}
|
|
3520
3511
|
}))?.exercise_history,
|
|
3521
|
-
|
|
3512
|
+
exercise_template_id
|
|
3522
3513
|
};
|
|
3523
3514
|
}
|
|
3524
3515
|
},
|
|
@@ -3526,38 +3517,20 @@ const templateToolDefinitions = [
|
|
|
3526
3517
|
name: "create-exercise-template",
|
|
3527
3518
|
feature: "templates",
|
|
3528
3519
|
operation: "create",
|
|
3529
|
-
description:
|
|
3530
|
-
summary: "Writes to the Hevy account by creating a custom exercise template.",
|
|
3531
|
-
aliases: [
|
|
3532
|
-
"add custom exercise",
|
|
3533
|
-
"create movement",
|
|
3534
|
-
"define exercise"
|
|
3535
|
-
],
|
|
3536
|
-
useCase: "Use only when the needed movement is absent; search-exercise-templates should check existing templates first.",
|
|
3537
|
-
importantNotes: "Requires title, exercise type, equipment category, and primary muscle group. Retrying or reusing a title can create duplicates."
|
|
3538
|
-
}),
|
|
3520
|
+
description: "Writes a custom exercise template. Search first; retries or reused titles can create duplicates.",
|
|
3539
3521
|
inputSchema: createExerciseTemplateSchema,
|
|
3540
3522
|
annotations: createAnnotations("Create Exercise Template"),
|
|
3541
3523
|
kind: "write",
|
|
3542
3524
|
responseContract: createExerciseTemplateResponse,
|
|
3543
3525
|
execute: async (runtime, args) => {
|
|
3544
|
-
return runtime.getClient().createExerciseTemplate(
|
|
3526
|
+
return runtime.getClient().createExerciseTemplate(args);
|
|
3545
3527
|
}
|
|
3546
3528
|
},
|
|
3547
3529
|
{
|
|
3548
3530
|
name: "search-exercise-templates",
|
|
3549
3531
|
feature: "templates",
|
|
3550
3532
|
operation: "search",
|
|
3551
|
-
description:
|
|
3552
|
-
summary: "Read-only for the Hevy account. Searches the full exercise template catalog by title substring.",
|
|
3553
|
-
aliases: [
|
|
3554
|
-
"find exercise",
|
|
3555
|
-
"look up movement",
|
|
3556
|
-
"search exercise IDs"
|
|
3557
|
-
],
|
|
3558
|
-
useCase: "Use when a name or partial name is known, especially to discover IDs for workouts and routines; use get-exercise-templates for page browsing.",
|
|
3559
|
-
importantNotes: "Matching is case-insensitive. The catalog is cached locally for 5 minutes; refresh:true re-fetches all pages and changes only local cache state."
|
|
3560
|
-
}),
|
|
3533
|
+
description: "Read-only. Searches template titles case-insensitively and returns IDs. refresh reloads the five-minute catalog cache.",
|
|
3561
3534
|
inputSchema: searchExerciseTemplatesSchema,
|
|
3562
3535
|
outputSchema: searchExerciseTemplatesResponse.outputSchema,
|
|
3563
3536
|
annotations: readOnlyAnnotations("Search Exercise Templates"),
|
|
@@ -3565,7 +3538,7 @@ const templateToolDefinitions = [
|
|
|
3565
3538
|
responseContract: searchExerciseTemplatesResponse,
|
|
3566
3539
|
execute: async (runtime, args) => {
|
|
3567
3540
|
runtime.getClient();
|
|
3568
|
-
const { query,
|
|
3541
|
+
const { query, primary_muscle_group, refresh } = args;
|
|
3569
3542
|
const templates = await runtime.catalog.get({
|
|
3570
3543
|
refresh,
|
|
3571
3544
|
onRefreshed: (refreshedCatalog, reason) => {
|
|
@@ -3586,11 +3559,11 @@ const templateToolDefinitions = [
|
|
|
3586
3559
|
});
|
|
3587
3560
|
const queryLower = query.toLowerCase();
|
|
3588
3561
|
let results = templates.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower));
|
|
3589
|
-
if (
|
|
3562
|
+
if (primary_muscle_group !== void 0) results = results.filter((t) => t.primary_muscle_group === primary_muscle_group);
|
|
3590
3563
|
return {
|
|
3591
3564
|
results,
|
|
3592
3565
|
query,
|
|
3593
|
-
|
|
3566
|
+
primary_muscle_group
|
|
3594
3567
|
};
|
|
3595
3568
|
}
|
|
3596
3569
|
}
|
|
@@ -3599,16 +3572,7 @@ const userToolDefinitions = [{
|
|
|
3599
3572
|
name: "get-user-info",
|
|
3600
3573
|
feature: "profile",
|
|
3601
3574
|
operation: "get",
|
|
3602
|
-
description:
|
|
3603
|
-
summary: "Read-only. Returns the authenticated account's user ID, display name, and public profile URL.",
|
|
3604
|
-
aliases: [
|
|
3605
|
-
"who am I",
|
|
3606
|
-
"account info",
|
|
3607
|
-
"verify Hevy user"
|
|
3608
|
-
],
|
|
3609
|
-
useCase: "Use to confirm which Hevy account is connected before reading or writing account data.",
|
|
3610
|
-
importantNotes: "Accepts no inputs and reports only the account associated with the configured credentials."
|
|
3611
|
-
}),
|
|
3575
|
+
description: "Read-only. Returns the authenticated Hevy user ID, display name, and public profile URL.",
|
|
3612
3576
|
inputSchema: {},
|
|
3613
3577
|
kind: "read",
|
|
3614
3578
|
outputSchema: userResponse.outputSchema,
|
|
@@ -3621,8 +3585,8 @@ const userToolDefinitions = [{
|
|
|
3621
3585
|
//#endregion
|
|
3622
3586
|
//#region ../core/src/tools/routine-discovery.ts
|
|
3623
3587
|
const routineDiscoverySchema = {
|
|
3624
|
-
query: z.string().min(1).optional()
|
|
3625
|
-
limit: z.coerce.number().int().min(1).max(100).default(20)
|
|
3588
|
+
query: z.string().min(1).optional(),
|
|
3589
|
+
limit: z.coerce.number().int().min(1).max(100).default(20)
|
|
3626
3590
|
};
|
|
3627
3591
|
async function discoverRoutines(runtime, { query, limit }) {
|
|
3628
3592
|
const normalizedQuery = query?.toLocaleLowerCase();
|
|
@@ -3649,14 +3613,7 @@ async function discoverRoutines(runtime, { query, limit }) {
|
|
|
3649
3613
|
page += 1;
|
|
3650
3614
|
}
|
|
3651
3615
|
return {
|
|
3652
|
-
routines: routines.slice(0, limit).map(
|
|
3653
|
-
...routine.id ? { id: routine.id } : {},
|
|
3654
|
-
...routine.title ? { title: routine.title } : {},
|
|
3655
|
-
folderId: routine.folder_id ?? null,
|
|
3656
|
-
...routine.updated_at ? { updatedAt: routine.updated_at } : {},
|
|
3657
|
-
exerciseCount: routine.exercises?.length ?? 0,
|
|
3658
|
-
setCount: routine.exercises?.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0) ?? 0
|
|
3659
|
-
})),
|
|
3616
|
+
routines: routines.slice(0, limit).map(summarizeRoutine),
|
|
3660
3617
|
workflow: {
|
|
3661
3618
|
name: "routine-discovery",
|
|
3662
3619
|
pagination: { routines: pages },
|
|
@@ -3669,16 +3626,7 @@ const routineDiscoveryToolDefinitions = [{
|
|
|
3669
3626
|
name: "search-routines",
|
|
3670
3627
|
feature: "workflows",
|
|
3671
3628
|
operation: "search",
|
|
3672
|
-
description:
|
|
3673
|
-
summary: "Read-only. Discovers routines by title and returns compact metadata without full set payloads.",
|
|
3674
|
-
aliases: [
|
|
3675
|
-
"find routine",
|
|
3676
|
-
"browse routine names",
|
|
3677
|
-
"compact routine list"
|
|
3678
|
-
],
|
|
3679
|
-
useCase: "Use to find a routine ID or shortlist plans before calling get-routine for full exercise details.",
|
|
3680
|
-
importantNotes: "Search scans routine pages at pageSize 10 and returns only IDs, titles, folder metadata, and exercise/set counts."
|
|
3681
|
-
}),
|
|
3629
|
+
description: "Read-only. Searches routine titles and returns compact IDs and counts. Use get-routine for full exercises and sets.",
|
|
3682
3630
|
inputSchema: routineDiscoverySchema,
|
|
3683
3631
|
outputSchema: compactRoutinesResponse.outputSchema,
|
|
3684
3632
|
annotations: readOnlyAnnotations("Search Routines"),
|
|
@@ -3688,7 +3636,7 @@ const routineDiscoveryToolDefinitions = [{
|
|
|
3688
3636
|
}];
|
|
3689
3637
|
//#endregion
|
|
3690
3638
|
//#region ../core/src/tools/workflows.ts
|
|
3691
|
-
const trainingSummarySchema = { weeks: z.coerce.number().int().min(1).max(12).default(4)
|
|
3639
|
+
const trainingSummarySchema = { weeks: z.coerce.number().int().min(1).max(12).default(4) };
|
|
3692
3640
|
const MILLISECONDS_PER_DAY = 1440 * 60 * 1e3;
|
|
3693
3641
|
function parseUtcDate(value) {
|
|
3694
3642
|
const normalized = value.length === 10 ? `${value}T00:00:00.000Z` : value;
|
|
@@ -3742,28 +3690,29 @@ function getPeriod(weeks) {
|
|
|
3742
3690
|
};
|
|
3743
3691
|
}
|
|
3744
3692
|
function durationSeconds(workout) {
|
|
3745
|
-
if (!workout.start_time || !workout.end_time) return
|
|
3693
|
+
if (!workout.start_time || !workout.end_time) return void 0;
|
|
3746
3694
|
const duration = new Date(workout.end_time).getTime() - new Date(workout.start_time).getTime();
|
|
3747
|
-
return Number.isFinite(duration) && duration >= 0 ? Math.floor(duration / 1e3) :
|
|
3695
|
+
return Number.isFinite(duration) && duration >= 0 ? Math.floor(duration / 1e3) : void 0;
|
|
3748
3696
|
}
|
|
3749
3697
|
function compactSession(workout) {
|
|
3750
3698
|
const exercises = workout.exercises ?? [];
|
|
3699
|
+
const elapsedSeconds = durationSeconds(workout);
|
|
3751
3700
|
return {
|
|
3752
3701
|
...workout.id ? { id: workout.id } : {},
|
|
3753
3702
|
...workout.title ? { title: workout.title } : {},
|
|
3754
|
-
...workout.start_time ? {
|
|
3755
|
-
...workout.end_time ? {
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3703
|
+
...workout.start_time ? { start_time: workout.start_time } : {},
|
|
3704
|
+
...workout.end_time ? { end_time: workout.end_time } : {},
|
|
3705
|
+
...elapsedSeconds === void 0 ? {} : { duration_seconds: elapsedSeconds },
|
|
3706
|
+
exercise_count: exercises.length,
|
|
3707
|
+
set_count: exercises.reduce((total, exercise) => total + (exercise.sets?.length ?? 0), 0)
|
|
3759
3708
|
};
|
|
3760
3709
|
}
|
|
3761
3710
|
function compactMeasurement(measurement) {
|
|
3762
3711
|
return {
|
|
3763
3712
|
date: measurement.date,
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3713
|
+
...measurement.weight_kg == null ? {} : { weight_kg: measurement.weight_kg },
|
|
3714
|
+
...measurement.lean_mass_kg == null ? {} : { lean_mass_kg: measurement.lean_mass_kg },
|
|
3715
|
+
...measurement.fat_percent == null ? {} : { fat_percent: measurement.fat_percent }
|
|
3767
3716
|
};
|
|
3768
3717
|
}
|
|
3769
3718
|
async function getTrainingSummary(runtime, weeks) {
|
|
@@ -3791,37 +3740,38 @@ async function getTrainingSummary(runtime, weeks) {
|
|
|
3791
3740
|
}, pageSize, period.startDate, period.endDate, (measurement) => measurement.date)]);
|
|
3792
3741
|
const workouts = workoutPages.items;
|
|
3793
3742
|
const sessions = workouts.map(compactSession);
|
|
3794
|
-
const
|
|
3743
|
+
const unique_exercise_template_ids = [...new Set(workouts.flatMap((workout) => (workout.exercises ?? []).map((exercise) => exercise.exercise_template_id).filter((id) => Boolean(id))))];
|
|
3795
3744
|
const measurements = [...measurementPages.items].sort((a, b) => a.date.localeCompare(b.date));
|
|
3796
3745
|
const earliestMeasurement = measurements[0];
|
|
3797
3746
|
const latestMeasurement = measurements.at(-1);
|
|
3798
|
-
const earliest = earliestMeasurement ? compactMeasurement(earliestMeasurement) :
|
|
3799
|
-
const latest = latestMeasurement ? compactMeasurement(latestMeasurement) :
|
|
3800
|
-
const
|
|
3747
|
+
const earliest = earliestMeasurement ? compactMeasurement(earliestMeasurement) : void 0;
|
|
3748
|
+
const latest = latestMeasurement ? compactMeasurement(latestMeasurement) : void 0;
|
|
3749
|
+
const weight_change_kg = latest?.weight_kg !== void 0 && earliest?.weight_kg !== void 0 ? latest.weight_kg - earliest.weight_kg : void 0;
|
|
3801
3750
|
return {
|
|
3802
3751
|
period: {
|
|
3803
|
-
|
|
3752
|
+
start_date: period.startDate,
|
|
3753
|
+
end_date: period.endDate,
|
|
3804
3754
|
weeks
|
|
3805
3755
|
},
|
|
3806
3756
|
workouts: {
|
|
3807
3757
|
count: workouts.length,
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3758
|
+
total_duration_seconds: sessions.reduce((total, session) => total + (session.duration_seconds ?? 0), 0),
|
|
3759
|
+
exercise_count: sessions.reduce((total, session) => total + session.exercise_count, 0),
|
|
3760
|
+
set_count: sessions.reduce((total, session) => total + session.set_count, 0),
|
|
3761
|
+
unique_exercise_template_ids,
|
|
3812
3762
|
sessions
|
|
3813
3763
|
},
|
|
3814
|
-
|
|
3764
|
+
body_measurements: {
|
|
3815
3765
|
count: measurements.length,
|
|
3816
|
-
latest,
|
|
3817
|
-
earliest,
|
|
3818
|
-
|
|
3766
|
+
...latest ? { latest } : {},
|
|
3767
|
+
...earliest ? { earliest } : {},
|
|
3768
|
+
...weight_change_kg === void 0 ? {} : { weight_change_kg }
|
|
3819
3769
|
},
|
|
3820
3770
|
workflow: {
|
|
3821
3771
|
name: "training-summary",
|
|
3822
3772
|
pagination: {
|
|
3823
3773
|
workouts: workoutPages.pages,
|
|
3824
|
-
|
|
3774
|
+
body_measurements: measurementPages.pages
|
|
3825
3775
|
},
|
|
3826
3776
|
cacheStatus: "not-used",
|
|
3827
3777
|
itemsScanned: workoutPages.itemsScanned + measurementPages.itemsScanned
|
|
@@ -3832,16 +3782,7 @@ const workflowToolDefinitions = [{
|
|
|
3832
3782
|
name: "get-training-summary",
|
|
3833
3783
|
feature: "workflows",
|
|
3834
3784
|
operation: "get",
|
|
3835
|
-
description:
|
|
3836
|
-
summary: "Read-only. Summarizes recent workout activity and body-measurement trends in one call.",
|
|
3837
|
-
aliases: [
|
|
3838
|
-
"training progress",
|
|
3839
|
-
"progress summary",
|
|
3840
|
-
"recent training overview"
|
|
3841
|
-
],
|
|
3842
|
-
useCase: "Use for a bounded progress review instead of separately counting and paging through workouts and body measurements.",
|
|
3843
|
-
importantNotes: "The summary covers the most recent 1-12 weeks, returns compact session evidence, and reports the pages and items scanned."
|
|
3844
|
-
}),
|
|
3785
|
+
description: "Read-only. Summarizes workouts and body-measurement trends for the last 1–12 weeks, including compact session and scan evidence.",
|
|
3845
3786
|
inputSchema: trainingSummarySchema,
|
|
3846
3787
|
outputSchema: trainingSummaryResponse.outputSchema,
|
|
3847
3788
|
annotations: readOnlyAnnotations("Get Training Summary"),
|
|
@@ -3856,7 +3797,7 @@ const getWorkoutsSchema = paginationShape({
|
|
|
3856
3797
|
maxPageSize: 10,
|
|
3857
3798
|
integerPage: false
|
|
3858
3799
|
});
|
|
3859
|
-
const getWorkoutSchema = {
|
|
3800
|
+
const getWorkoutSchema = { workout_id: nonEmptyId };
|
|
3860
3801
|
const getWorkoutEventsSchema = {
|
|
3861
3802
|
...paginationShape({
|
|
3862
3803
|
defaultPageSize: 5,
|
|
@@ -3864,11 +3805,9 @@ const getWorkoutEventsSchema = {
|
|
|
3864
3805
|
}),
|
|
3865
3806
|
since: z.string().default("1970-01-01T00:00:00Z")
|
|
3866
3807
|
};
|
|
3867
|
-
const createWorkoutSchema =
|
|
3868
|
-
const updateWorkoutSchema =
|
|
3869
|
-
|
|
3870
|
-
...workoutPayloadShape
|
|
3871
|
-
};
|
|
3808
|
+
const createWorkoutSchema = workoutInputShape;
|
|
3809
|
+
const updateWorkoutSchema = updateWorkoutInputShape;
|
|
3810
|
+
const replaceWorkoutExercisesSchema = replaceWorkoutExercisesInputShape;
|
|
3872
3811
|
//#endregion
|
|
3873
3812
|
//#region ../core/src/tools/register.ts
|
|
3874
3813
|
const hevyToolDefinitions = [
|
|
@@ -3877,16 +3816,7 @@ const hevyToolDefinitions = [
|
|
|
3877
3816
|
name: "get-workouts",
|
|
3878
3817
|
feature: "workouts",
|
|
3879
3818
|
operation: "list",
|
|
3880
|
-
description:
|
|
3881
|
-
summary: "Read-only. Lists workouts from newest to oldest with exercise and timing details.",
|
|
3882
|
-
aliases: [
|
|
3883
|
-
"list workout history",
|
|
3884
|
-
"show recent workouts",
|
|
3885
|
-
"browse logs"
|
|
3886
|
-
],
|
|
3887
|
-
useCase: "Use to browse or page through workout history; use get-workout when a workout ID is already known.",
|
|
3888
|
-
importantNotes: "Results are paginated; page starts at 1 and pageSize is limited to 10."
|
|
3889
|
-
}),
|
|
3819
|
+
description: "Read-only. Lists compact workout summaries newest first. Use get-workout for exercises and sets; results are paginated.",
|
|
3890
3820
|
inputSchema: getWorkoutsSchema,
|
|
3891
3821
|
outputSchema: workoutsResponse.outputSchema,
|
|
3892
3822
|
annotations: readOnlyAnnotations("Get Workouts"),
|
|
@@ -3896,7 +3826,7 @@ const hevyToolDefinitions = [
|
|
|
3896
3826
|
try {
|
|
3897
3827
|
const data = await runtime.getClient().getWorkouts({
|
|
3898
3828
|
page: args.page,
|
|
3899
|
-
pageSize: args.
|
|
3829
|
+
pageSize: args.page_size
|
|
3900
3830
|
});
|
|
3901
3831
|
return {
|
|
3902
3832
|
items: data?.workouts ?? [],
|
|
@@ -3917,16 +3847,7 @@ const hevyToolDefinitions = [
|
|
|
3917
3847
|
name: "get-workout",
|
|
3918
3848
|
feature: "workouts",
|
|
3919
3849
|
operation: "get",
|
|
3920
|
-
description:
|
|
3921
|
-
summary: "Read-only. Retrieves complete details for one workout by its ID.",
|
|
3922
|
-
aliases: [
|
|
3923
|
-
"show workout",
|
|
3924
|
-
"fetch workout details",
|
|
3925
|
-
"open workout log"
|
|
3926
|
-
],
|
|
3927
|
-
useCase: "Use after get-workouts identifies the exact workout; do not use for browsing multiple workouts.",
|
|
3928
|
-
importantNotes: "Requires a workoutId discovered from a workout list, event, or prior create response."
|
|
3929
|
-
}),
|
|
3850
|
+
description: "Read-only. Gets one workout with exercises and sets by workout_id. Use get-workouts to discover IDs.",
|
|
3930
3851
|
inputSchema: getWorkoutSchema,
|
|
3931
3852
|
outputSchema: workoutResponse.outputSchema,
|
|
3932
3853
|
annotations: readOnlyAnnotations("Get Workout"),
|
|
@@ -3935,13 +3856,13 @@ const hevyToolDefinitions = [
|
|
|
3935
3856
|
execute: async (runtime, args) => {
|
|
3936
3857
|
try {
|
|
3937
3858
|
return {
|
|
3938
|
-
workout: await runtime.getClient().getWorkout(args.
|
|
3939
|
-
|
|
3859
|
+
workout: await runtime.getClient().getWorkout(args.workout_id),
|
|
3860
|
+
workout_id: args.workout_id
|
|
3940
3861
|
};
|
|
3941
3862
|
} catch (error) {
|
|
3942
3863
|
if (isExpectedReadNotFound(error)) return {
|
|
3943
3864
|
workout: null,
|
|
3944
|
-
|
|
3865
|
+
workout_id: args.workout_id,
|
|
3945
3866
|
expected404Outcome: "not_found"
|
|
3946
3867
|
};
|
|
3947
3868
|
throw error;
|
|
@@ -3952,16 +3873,7 @@ const hevyToolDefinitions = [
|
|
|
3952
3873
|
name: "get-workout-count",
|
|
3953
3874
|
feature: "workouts",
|
|
3954
3875
|
operation: "count",
|
|
3955
|
-
description:
|
|
3956
|
-
summary: "Read-only. Returns the total workout count for the account.",
|
|
3957
|
-
aliases: [
|
|
3958
|
-
"count workouts",
|
|
3959
|
-
"how many workouts",
|
|
3960
|
-
"workout total"
|
|
3961
|
-
],
|
|
3962
|
-
useCase: "Use for totals, statistics, or estimating pages; use get-workouts for actual workout records.",
|
|
3963
|
-
importantNotes: "Returns only a count and accepts no paging or date filters."
|
|
3964
|
-
}),
|
|
3876
|
+
description: "Read-only. Returns the total workout count; it does not return records or accept date filters.",
|
|
3965
3877
|
inputSchema: {},
|
|
3966
3878
|
outputSchema: workoutCountResponse.outputSchema,
|
|
3967
3879
|
annotations: readOnlyAnnotations("Get Workout Count"),
|
|
@@ -3975,16 +3887,7 @@ const hevyToolDefinitions = [
|
|
|
3975
3887
|
name: "get-workout-events",
|
|
3976
3888
|
feature: "workouts",
|
|
3977
3889
|
operation: "sync",
|
|
3978
|
-
description:
|
|
3979
|
-
summary: "Read-only. Lists workout update and delete events since a timestamp, newest first.",
|
|
3980
|
-
aliases: [
|
|
3981
|
-
"sync workout changes",
|
|
3982
|
-
"workout change feed",
|
|
3983
|
-
"deleted workouts"
|
|
3984
|
-
],
|
|
3985
|
-
useCase: "Use to incrementally synchronize a local workout cache; use get-workouts for the current workout list.",
|
|
3986
|
-
importantNotes: "since must be a timestamp string; events are paginated with pageSize at most 10, and the default since value reads from 1970."
|
|
3987
|
-
}),
|
|
3890
|
+
description: "Read-only. Lists workout update and deletion events since a timestamp for incremental sync; results are paginated.",
|
|
3988
3891
|
inputSchema: getWorkoutEventsSchema,
|
|
3989
3892
|
outputSchema: workoutEventsResponse.outputSchema,
|
|
3990
3893
|
annotations: readOnlyAnnotations("Get Workout Events"),
|
|
@@ -3994,7 +3897,7 @@ const hevyToolDefinitions = [
|
|
|
3994
3897
|
try {
|
|
3995
3898
|
const data = await runtime.getClient().getWorkoutEvents({
|
|
3996
3899
|
page: args.page,
|
|
3997
|
-
pageSize: args.
|
|
3900
|
+
pageSize: args.page_size,
|
|
3998
3901
|
since: args.since
|
|
3999
3902
|
});
|
|
4000
3903
|
return {
|
|
@@ -4018,46 +3921,48 @@ const hevyToolDefinitions = [
|
|
|
4018
3921
|
name: "create-workout",
|
|
4019
3922
|
feature: "workouts",
|
|
4020
3923
|
operation: "create",
|
|
4021
|
-
description:
|
|
4022
|
-
summary: "Writes to the Hevy account by creating a new workout.",
|
|
4023
|
-
aliases: [
|
|
4024
|
-
"log workout",
|
|
4025
|
-
"add workout",
|
|
4026
|
-
"record training session"
|
|
4027
|
-
],
|
|
4028
|
-
useCase: "Use to add a completed workout; use update-workout only when modifying an existing workout ID.",
|
|
4029
|
-
importantNotes: "Requires UTC startTime/endTime in YYYY-MM-DDTHH:mm:ssZ form and exercise template IDs. Retrying can create duplicates."
|
|
4030
|
-
}),
|
|
3924
|
+
description: "Writes a completed workout. Requires exercise-template IDs and UTC times. Retries can create duplicates.",
|
|
4031
3925
|
inputSchema: createWorkoutSchema,
|
|
4032
3926
|
annotations: createAnnotations("Create Workout"),
|
|
4033
3927
|
kind: "write",
|
|
4034
3928
|
responseContract: createWorkoutResponse,
|
|
4035
3929
|
execute: async (runtime, args) => {
|
|
4036
|
-
return await runtime.getClient().createWorkout({ workout:
|
|
3930
|
+
return await runtime.getClient().createWorkout({ workout: args.workout });
|
|
4037
3931
|
}
|
|
4038
3932
|
},
|
|
4039
3933
|
{
|
|
4040
3934
|
name: "update-workout",
|
|
4041
3935
|
feature: "workouts",
|
|
4042
3936
|
operation: "update",
|
|
4043
|
-
description:
|
|
4044
|
-
summary: "Mutates the Hevy account by replacing an existing workout.",
|
|
4045
|
-
aliases: [
|
|
4046
|
-
"edit workout",
|
|
4047
|
-
"correct workout log",
|
|
4048
|
-
"replace workout details"
|
|
4049
|
-
],
|
|
4050
|
-
useCase: "Use to revise a known workout; use create-workout for a new training session.",
|
|
4051
|
-
importantNotes: "Requires workoutId plus the complete title, times, privacy, exercises, and sets payload; omitted optional values may be cleared or defaulted."
|
|
4052
|
-
}),
|
|
3937
|
+
description: "Mutates workout metadata by ID. Omitted fields and all exercises remain unchanged.",
|
|
4053
3938
|
inputSchema: updateWorkoutSchema,
|
|
4054
3939
|
annotations: updateAnnotations("Update Workout"),
|
|
4055
3940
|
kind: "write",
|
|
4056
3941
|
responseContract: updateWorkoutResponse,
|
|
4057
3942
|
execute: async (runtime, args) => {
|
|
3943
|
+
const client = runtime.getClient();
|
|
3944
|
+
const payload = buildWorkoutUpdatePayload(await client.getWorkout(args.workout_id), args.workout);
|
|
3945
|
+
return {
|
|
3946
|
+
workout: await client.updateWorkout(args.workout_id, { workout: payload }),
|
|
3947
|
+
workout_id: args.workout_id
|
|
3948
|
+
};
|
|
3949
|
+
}
|
|
3950
|
+
},
|
|
3951
|
+
{
|
|
3952
|
+
name: "replace-workout-exercises",
|
|
3953
|
+
feature: "workouts",
|
|
3954
|
+
operation: "update",
|
|
3955
|
+
description: "Mutates a workout by replacing all exercises and sets. Workout metadata remains unchanged.",
|
|
3956
|
+
inputSchema: replaceWorkoutExercisesSchema,
|
|
3957
|
+
annotations: updateAnnotations("Replace Workout Exercises"),
|
|
3958
|
+
kind: "write",
|
|
3959
|
+
responseContract: updateWorkoutResponse,
|
|
3960
|
+
execute: async (runtime, args) => {
|
|
3961
|
+
const client = runtime.getClient();
|
|
3962
|
+
const payload = buildWorkoutUpdatePayload(await client.getWorkout(args.workout_id), {}, args.workout.exercises);
|
|
4058
3963
|
return {
|
|
4059
|
-
workout: await
|
|
4060
|
-
|
|
3964
|
+
workout: await client.updateWorkout(args.workout_id, { workout: payload }),
|
|
3965
|
+
workout_id: args.workout_id
|
|
4061
3966
|
};
|
|
4062
3967
|
}
|
|
4063
3968
|
}
|
|
@@ -4136,55 +4041,59 @@ function withErrorHandling(fn, context, onError) {
|
|
|
4136
4041
|
}
|
|
4137
4042
|
//#endregion
|
|
4138
4043
|
//#region ../core/src/tools/tool-runtime.ts
|
|
4139
|
-
const STRUCTURAL_ARGUMENT_KEYS =
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
const PRESENCE_ARGUMENT_KEYS =
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
const NUMERIC_ARGUMENT_KEYS =
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
const BOOLEAN_ARGUMENT_KEYS =
|
|
4044
|
+
const STRUCTURAL_ARGUMENT_KEYS = {
|
|
4045
|
+
page: true,
|
|
4046
|
+
page_size: true,
|
|
4047
|
+
since: true,
|
|
4048
|
+
workout_id: true,
|
|
4049
|
+
routine_id: true,
|
|
4050
|
+
folder_id: true,
|
|
4051
|
+
exercise_template_id: true,
|
|
4052
|
+
date: true,
|
|
4053
|
+
start_date: true,
|
|
4054
|
+
end_date: true,
|
|
4055
|
+
updated_since: true,
|
|
4056
|
+
include_custom: true,
|
|
4057
|
+
limit: true,
|
|
4058
|
+
offset: true,
|
|
4059
|
+
refresh: true,
|
|
4060
|
+
query: true,
|
|
4061
|
+
primary_muscle_group: true
|
|
4062
|
+
};
|
|
4063
|
+
const PRESENCE_ARGUMENT_KEYS = {
|
|
4064
|
+
since: true,
|
|
4065
|
+
workout_id: true,
|
|
4066
|
+
routine_id: true,
|
|
4067
|
+
folder_id: true,
|
|
4068
|
+
exercise_template_id: true,
|
|
4069
|
+
date: true,
|
|
4070
|
+
start_date: true,
|
|
4071
|
+
end_date: true,
|
|
4072
|
+
updated_since: true,
|
|
4073
|
+
query: true,
|
|
4074
|
+
primary_muscle_group: true
|
|
4075
|
+
};
|
|
4076
|
+
const NUMERIC_ARGUMENT_KEYS = {
|
|
4077
|
+
page: true,
|
|
4078
|
+
page_size: true,
|
|
4079
|
+
limit: true,
|
|
4080
|
+
offset: true
|
|
4081
|
+
};
|
|
4082
|
+
const BOOLEAN_ARGUMENT_KEYS = {
|
|
4083
|
+
include_custom: true,
|
|
4084
|
+
refresh: true
|
|
4085
|
+
};
|
|
4086
|
+
const structuralArgumentKeys = Object.keys(STRUCTURAL_ARGUMENT_KEYS);
|
|
4178
4087
|
function createSafeInvocation(name, args, taxonomy) {
|
|
4179
|
-
const argumentKeys =
|
|
4088
|
+
const argumentKeys = structuralArgumentKeys.filter((key) => key in args);
|
|
4180
4089
|
const argumentPresence = {};
|
|
4181
4090
|
const numericArgumentBuckets = {};
|
|
4182
4091
|
const booleanArguments = {};
|
|
4183
4092
|
for (const key of argumentKeys) {
|
|
4184
4093
|
const value = args[key];
|
|
4185
|
-
if (
|
|
4186
|
-
if (
|
|
4187
|
-
if (
|
|
4094
|
+
if (key in PRESENCE_ARGUMENT_KEYS && value !== null && value !== void 0) argumentPresence[key] = true;
|
|
4095
|
+
if (key in NUMERIC_ARGUMENT_KEYS && typeof value === "number") numericArgumentBuckets[key] = bucketCount(value);
|
|
4096
|
+
if (key in BOOLEAN_ARGUMENT_KEYS && typeof value === "boolean") booleanArguments[key] = value;
|
|
4188
4097
|
}
|
|
4189
4098
|
return {
|
|
4190
4099
|
name,
|
|
@@ -4399,18 +4308,13 @@ function createCountingServer(server) {
|
|
|
4399
4308
|
let count = 0;
|
|
4400
4309
|
return {
|
|
4401
4310
|
server: new Proxy(server, { get(target, property, receiver) {
|
|
4402
|
-
if (property === "tool") return (...args) => {
|
|
4403
|
-
const result = target.tool(...args);
|
|
4404
|
-
count += 1;
|
|
4405
|
-
return result;
|
|
4406
|
-
};
|
|
4407
4311
|
if (property === "registerTool") {
|
|
4408
|
-
const registerTool = (
|
|
4409
|
-
|
|
4312
|
+
const registerTool = target.registerTool.bind(target);
|
|
4313
|
+
return (...args) => {
|
|
4314
|
+
const result = Reflect.apply(registerTool, target, args);
|
|
4410
4315
|
count += 1;
|
|
4411
4316
|
return result;
|
|
4412
4317
|
};
|
|
4413
|
-
return registerTool;
|
|
4414
4318
|
}
|
|
4415
4319
|
return Reflect.get(target, property, receiver);
|
|
4416
4320
|
} }),
|
|
@@ -4812,7 +4716,7 @@ async function startStreamableHttpServer(options, apiKey, createMcpServer) {
|
|
|
4812
4716
|
let session;
|
|
4813
4717
|
let mcpServer;
|
|
4814
4718
|
let connected = false;
|
|
4815
|
-
const transport = new
|
|
4719
|
+
const transport = new NodeStreamableHTTPServerTransport({
|
|
4816
4720
|
sessionIdGenerator: randomUUID,
|
|
4817
4721
|
onsessioninitialized: (id) => {
|
|
4818
4722
|
if (session) sessions.set(id, session);
|