hevy-mcp 1.28.1-beta.1 → 3.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 +76 -32
- package/dist/cli.mjs +2 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{src-CHT_Meh8.mjs → src-BBzEvkOX.mjs} +89 -65
- package/dist/src-BBzEvkOX.mjs.map +1 -0
- package/package.json +15 -2
- package/server.json +2 -2
- package/dist/src-CHT_Meh8.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -15,6 +15,7 @@ A Model Context Protocol (MCP) server implementation that interfaces with the [H
|
|
|
15
15
|
- [Quick Start](#quick-start)
|
|
16
16
|
- [Prerequisites](#prerequisites)
|
|
17
17
|
- [Installation](#installation)
|
|
18
|
+
- [Run with Docker](#run-with-docker)
|
|
18
19
|
- [Claude Desktop Configuration](#claude-desktop-configuration)
|
|
19
20
|
- [Cursor Configuration](#cursor-configuration)
|
|
20
21
|
- [Other MCP Clients (via add-mcp)](#other-mcp-clients-via-add-mcp)
|
|
@@ -39,10 +40,11 @@ A Model Context Protocol (MCP) server implementation that interfaces with the [H
|
|
|
39
40
|
|
|
40
41
|
Pick the workflow that fits your setup:
|
|
41
42
|
|
|
42
|
-
| Scenario | Command
|
|
43
|
-
| :-------------------- |
|
|
44
|
-
| **One-off stdio run** | `HEVY_API_KEY=
|
|
45
|
-
| **
|
|
43
|
+
| Scenario | Command | Requirements |
|
|
44
|
+
| :-------------------- | :-------------------------------------------------------------------------------------- | :------------------------- |
|
|
45
|
+
| **One-off stdio run** | `HEVY_API_KEY=your_key npx -y hevy-mcp` or `HEVY_API_KEY=your_key bunx hevy-mcp@latest` | Node.js ≥ 20, Hevy API key |
|
|
46
|
+
| **Docker stdio run** | `docker run -i --rm -e HEVY_API_KEY ghcr.io/chrisdoc/hevy-mcp:latest` | Docker, Hevy API key |
|
|
47
|
+
| **Local development** | `npm install && npm run build && npm start` | `.env` with `HEVY_API_KEY` |
|
|
46
48
|
|
|
47
49
|
---
|
|
48
50
|
|
|
@@ -51,6 +53,7 @@ Pick the workflow that fits your setup:
|
|
|
51
53
|
- **Node.js**: v20 or higher (strongly recommended to use the exact version pinned in `.nvmrc`).
|
|
52
54
|
- **npm**: v10 or higher.
|
|
53
55
|
- **Bun** (optional): If you want to launch with `bunx`.
|
|
56
|
+
- **Docker** (optional): If you want an isolated container-based stdio setup.
|
|
54
57
|
- **Hevy API key**: Required for all operations (available with Hevy PRO).
|
|
55
58
|
|
|
56
59
|
---
|
|
@@ -70,25 +73,6 @@ HEVY_API_KEY=your_hevy_api_key_here npx -y hevy-mcp
|
|
|
70
73
|
HEVY_API_KEY=your_hevy_api_key_here bunx hevy-mcp@latest
|
|
71
74
|
```
|
|
72
75
|
|
|
73
|
-
### Prerelease channel
|
|
74
|
-
|
|
75
|
-
Install `hevy-mcp@beta` to opt into changes merged to `main` that have not yet
|
|
76
|
-
reached a stable release:
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
npm install hevy-mcp@beta
|
|
80
|
-
|
|
81
|
-
# npx launcher
|
|
82
|
-
npx -y hevy-mcp@beta
|
|
83
|
-
|
|
84
|
-
# bun launcher
|
|
85
|
-
bunx hevy-mcp@beta
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Beta builds may be less stable and can change before their stable release.
|
|
89
|
-
`npm install hevy-mcp@latest` remains the stable channel. To switch back from
|
|
90
|
-
beta, install `hevy-mcp@latest` again.
|
|
91
|
-
|
|
92
76
|
### Manual Installation
|
|
93
77
|
|
|
94
78
|
```bash
|
|
@@ -104,6 +88,32 @@ cp .env.sample .env
|
|
|
104
88
|
# Edit .env and add your HEVY_API_KEY
|
|
105
89
|
```
|
|
106
90
|
|
|
91
|
+
### Run with Docker
|
|
92
|
+
|
|
93
|
+
Official multi-platform images are published to GitHub Container Registry for
|
|
94
|
+
`linux/amd64` and `linux/arm64`:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
export HEVY_API_KEY=your_hevy_api_key_here
|
|
98
|
+
docker run -i --rm -e HEVY_API_KEY ghcr.io/chrisdoc/hevy-mcp:latest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The server uses stdio, so `-i` keeps standard input open for the MCP client.
|
|
102
|
+
`--rm` removes the stopped container automatically. The `-e HEVY_API_KEY`
|
|
103
|
+
form forwards the variable from the host environment without putting the key
|
|
104
|
+
in the command arguments.
|
|
105
|
+
|
|
106
|
+
The image uses the official Node.js LTS Alpine base, runs as the non-root
|
|
107
|
+
`node` user, and ships the application and its third-party runtime dependencies
|
|
108
|
+
as a standalone bundle. It does not include an application `/app/node_modules`
|
|
109
|
+
directory; the official Node base image may still contain its own globally
|
|
110
|
+
packaged npm or Corepack files.
|
|
111
|
+
|
|
112
|
+
Use `latest` to follow the newest stable release. For reproducible deployments,
|
|
113
|
+
pin the exact version shown on the release, using a tag such as
|
|
114
|
+
`ghcr.io/chrisdoc/hevy-mcp:X.Y.Z`. Major (`:X`) and major.minor (`:X.Y`) tags
|
|
115
|
+
are also published for controlled automatic updates.
|
|
116
|
+
|
|
107
117
|
---
|
|
108
118
|
|
|
109
119
|
## 🔗 Integration
|
|
@@ -138,6 +148,41 @@ If you prefer Bun, swap the launcher fields:
|
|
|
138
148
|
}
|
|
139
149
|
```
|
|
140
150
|
|
|
151
|
+
To run Claude Desktop through Docker instead, first create an environment file
|
|
152
|
+
outside the repository containing your real key:
|
|
153
|
+
|
|
154
|
+
```dotenv
|
|
155
|
+
HEVY_API_KEY=replace_with_your_real_key
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Restrict access to that file where supported (for example,
|
|
159
|
+
`chmod 600 /absolute/path/to/hevy-mcp.env`), then use its absolute path in the
|
|
160
|
+
Claude Desktop configuration:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"mcpServers": {
|
|
165
|
+
"hevy-mcp": {
|
|
166
|
+
"command": "docker",
|
|
167
|
+
"args": [
|
|
168
|
+
"run",
|
|
169
|
+
"-i",
|
|
170
|
+
"--rm",
|
|
171
|
+
"--env-file",
|
|
172
|
+
"/absolute/path/to/hevy-mcp.env",
|
|
173
|
+
"ghcr.io/chrisdoc/hevy-mcp:latest"
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
This configuration runs the same stdio server inside the container; it does
|
|
181
|
+
not expose an HTTP port or start a detached service. Docker reads the key from
|
|
182
|
+
the environment file, so the Claude configuration does not replace an
|
|
183
|
+
inherited key with a placeholder. Replace `latest` with an exact version tag if
|
|
184
|
+
you want Claude Desktop to stay on a pinned release.
|
|
185
|
+
|
|
141
186
|
### Cursor Configuration
|
|
142
187
|
|
|
143
188
|
Add this server under `"mcpServers"` in `~/.cursor/mcp.json`:
|
|
@@ -191,10 +236,6 @@ This bootstraps the `hevy-mcp` entry in your client config without manual JSON e
|
|
|
191
236
|
Supply your Hevy API key via the `HEVY_API_KEY` environment variable (in
|
|
192
237
|
`.env` or system environment).
|
|
193
238
|
|
|
194
|
-
> ⚠️ CLI API key arguments (`--hevy-api-key=...`, `--hevyApiKey=...`,
|
|
195
|
-
> `hevy-api-key=...`) are still accepted for backward compatibility, but are
|
|
196
|
-
> deprecated and insecure. Use `HEVY_API_KEY` instead.
|
|
197
|
-
|
|
198
239
|
Set `HEVY_MCP_API_TIMEOUT` to override the default 30-second Hevy API request
|
|
199
240
|
timeout. Its value is in milliseconds.
|
|
200
241
|
|
|
@@ -241,10 +282,11 @@ paging behavior explicit and avoid cross-page invalidation complexity.
|
|
|
241
282
|
<details>
|
|
242
283
|
<summary><strong>⚠️ Migration Note (v1.18.0)</strong></summary>
|
|
243
284
|
|
|
244
|
-
As of **v1.18.0**, `hevy-mcp` removed
|
|
245
|
-
support.
|
|
285
|
+
As of **v1.18.0**, `hevy-mcp` removed HTTP/SSE transport and its previous
|
|
286
|
+
Docker packaging. Docker support is now available again for the stdio server.
|
|
246
287
|
|
|
247
|
-
|
|
288
|
+
Both `npx hevy-mcp` and the official container image use stdio; HTTP ports and
|
|
289
|
+
detached-container deployment are not supported.
|
|
248
290
|
|
|
249
291
|
</details>
|
|
250
292
|
|
|
@@ -297,9 +339,11 @@ Compatibility note: with MCP SDK v1.29.0, clients using the default must send
|
|
|
297
339
|
- **Build**: `npm run build`
|
|
298
340
|
- **Lint/Format**: `npm run check` (uses oxlint/oxfmt)
|
|
299
341
|
- **Type Check**: `npm run check:types`
|
|
300
|
-
- **Unit Tests**: `
|
|
301
|
-
- **Full
|
|
342
|
+
- **Unit Tests**: `npm run test:unit`
|
|
343
|
+
- **Full Vitest Discovery**: `npm test` (builds first; live tests skip when
|
|
344
|
+
`HEVY_API_KEY` is absent)
|
|
302
345
|
- **Changeset Check**: `npm run check:changeset`
|
|
346
|
+
- **Tool Token Cost**: `npm run measure:tokens` ([measurement guide](./docs/token-cost-tracking.md))
|
|
303
347
|
|
|
304
348
|
For a detailed senior engineer guide, please refer to [AGENTS.md](./AGENTS.md).
|
|
305
349
|
|
package/dist/cli.mjs
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@3.0.0" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
9
|
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "03cf150f-6466-4ca3-9cf1-e423cc0e3cd2", e._sentryDebugIdIdentifier = "sentry-dbid-03cf150f-6466-4ca3-9cf1-e423cc0e3cd2");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { r as runServer } from "./src-
|
|
12
|
+
import { r as runServer } from "./src-BBzEvkOX.mjs";
|
|
13
13
|
//#region src/cli.ts
|
|
14
14
|
runServer().catch((error) => {
|
|
15
15
|
console.error("Fatal error in main():", error);
|
package/dist/index.d.mts
CHANGED
|
@@ -14,7 +14,7 @@ declare function createServer({
|
|
|
14
14
|
config
|
|
15
15
|
}: {
|
|
16
16
|
config: ServerConfig;
|
|
17
|
-
}): McpServer
|
|
17
|
+
}): Promise<McpServer>;
|
|
18
18
|
declare function runServer(): Promise<void>;
|
|
19
19
|
//#endregion
|
|
20
20
|
export { configSchema, createServer, createServer as default, runServer };
|
package/dist/index.mjs
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
try {
|
|
6
6
|
var e = "undefined" != typeof window ? window : "undefined" != typeof global ? global : "undefined" != typeof globalThis ? globalThis : "undefined" != typeof self ? self : {};
|
|
7
|
-
e.SENTRY_RELEASE = { id: "hevy-mcp@
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@3.0.0" };
|
|
8
8
|
var n = new e.Error().stack;
|
|
9
9
|
n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "5b077b94-9ef8-4f24-a0b0-8dcfbf491be1", e._sentryDebugIdIdentifier = "sentry-dbid-5b077b94-9ef8-4f24-a0b0-8dcfbf491be1");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { n as createServer, r as runServer, t as configSchema } from "./src-
|
|
12
|
+
import { n as createServer, r as runServer, t as configSchema } from "./src-BBzEvkOX.mjs";
|
|
13
13
|
export { configSchema, createServer, createServer as default, runServer };
|
|
@@ -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@3.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] = "06533ca8-0b74-40dd-a418-fc98c764276c", e._sentryDebugIdIdentifier = "sentry-dbid-06533ca8-0b74-40dd-a418-fc98c764276c");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
12
|
import * as Sentry from "@sentry/node";
|
|
@@ -41,10 +41,10 @@ import semver from "semver";
|
|
|
41
41
|
* OTel Collector → Honeycomb: performance traces, metrics
|
|
42
42
|
*/
|
|
43
43
|
const name$1 = "hevy-mcp";
|
|
44
|
-
const version$1 = "
|
|
44
|
+
const version$1 = "3.0.0";
|
|
45
45
|
const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
|
|
46
46
|
const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
|
|
47
|
-
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@
|
|
47
|
+
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@3.0.0`;
|
|
48
48
|
const resource = resourceFromAttributes({
|
|
49
49
|
"service.name": name$1,
|
|
50
50
|
"service.version": version$1
|
|
@@ -2110,43 +2110,20 @@ function registerWorkoutTools(server, hevyClient) {
|
|
|
2110
2110
|
}, "update-workout-operation"));
|
|
2111
2111
|
}
|
|
2112
2112
|
//#endregion
|
|
2113
|
+
//#region src/tools/register.ts
|
|
2114
|
+
/** Register every Hevy tool in its production ordering. */
|
|
2115
|
+
function registerHevyTools(server, hevyClient, options = {}) {
|
|
2116
|
+
registerWorkoutTools(server, hevyClient);
|
|
2117
|
+
registerRoutineTools(server, hevyClient);
|
|
2118
|
+
registerTemplateTools(server, hevyClient, { logger: options.logger });
|
|
2119
|
+
registerFolderTools(server, hevyClient);
|
|
2120
|
+
registerBodyMeasurementTools(server, hevyClient);
|
|
2121
|
+
registerUserTools(server, hevyClient);
|
|
2122
|
+
}
|
|
2123
|
+
//#endregion
|
|
2113
2124
|
//#region src/utils/config.ts
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
"(--hevy-api-key=..., --hevyApiKey=..., hevy-api-key=...) is",
|
|
2117
|
-
"deprecated and insecure. Use the HEVY_API_KEY environment",
|
|
2118
|
-
"variable instead."
|
|
2119
|
-
].join(" ");
|
|
2120
|
-
/**
|
|
2121
|
-
* Parse CLI arguments and environment to derive configuration.
|
|
2122
|
-
* Priority order for API key: deprecated CLI flag forms > environment variable.
|
|
2123
|
-
* Supported deprecated CLI arg forms:
|
|
2124
|
-
* --hevy-api-key=KEY
|
|
2125
|
-
* --hevyApiKey=KEY
|
|
2126
|
-
* hevy-api-key=KEY (bare, e.g. when passed after npm start -- )
|
|
2127
|
-
*/
|
|
2128
|
-
function parseConfig(argv, env) {
|
|
2129
|
-
let apiKey = "";
|
|
2130
|
-
let usedDeprecatedApiKeyArg = false;
|
|
2131
|
-
const apiKeyArgPatterns = [
|
|
2132
|
-
/^--hevy-api-key=(.+)$/i,
|
|
2133
|
-
/^--hevyApiKey=(.+)$/i,
|
|
2134
|
-
/^hevy-api-key=(.+)$/i
|
|
2135
|
-
];
|
|
2136
|
-
for (const raw of argv) {
|
|
2137
|
-
for (const pattern of apiKeyArgPatterns) {
|
|
2138
|
-
const m = raw.match(pattern);
|
|
2139
|
-
if (m) {
|
|
2140
|
-
apiKey = m[1];
|
|
2141
|
-
usedDeprecatedApiKeyArg = true;
|
|
2142
|
-
break;
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
if (apiKey) break;
|
|
2146
|
-
}
|
|
2147
|
-
if (usedDeprecatedApiKeyArg) console.error(DEPRECATED_CLI_ARGUMENT_WARNING);
|
|
2148
|
-
if (!apiKey) apiKey = env.HEVY_API_KEY || "";
|
|
2149
|
-
return { apiKey };
|
|
2125
|
+
function parseConfig(env) {
|
|
2126
|
+
return { apiKey: env.HEVY_API_KEY || "" };
|
|
2150
2127
|
}
|
|
2151
2128
|
function assertApiKey(apiKey) {
|
|
2152
2129
|
if (!apiKey) {
|
|
@@ -2871,6 +2848,15 @@ function getApiTimeoutMs() {
|
|
|
2871
2848
|
if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_API_TIMEOUT_MS;
|
|
2872
2849
|
return Math.trunc(parsed);
|
|
2873
2850
|
}
|
|
2851
|
+
function normalizeMaxGetRetries(value) {
|
|
2852
|
+
if (value === void 0 || !Number.isFinite(value) || value < 0) return 3;
|
|
2853
|
+
return Math.floor(value);
|
|
2854
|
+
}
|
|
2855
|
+
function normalizeTimeoutMs(value) {
|
|
2856
|
+
if (value === void 0 || !Number.isFinite(value) || value <= 0) return getApiTimeoutMs();
|
|
2857
|
+
const normalizedValue = Math.floor(value);
|
|
2858
|
+
return normalizedValue > 0 ? normalizedValue : getApiTimeoutMs();
|
|
2859
|
+
}
|
|
2874
2860
|
function sleep(ms) {
|
|
2875
2861
|
return new Promise((resolve) => {
|
|
2876
2862
|
setTimeout(resolve, ms);
|
|
@@ -2958,7 +2944,7 @@ function markRetryExhausted(error, retryCount) {
|
|
|
2958
2944
|
if (retryError.code) retryError.hevyRetryOriginalCode = retryError.code;
|
|
2959
2945
|
retryError.code = HEVY_RETRY_EXHAUSTED_ERROR_CODE;
|
|
2960
2946
|
}
|
|
2961
|
-
async function requestWithRetries(axiosInstance, config, logger) {
|
|
2947
|
+
async function requestWithRetries(axiosInstance, config, maxGetRetries, logger) {
|
|
2962
2948
|
let retryCount = 0;
|
|
2963
2949
|
while (true) try {
|
|
2964
2950
|
return await axiosInstance.request(config);
|
|
@@ -2980,7 +2966,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
|
|
|
2980
2966
|
});
|
|
2981
2967
|
throw error;
|
|
2982
2968
|
}
|
|
2983
|
-
if (retryCount >=
|
|
2969
|
+
if (retryCount >= maxGetRetries) {
|
|
2984
2970
|
emitClientLog(logger, {
|
|
2985
2971
|
level: status === 429 ? "warning" : "error",
|
|
2986
2972
|
logger: "hevy-api",
|
|
@@ -2988,7 +2974,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
|
|
|
2988
2974
|
message: "Hevy API request failed after retries",
|
|
2989
2975
|
status,
|
|
2990
2976
|
attempt: retryCount + 1,
|
|
2991
|
-
maxAttempts:
|
|
2977
|
+
maxAttempts: maxGetRetries + 1,
|
|
2992
2978
|
method,
|
|
2993
2979
|
endpoint
|
|
2994
2980
|
}
|
|
@@ -3006,7 +2992,7 @@ async function requestWithRetries(axiosInstance, config, logger) {
|
|
|
3006
2992
|
message: status === 429 ? "Hevy API rate limit; retrying request" : "Retrying Hevy API request",
|
|
3007
2993
|
status,
|
|
3008
2994
|
attempt: retryCount + 1,
|
|
3009
|
-
maxAttempts:
|
|
2995
|
+
maxAttempts: maxGetRetries + 1,
|
|
3010
2996
|
delayMs: retryDelayMs,
|
|
3011
2997
|
...status === 429 ? { retryAfterMs: retryAfterMs ?? null } : {},
|
|
3012
2998
|
method,
|
|
@@ -3016,10 +3002,10 @@ async function requestWithRetries(axiosInstance, config, logger) {
|
|
|
3016
3002
|
await sleep(retryDelayMs);
|
|
3017
3003
|
}
|
|
3018
3004
|
}
|
|
3019
|
-
function createResilientClient(axiosInstance, logger) {
|
|
3005
|
+
function createResilientClient(axiosInstance, maxGetRetries, logger) {
|
|
3020
3006
|
let clientConfig = { baseURL: axiosInstance.defaults.baseURL };
|
|
3021
3007
|
const resilientClient = (async (config) => {
|
|
3022
|
-
return requestWithRetries(axiosInstance, config, logger);
|
|
3008
|
+
return requestWithRetries(axiosInstance, config, maxGetRetries, logger);
|
|
3023
3009
|
});
|
|
3024
3010
|
resilientClient.getConfig = () => ({ ...clientConfig });
|
|
3025
3011
|
resilientClient.setConfig = (config) => {
|
|
@@ -3075,9 +3061,11 @@ function finalizeRequestTrace(opts) {
|
|
|
3075
3061
|
}
|
|
3076
3062
|
function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {}) {
|
|
3077
3063
|
const { logger } = options;
|
|
3064
|
+
const maxGetRetries = normalizeMaxGetRetries(options.maxGetRetries);
|
|
3065
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs);
|
|
3078
3066
|
const axiosInstance = axios.create({
|
|
3079
3067
|
baseURL: baseUrl,
|
|
3080
|
-
timeout:
|
|
3068
|
+
timeout: timeoutMs,
|
|
3081
3069
|
headers: { "api-key": apiKey }
|
|
3082
3070
|
});
|
|
3083
3071
|
axiosInstance.interceptors.request.use((config) => {
|
|
@@ -3085,11 +3073,13 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
|
|
|
3085
3073
|
const method = (config.method ?? "get").toUpperCase();
|
|
3086
3074
|
const url = config.url ?? "";
|
|
3087
3075
|
const endpoint = url.split("?")[0] ?? url;
|
|
3076
|
+
const userId = getCurrentUserId();
|
|
3088
3077
|
tracedConfig._span = tracer.startSpan(`hevy.api.${method}`, { attributes: {
|
|
3089
3078
|
"http.method": method,
|
|
3090
3079
|
"http.url": url,
|
|
3091
3080
|
"http.base_url": config.baseURL ?? "",
|
|
3092
|
-
"hevy.api.endpoint": endpoint
|
|
3081
|
+
"hevy.api.endpoint": endpoint,
|
|
3082
|
+
...userId ? { "user.id": userId } : {}
|
|
3093
3083
|
} });
|
|
3094
3084
|
tracedConfig._startTime = Date.now();
|
|
3095
3085
|
return config;
|
|
@@ -3141,7 +3131,7 @@ function createClient$1(apiKey, baseUrl = "https://api.hevyapp.com", options = {
|
|
|
3141
3131
|
throw error;
|
|
3142
3132
|
});
|
|
3143
3133
|
const headers = { "api-key": apiKey };
|
|
3144
|
-
const client = createResilientClient(axiosInstance, logger);
|
|
3134
|
+
const client = createResilientClient(axiosInstance, maxGetRetries, logger);
|
|
3145
3135
|
return {
|
|
3146
3136
|
getWorkouts: (params) => wrapApi(getV1Workouts)(headers, params, { client }),
|
|
3147
3137
|
getWorkout: (workoutId) => wrapApi(getV1WorkoutsWorkoutid)(workoutId, headers, { client }),
|
|
@@ -3542,16 +3532,13 @@ const HELP_TEXT = [
|
|
|
3542
3532
|
"Options:",
|
|
3543
3533
|
" -h, --help Show this help message and exit",
|
|
3544
3534
|
" -v, --version Show version and exit",
|
|
3545
|
-
" --hevy-api-key=<api-key> (deprecated, use HEVY_API_KEY env var)",
|
|
3546
3535
|
"",
|
|
3547
3536
|
"Environment:",
|
|
3548
3537
|
" HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
|
|
3549
3538
|
" HEVY_MCP_DEBUG=1 Enable verbose diagnostics on stderr",
|
|
3550
3539
|
"",
|
|
3551
3540
|
"Examples:",
|
|
3552
|
-
" HEVY_API_KEY=your-key npx hevy-mcp"
|
|
3553
|
-
" npx hevy-mcp --hevy-api-key=your-key",
|
|
3554
|
-
" npm start -- --hevy-api-key=your-key"
|
|
3541
|
+
" HEVY_API_KEY=your-key npx hevy-mcp"
|
|
3555
3542
|
].join("\n");
|
|
3556
3543
|
function getCliAction(args) {
|
|
3557
3544
|
for (const arg of args) {
|
|
@@ -3561,6 +3548,21 @@ function getCliAction(args) {
|
|
|
3561
3548
|
return "start";
|
|
3562
3549
|
}
|
|
3563
3550
|
const HEVY_API_BASEURL = "https://api.hevyapp.com";
|
|
3551
|
+
const STARTUP_PROBE_TIMEOUT_MS = 5e3;
|
|
3552
|
+
const INVALID_API_KEY_MESSAGE = "HEVY_API_KEY is invalid or expired. Please check your API key in the Hevy app under Settings > API Key.";
|
|
3553
|
+
const API_KEY_VALIDATION_WARNING = "Warning: HEVY_API_KEY could not be validated during startup. Startup will continue; check your network connection and Hevy API availability.";
|
|
3554
|
+
const SAFE_NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3555
|
+
"EAI_AGAIN",
|
|
3556
|
+
"ECONNABORTED",
|
|
3557
|
+
"ECONNREFUSED",
|
|
3558
|
+
"ECONNRESET",
|
|
3559
|
+
"ENETUNREACH",
|
|
3560
|
+
"ENOTFOUND",
|
|
3561
|
+
"ERR_NETWORK",
|
|
3562
|
+
"ERR_SOCKET_TIMEOUT",
|
|
3563
|
+
"ETIMEDOUT",
|
|
3564
|
+
"HEVY_RETRY_EXHAUSTED"
|
|
3565
|
+
]);
|
|
3564
3566
|
const SENTRY_USER_ID_CONTEXT = "hevy-mcp:sentry-user-id:v1";
|
|
3565
3567
|
function fingerprintApiKey(apiKey) {
|
|
3566
3568
|
return createHmac("sha256", apiKey).update(SENTRY_USER_ID_CONTEXT).digest("hex").slice(0, 10);
|
|
@@ -3589,6 +3591,33 @@ function createToolCountingServer(server) {
|
|
|
3589
3591
|
getCount: () => count
|
|
3590
3592
|
};
|
|
3591
3593
|
}
|
|
3594
|
+
function getHttpStatus(error) {
|
|
3595
|
+
if (!error || typeof error !== "object" || !("response" in error)) return;
|
|
3596
|
+
const response = error.response;
|
|
3597
|
+
if (!response || typeof response !== "object" || !("status" in response)) return;
|
|
3598
|
+
return typeof response.status === "number" && Number.isInteger(response.status) && response.status >= 100 && response.status <= 599 ? response.status : void 0;
|
|
3599
|
+
}
|
|
3600
|
+
function getSafeValidationDiagnostic(error) {
|
|
3601
|
+
const status = getHttpStatus(error);
|
|
3602
|
+
if (status !== void 0) return `HTTP ${status}`;
|
|
3603
|
+
if (!error || typeof error !== "object" || !("code" in error)) return;
|
|
3604
|
+
const code = error.code;
|
|
3605
|
+
return typeof code === "string" && SAFE_NETWORK_ERROR_CODES.has(code) ? code : void 0;
|
|
3606
|
+
}
|
|
3607
|
+
async function validateApiKey(apiKey) {
|
|
3608
|
+
const startupProbeClient = createClient(apiKey, HEVY_API_BASEURL, {
|
|
3609
|
+
maxGetRetries: 0,
|
|
3610
|
+
timeoutMs: STARTUP_PROBE_TIMEOUT_MS
|
|
3611
|
+
});
|
|
3612
|
+
try {
|
|
3613
|
+
await startupProbeClient.getUserInfo();
|
|
3614
|
+
} catch (error) {
|
|
3615
|
+
const status = getHttpStatus(error);
|
|
3616
|
+
if (status === 401 || status === 403) throw new Error(INVALID_API_KEY_MESSAGE);
|
|
3617
|
+
const diagnostic = getSafeValidationDiagnostic(error);
|
|
3618
|
+
console.error(diagnostic ? `${API_KEY_VALIDATION_WARNING} Diagnostic: ${diagnostic}.` : API_KEY_VALIDATION_WARNING);
|
|
3619
|
+
}
|
|
3620
|
+
}
|
|
3592
3621
|
function buildServer(apiKey) {
|
|
3593
3622
|
const userId = fingerprintApiKey(apiKey);
|
|
3594
3623
|
return tracer.startActiveSpan("mcp.server.build", { attributes: {
|
|
@@ -3620,12 +3649,7 @@ function buildServer(apiKey) {
|
|
|
3620
3649
|
tracer.startActiveSpan("mcp.tools.register", (toolsSpan) => {
|
|
3621
3650
|
try {
|
|
3622
3651
|
const counting = createToolCountingServer(server);
|
|
3623
|
-
|
|
3624
|
-
registerRoutineTools(counting.server, hevyClient);
|
|
3625
|
-
registerTemplateTools(counting.server, hevyClient, { logger: clientLogger });
|
|
3626
|
-
registerFolderTools(counting.server, hevyClient);
|
|
3627
|
-
registerBodyMeasurementTools(counting.server, hevyClient);
|
|
3628
|
-
registerUserTools(counting.server, hevyClient);
|
|
3652
|
+
registerHevyTools(counting.server, hevyClient, { logger: clientLogger });
|
|
3629
3653
|
toolsSpan.setAttribute("mcp.tools.count", counting.getCount());
|
|
3630
3654
|
} finally {
|
|
3631
3655
|
toolsSpan.end();
|
|
@@ -3649,13 +3673,13 @@ function buildServer(apiKey) {
|
|
|
3649
3673
|
}
|
|
3650
3674
|
});
|
|
3651
3675
|
}
|
|
3652
|
-
function createServer({ config }) {
|
|
3676
|
+
async function createServer({ config }) {
|
|
3653
3677
|
const { apiKey } = serverConfigSchema.parse(config);
|
|
3678
|
+
await validateApiKey(apiKey);
|
|
3654
3679
|
return buildServer(apiKey);
|
|
3655
3680
|
}
|
|
3656
3681
|
async function runServer() {
|
|
3657
|
-
const
|
|
3658
|
-
const cliAction = getCliAction(args);
|
|
3682
|
+
const cliAction = getCliAction(process.argv.slice(2));
|
|
3659
3683
|
if (cliAction === "version") {
|
|
3660
3684
|
console.error(`${name} v${version}`);
|
|
3661
3685
|
return;
|
|
@@ -3667,9 +3691,9 @@ async function runServer() {
|
|
|
3667
3691
|
serverStartups.add(1, { version });
|
|
3668
3692
|
await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
|
|
3669
3693
|
try {
|
|
3670
|
-
const apiKey = parseConfig(
|
|
3694
|
+
const apiKey = parseConfig(process.env).apiKey;
|
|
3671
3695
|
assertApiKey(apiKey);
|
|
3672
|
-
const server =
|
|
3696
|
+
const server = await createServer({ config: { apiKey } });
|
|
3673
3697
|
console.error("Starting MCP server in stdio mode");
|
|
3674
3698
|
const transport = createInstrumentedStdioTransport(new StdioServerTransport());
|
|
3675
3699
|
await tracer.startActiveSpan("mcp.server.connect", { attributes: { "mcp.transport": "stdio" } }, async (connectSpan) => {
|
|
@@ -3700,4 +3724,4 @@ async function runServer() {
|
|
|
3700
3724
|
//#endregion
|
|
3701
3725
|
export { createServer as n, runServer as r, configSchema as t };
|
|
3702
3726
|
|
|
3703
|
-
//# sourceMappingURL=src-
|
|
3727
|
+
//# sourceMappingURL=src-BBzEvkOX.mjs.map
|