hevy-mcp 4.0.1 → 4.1.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 +33 -14
- package/dist/cli.mjs +4 -4
- package/dist/index.d.mts +6 -2
- package/dist/index.mjs +4 -4
- package/dist/{src-D0fOBh7M.mjs → src-wSWxe4LJ.mjs} +506 -106
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -414,20 +414,39 @@ self-hosted Streamable HTTP.
|
|
|
414
414
|
|
|
415
415
|
## Advanced configuration
|
|
416
416
|
|
|
417
|
-
| Setting
|
|
418
|
-
|
|
|
419
|
-
| `HEVY_API_KEY`
|
|
420
|
-
| `HEVY_MCP_API_TIMEOUT`
|
|
421
|
-
| `HEVY_MCP_DEBUG`
|
|
422
|
-
| `
|
|
423
|
-
| `
|
|
424
|
-
| `
|
|
425
|
-
|
|
|
426
|
-
| `-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
417
|
+
| Setting | Default | Scope | Notes |
|
|
418
|
+
| ---------------------------- | ------------------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
419
|
+
| `HEVY_API_KEY` | None; required | Local stdio or HTTP | Hevy API key from the Hevy app. Never pass it in a URL. |
|
|
420
|
+
| `HEVY_MCP_API_TIMEOUT` | `30000` ms | Local stdio | Positive Hevy API timeout in milliseconds. Invalid values fall back to 30 seconds. |
|
|
421
|
+
| `HEVY_MCP_DEBUG` | Disabled | Local Node | Set to exactly `1` for privacy-bounded diagnostics on stderr. Stdout remains reserved for MCP JSON-RPC. |
|
|
422
|
+
| `HEVY_MCP_HTTP_BEARER_TOKEN` | None | Non-loopback HTTP | Required when `--host` is not loopback; use a separate token, never the Hevy API key. |
|
|
423
|
+
| `XDG_CACHE_HOME` | `~/.cache` | Local stdio | Changes the root for the npm update-check cache at `hevy-mcp/update-check.json`. |
|
|
424
|
+
| `SENTRY_DSN` | Packaged project DSN | Optional local Node telemetry | Overrides the Sentry destination. An empty value disables Sentry export. The Worker does not import Node telemetry. |
|
|
425
|
+
| `SENTRY_RELEASE` | `hevy-mcp@<installed-version>` | Optional local Node telemetry | Overrides the release label attached to local Sentry events and traces. |
|
|
426
|
+
| `-h`, `--help` | N/A | Local stdio CLI | Print supported options and exit. |
|
|
427
|
+
| `-v`, `--version` | N/A | Local stdio CLI | Print the installed version and exit. |
|
|
428
|
+
|
|
429
|
+
The local executable uses stdio by default. To opt into Streamable HTTP, run:
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
HEVY_API_KEY=your-hevy-api-key npx hevy-mcp --transport http --host 127.0.0.1 --port 3000
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
The MCP endpoint is `http://127.0.0.1:3000/mcp`. For a specific bind host,
|
|
436
|
+
HTTP mode validates the Host header and configured port to protect against DNS
|
|
437
|
+
rebinding. Loopback is the default. Wildcard binds (`0.0.0.0` or `::`) accept
|
|
438
|
+
any hostname so they can be used behind Docker port mappings or a reverse
|
|
439
|
+
proxy; they require `HEVY_MCP_HTTP_BEARER_TOKEN` and rely on that separate
|
|
440
|
+
authentication token. Do not expose an unprotected shared Hevy account to the
|
|
441
|
+
public internet. For Docker HTTP mode, publish the port explicitly:
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
docker run --rm -p 3000:3000 -e HEVY_API_KEY -e HEVY_MCP_HTTP_BEARER_TOKEN \
|
|
445
|
+
ghcr.io/chrisdoc/hevy-mcp:latest --transport http --host 0.0.0.0 --port 3000
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
Wildcard binds are allowed only with the separate bearer token; publish the
|
|
449
|
+
container port deliberately and keep that token private.
|
|
431
450
|
|
|
432
451
|
### Cache behavior
|
|
433
452
|
|
package/dist/cli.mjs
CHANGED
|
@@ -4,14 +4,14 @@
|
|
|
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@4.0
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@4.1.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] = "625c4c9c-e20d-40b3-96b1-4e799ef130f3", e._sentryDebugIdIdentifier = "sentry-dbid-625c4c9c-e20d-40b3-96b1-4e799ef130f3");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { a as
|
|
12
|
+
import { a as createSafeErrorDiagnostic, i as MissingHevyApiKeyError, n as runServer, o as flushTelemetry } from "./src-wSWxe4LJ.mjs";
|
|
13
13
|
//#region src/cli.ts
|
|
14
|
-
|
|
14
|
+
runServer().catch(async (error) => {
|
|
15
15
|
if (error instanceof MissingHevyApiKeyError) console.error(error.message);
|
|
16
16
|
else console.error("Fatal error in main()", createSafeErrorDiagnostic(error));
|
|
17
17
|
try {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
|
|
2
|
+
//#region src/utils/arguments.d.ts
|
|
3
|
+
type NodeTransport = "stdio" | "http";
|
|
4
|
+
//#endregion
|
|
2
5
|
//#region src/index.d.ts
|
|
3
6
|
declare function createNodeMcpServer({ apiKey }: {
|
|
4
7
|
apiKey: string;
|
|
5
|
-
}): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
|
|
8
|
+
}, transport?: NodeTransport): Promise<import("@modelcontextprotocol/sdk/server/mcp").McpServer>;
|
|
6
9
|
declare function runStdioServer(): Promise<void>;
|
|
10
|
+
declare function runServer(): Promise<void>;
|
|
7
11
|
//#endregion
|
|
8
|
-
export { createNodeMcpServer, runStdioServer };
|
|
12
|
+
export { createNodeMcpServer, runServer, runStdioServer };
|
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@4.0
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@4.1.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] = "e9496b31-626b-4ad1-9416-47ba4ffd2995", e._sentryDebugIdIdentifier = "sentry-dbid-e9496b31-626b-4ad1-9416-47ba4ffd2995");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
|
-
import { n as runStdioServer, t as createNodeMcpServer } from "./src-
|
|
13
|
-
export { createNodeMcpServer, runStdioServer };
|
|
12
|
+
import { n as runServer, r as runStdioServer, t as createNodeMcpServer } from "./src-wSWxe4LJ.mjs";
|
|
13
|
+
export { createNodeMcpServer, runServer, runStdioServer };
|
|
@@ -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@4.0
|
|
7
|
+
e.SENTRY_RELEASE = { id: "hevy-mcp@4.1.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] = "4b988c15-eb76-40ac-8399-033bebec17f8", e._sentryDebugIdIdentifier = "sentry-dbid-4b988c15-eb76-40ac-8399-033bebec17f8");
|
|
10
10
|
} catch (e) {}
|
|
11
11
|
})();
|
|
12
12
|
import * as Sentry from "@sentry/node";
|
|
@@ -18,11 +18,15 @@ import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
|
18
18
|
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
19
19
|
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
20
20
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
21
|
-
import { createHmac, randomUUID } from "node:crypto";
|
|
21
|
+
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
22
22
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
23
23
|
import { z } from "zod";
|
|
24
24
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
25
25
|
import { z as z$1 } from "zod/v4";
|
|
26
|
+
import { isIP } from "node:net";
|
|
27
|
+
import { createServer } from "node:http";
|
|
28
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
29
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
26
30
|
import { deserializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js";
|
|
27
31
|
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
28
32
|
import { homedir } from "node:os";
|
|
@@ -77,10 +81,10 @@ function sanitizeSentryMcpSpan(span) {
|
|
|
77
81
|
* OTel Collector → Honeycomb: performance traces, metrics
|
|
78
82
|
*/
|
|
79
83
|
const name$1 = "hevy-mcp";
|
|
80
|
-
const version$1 = "4.0
|
|
84
|
+
const version$1 = "4.1.0";
|
|
81
85
|
const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
|
|
82
86
|
const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
|
|
83
|
-
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@4.0
|
|
87
|
+
const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@4.1.0`;
|
|
84
88
|
const resource = resourceFromAttributes({
|
|
85
89
|
"service.name": name$1,
|
|
86
90
|
"service.version": version$1
|
|
@@ -2582,7 +2586,7 @@ function registerHevyResources(server, runtime) {
|
|
|
2582
2586
|
//#endregion
|
|
2583
2587
|
//#region ../core/src/server-metadata.ts
|
|
2584
2588
|
const SERVER_NAME = "hevy-mcp";
|
|
2585
|
-
const SERVER_VERSION = "4.0
|
|
2589
|
+
const SERVER_VERSION = "4.1.0";
|
|
2586
2590
|
const SERVER_INSTRUCTIONS = [
|
|
2587
2591
|
"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.",
|
|
2588
2592
|
"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.",
|
|
@@ -4432,6 +4436,444 @@ function assertApiKey(apiKey) {
|
|
|
4432
4436
|
if (!apiKey) throw new MissingHevyApiKeyError();
|
|
4433
4437
|
}
|
|
4434
4438
|
//#endregion
|
|
4439
|
+
//#region src/utils/arguments.ts
|
|
4440
|
+
const DEFAULT_HOST = "127.0.0.1";
|
|
4441
|
+
const DEFAULT_PORT = 3e3;
|
|
4442
|
+
function valueAfter(args, index, option) {
|
|
4443
|
+
const value = args[index + 1];
|
|
4444
|
+
if (!value || value.startsWith("-")) throw new Error(`${option} requires a value.`);
|
|
4445
|
+
return value;
|
|
4446
|
+
}
|
|
4447
|
+
function parseHost(value) {
|
|
4448
|
+
const host = value.trim();
|
|
4449
|
+
const unbracketed = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
4450
|
+
const isBracketed = host.startsWith("[") || host.endsWith("]");
|
|
4451
|
+
if (host.length === 0 || isBracketed && !(host.startsWith("[") && host.endsWith("]")) || isBracketed && isIP(unbracketed) !== 6 || !isBracketed && host.includes(":") && isIP(host) !== 6 || !isBracketed && !host.includes(":") && isIP(host) === 0 && !/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/u.test(host) || /\s/u.test(host) || host.includes("/") || host.includes("@") || host.includes("://")) throw new Error(`Invalid host: ${value}. Provide a hostname or IP address.`);
|
|
4452
|
+
return unbracketed;
|
|
4453
|
+
}
|
|
4454
|
+
function parsePort(value) {
|
|
4455
|
+
if (!/^[0-9]+$/u.test(value)) throw new Error(`Invalid port: ${value}. Use an integer from 1 to 65535.`);
|
|
4456
|
+
const port = Number(value);
|
|
4457
|
+
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) throw new Error(`Invalid port: ${value}. Use an integer from 1 to 65535.`);
|
|
4458
|
+
return port;
|
|
4459
|
+
}
|
|
4460
|
+
function parseNodeCliOptions(args) {
|
|
4461
|
+
let transport = "stdio";
|
|
4462
|
+
let host = DEFAULT_HOST;
|
|
4463
|
+
let port = DEFAULT_PORT;
|
|
4464
|
+
let transportExplicit = false;
|
|
4465
|
+
let hostExplicit = false;
|
|
4466
|
+
let portExplicit = false;
|
|
4467
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
4468
|
+
const arg = args[index];
|
|
4469
|
+
if (!arg || [
|
|
4470
|
+
"--help",
|
|
4471
|
+
"-h",
|
|
4472
|
+
"--version",
|
|
4473
|
+
"-v"
|
|
4474
|
+
].includes(arg)) continue;
|
|
4475
|
+
switch (arg) {
|
|
4476
|
+
case "--transport": {
|
|
4477
|
+
const value = valueAfter(args, index, arg);
|
|
4478
|
+
index += 1;
|
|
4479
|
+
if (value !== "stdio" && value !== "http") throw new Error(`Invalid transport: ${value}. Use either stdio or http.`);
|
|
4480
|
+
transport = value;
|
|
4481
|
+
transportExplicit = true;
|
|
4482
|
+
break;
|
|
4483
|
+
}
|
|
4484
|
+
case "--host":
|
|
4485
|
+
host = parseHost(valueAfter(args, index, arg));
|
|
4486
|
+
hostExplicit = true;
|
|
4487
|
+
index += 1;
|
|
4488
|
+
break;
|
|
4489
|
+
case "--port":
|
|
4490
|
+
port = parsePort(valueAfter(args, index, arg));
|
|
4491
|
+
portExplicit = true;
|
|
4492
|
+
index += 1;
|
|
4493
|
+
break;
|
|
4494
|
+
default: throw new Error(`Unknown option: ${arg}`);
|
|
4495
|
+
}
|
|
4496
|
+
}
|
|
4497
|
+
if (transport === "stdio" && (hostExplicit || portExplicit)) throw new Error("--host and --port can only be used with --transport http.");
|
|
4498
|
+
if (transport === "stdio" && transportExplicit) return {
|
|
4499
|
+
transport,
|
|
4500
|
+
host: DEFAULT_HOST,
|
|
4501
|
+
port: DEFAULT_PORT
|
|
4502
|
+
};
|
|
4503
|
+
return {
|
|
4504
|
+
transport,
|
|
4505
|
+
host,
|
|
4506
|
+
port
|
|
4507
|
+
};
|
|
4508
|
+
}
|
|
4509
|
+
//#endregion
|
|
4510
|
+
//#region src/utils/mcp-session-observability.ts
|
|
4511
|
+
const UNKNOWN_METADATA = "unknown";
|
|
4512
|
+
const MAX_METADATA_LENGTH = 64;
|
|
4513
|
+
const SAFE_METADATA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/@-]{0,63}$/u;
|
|
4514
|
+
const contextStorage = new AsyncLocalStorage();
|
|
4515
|
+
let activeStdioSession;
|
|
4516
|
+
function isRecord(value) {
|
|
4517
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4518
|
+
}
|
|
4519
|
+
function normalizeMetadata(value) {
|
|
4520
|
+
if (typeof value !== "string") return UNKNOWN_METADATA;
|
|
4521
|
+
const normalized = value.trim();
|
|
4522
|
+
if (normalized.length === 0 || normalized.length > MAX_METADATA_LENGTH || !SAFE_METADATA_PATTERN.test(normalized)) return UNKNOWN_METADATA;
|
|
4523
|
+
return normalized;
|
|
4524
|
+
}
|
|
4525
|
+
function getInitializeParams(message) {
|
|
4526
|
+
if (!isRecord(message) || message.method !== "initialize") return {};
|
|
4527
|
+
return isRecord(message.params) ? message.params : {};
|
|
4528
|
+
}
|
|
4529
|
+
function extractMcpClientMetadata(message) {
|
|
4530
|
+
const params = getInitializeParams(message);
|
|
4531
|
+
const clientInfo = isRecord(params.clientInfo) ? params.clientInfo : {};
|
|
4532
|
+
return {
|
|
4533
|
+
name: normalizeMetadata(clientInfo.name),
|
|
4534
|
+
version: normalizeMetadata(clientInfo.version),
|
|
4535
|
+
protocolVersion: normalizeMetadata(params.protocolVersion)
|
|
4536
|
+
};
|
|
4537
|
+
}
|
|
4538
|
+
function createMcpSessionContext(message, transport = "stdio") {
|
|
4539
|
+
return {
|
|
4540
|
+
metadata: extractMcpClientMetadata(message),
|
|
4541
|
+
startedAt: Date.now(),
|
|
4542
|
+
toolCalls: 0,
|
|
4543
|
+
hadToolFailure: false,
|
|
4544
|
+
transport
|
|
4545
|
+
};
|
|
4546
|
+
}
|
|
4547
|
+
function runWithMcpSessionContext(context, callback) {
|
|
4548
|
+
return contextStorage.run(context, callback);
|
|
4549
|
+
}
|
|
4550
|
+
function getSession() {
|
|
4551
|
+
return contextStorage.getStore() ?? activeStdioSession;
|
|
4552
|
+
}
|
|
4553
|
+
function metadataAttributes(metadata, transport) {
|
|
4554
|
+
return {
|
|
4555
|
+
client_name: metadata.name,
|
|
4556
|
+
client_version: metadata.version,
|
|
4557
|
+
protocol_version: metadata.protocolVersion,
|
|
4558
|
+
transport
|
|
4559
|
+
};
|
|
4560
|
+
}
|
|
4561
|
+
function durationBucket(durationMs) {
|
|
4562
|
+
if (durationMs < 1e3) return "<1s";
|
|
4563
|
+
if (durationMs < 1e4) return "1-10s";
|
|
4564
|
+
if (durationMs < 6e4) return "10-60s";
|
|
4565
|
+
if (durationMs < 3e5) return "1-5m";
|
|
4566
|
+
return "5m+";
|
|
4567
|
+
}
|
|
4568
|
+
function recordMcpSessionStart(message, transport = "stdio", context) {
|
|
4569
|
+
const session = context ?? createMcpSessionContext(message, transport);
|
|
4570
|
+
if (session.transport === "stdio") activeStdioSession = session;
|
|
4571
|
+
const attributes = metadataAttributes(session.metadata, session.transport);
|
|
4572
|
+
sessionStarted.add(1, attributes);
|
|
4573
|
+
return session.metadata;
|
|
4574
|
+
}
|
|
4575
|
+
function recordMcpToolInvocation() {
|
|
4576
|
+
const session = getSession();
|
|
4577
|
+
if (session) session.toolCalls += 1;
|
|
4578
|
+
return metadataAttributes(session?.metadata ?? {
|
|
4579
|
+
name: UNKNOWN_METADATA,
|
|
4580
|
+
version: UNKNOWN_METADATA,
|
|
4581
|
+
protocolVersion: UNKNOWN_METADATA
|
|
4582
|
+
}, session?.transport ?? "stdio");
|
|
4583
|
+
}
|
|
4584
|
+
function recordMcpToolFailure() {
|
|
4585
|
+
const session = getSession();
|
|
4586
|
+
if (session) session.hadToolFailure = true;
|
|
4587
|
+
}
|
|
4588
|
+
function getCurrentMcpClientMetadata() {
|
|
4589
|
+
return getSession()?.metadata ?? {
|
|
4590
|
+
name: UNKNOWN_METADATA,
|
|
4591
|
+
version: UNKNOWN_METADATA,
|
|
4592
|
+
protocolVersion: UNKNOWN_METADATA
|
|
4593
|
+
};
|
|
4594
|
+
}
|
|
4595
|
+
function getCurrentMcpTransport() {
|
|
4596
|
+
return getSession()?.transport ?? "stdio";
|
|
4597
|
+
}
|
|
4598
|
+
function recordMcpSessionTermination(category, context) {
|
|
4599
|
+
const session = context ?? getSession();
|
|
4600
|
+
const durationMs = session ? Math.max(0, Date.now() - session.startedAt) : 0;
|
|
4601
|
+
const attributes = {
|
|
4602
|
+
...metadataAttributes(session?.metadata ?? {
|
|
4603
|
+
name: UNKNOWN_METADATA,
|
|
4604
|
+
version: UNKNOWN_METADATA,
|
|
4605
|
+
protocolVersion: UNKNOWN_METADATA
|
|
4606
|
+
}, session?.transport ?? "stdio"),
|
|
4607
|
+
termination_category: category,
|
|
4608
|
+
session_duration_bucket: durationBucket(durationMs),
|
|
4609
|
+
tool_calls_bucket: bucketCount(session?.toolCalls ?? 0)
|
|
4610
|
+
};
|
|
4611
|
+
sessionEnded.add(1, attributes);
|
|
4612
|
+
if (!context || context === activeStdioSession) activeStdioSession = void 0;
|
|
4613
|
+
}
|
|
4614
|
+
function resolveSessionTerminationCategory(shutdownSucceeded, context) {
|
|
4615
|
+
if (!shutdownSucceeded) return "unknown";
|
|
4616
|
+
return (context ?? getSession())?.hadToolFailure ? "tool_failure" : "clean";
|
|
4617
|
+
}
|
|
4618
|
+
//#endregion
|
|
4619
|
+
//#region src/utils/streamable-http.ts
|
|
4620
|
+
const MCP_PATH = "/mcp";
|
|
4621
|
+
const MAX_BODY_BYTES = 1048576;
|
|
4622
|
+
const HTTP_BEARER_TOKEN = "HEVY_MCP_HTTP_BEARER_TOKEN";
|
|
4623
|
+
var HttpRequestError = class extends Error {
|
|
4624
|
+
statusCode;
|
|
4625
|
+
constructor(statusCode, message) {
|
|
4626
|
+
super(message);
|
|
4627
|
+
this.name = "HttpRequestError";
|
|
4628
|
+
this.statusCode = statusCode;
|
|
4629
|
+
}
|
|
4630
|
+
};
|
|
4631
|
+
function normalizeHost(host) {
|
|
4632
|
+
return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
4633
|
+
}
|
|
4634
|
+
function isLoopbackHost(host) {
|
|
4635
|
+
const normalized = normalizeHost(host).toLowerCase();
|
|
4636
|
+
return normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1";
|
|
4637
|
+
}
|
|
4638
|
+
function hostNamesFor(options) {
|
|
4639
|
+
const configured = normalizeHost(options.host).toLowerCase();
|
|
4640
|
+
const names = /* @__PURE__ */ new Set([configured]);
|
|
4641
|
+
if (isLoopbackHost(options.host)) {
|
|
4642
|
+
names.add("localhost");
|
|
4643
|
+
names.add("127.0.0.1");
|
|
4644
|
+
names.add("::1");
|
|
4645
|
+
}
|
|
4646
|
+
return names;
|
|
4647
|
+
}
|
|
4648
|
+
function expectedPort(options, server) {
|
|
4649
|
+
if (options.port !== 0) return options.port;
|
|
4650
|
+
const address = server.address();
|
|
4651
|
+
return address && typeof address !== "string" ? address.port : 0;
|
|
4652
|
+
}
|
|
4653
|
+
function validateHostHeader(request, allowedHosts, port, allowAnyHostname = false) {
|
|
4654
|
+
const header = request.headers.host;
|
|
4655
|
+
if (!header || Array.isArray(header)) return false;
|
|
4656
|
+
try {
|
|
4657
|
+
const parsed = new URL(`http://${header}`);
|
|
4658
|
+
if (parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || !allowAnyHostname && (parsed.port ? Number(parsed.port) : 80) !== port) return false;
|
|
4659
|
+
return allowAnyHostname || allowedHosts.has(normalizeHost(parsed.hostname).toLowerCase());
|
|
4660
|
+
} catch {
|
|
4661
|
+
return false;
|
|
4662
|
+
}
|
|
4663
|
+
}
|
|
4664
|
+
function safeDiagnostic(error) {
|
|
4665
|
+
return JSON.stringify(createSafeErrorDiagnostic(error));
|
|
4666
|
+
}
|
|
4667
|
+
function writeJson(res, status, message) {
|
|
4668
|
+
if (res.headersSent) return;
|
|
4669
|
+
res.statusCode = status;
|
|
4670
|
+
res.setHeader("Content-Type", "application/json");
|
|
4671
|
+
res.end(JSON.stringify({ error: message }));
|
|
4672
|
+
}
|
|
4673
|
+
function readBody(request) {
|
|
4674
|
+
return new Promise((resolve, reject) => {
|
|
4675
|
+
let size = 0;
|
|
4676
|
+
let rejected = false;
|
|
4677
|
+
const chunks = [];
|
|
4678
|
+
const onData = (chunk) => {
|
|
4679
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
4680
|
+
size += buffer.byteLength;
|
|
4681
|
+
if (size > MAX_BODY_BYTES) {
|
|
4682
|
+
if (!rejected) {
|
|
4683
|
+
rejected = true;
|
|
4684
|
+
request.removeListener("data", onData);
|
|
4685
|
+
request.resume();
|
|
4686
|
+
reject(new HttpRequestError(413, "Request body is too large."));
|
|
4687
|
+
}
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
if (!rejected) chunks.push(buffer);
|
|
4691
|
+
};
|
|
4692
|
+
request.on("data", onData);
|
|
4693
|
+
request.once("error", (error) => {
|
|
4694
|
+
if (!rejected) reject(error);
|
|
4695
|
+
});
|
|
4696
|
+
request.once("end", () => {
|
|
4697
|
+
if (rejected) return;
|
|
4698
|
+
try {
|
|
4699
|
+
const raw = Buffer.concat(chunks).toString("utf8");
|
|
4700
|
+
resolve(raw.length === 0 ? void 0 : JSON.parse(raw));
|
|
4701
|
+
} catch {
|
|
4702
|
+
reject(new HttpRequestError(400, "Request body must be valid JSON."));
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
});
|
|
4706
|
+
}
|
|
4707
|
+
function isInitializeRequest(body) {
|
|
4708
|
+
return Boolean(body && typeof body === "object" && !Array.isArray(body) && "method" in body && body.method === "initialize");
|
|
4709
|
+
}
|
|
4710
|
+
function closeServer(server) {
|
|
4711
|
+
return new Promise((resolve, reject) => {
|
|
4712
|
+
if (!server.listening) {
|
|
4713
|
+
resolve();
|
|
4714
|
+
return;
|
|
4715
|
+
}
|
|
4716
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
4717
|
+
server.closeIdleConnections();
|
|
4718
|
+
server.closeAllConnections();
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
function isBearerAuthorized(request, token) {
|
|
4722
|
+
if (!token) return false;
|
|
4723
|
+
const header = request.headers.authorization;
|
|
4724
|
+
if (typeof header !== "string") return false;
|
|
4725
|
+
const expected = Buffer.from(`Bearer ${token}`);
|
|
4726
|
+
const actual = Buffer.from(header);
|
|
4727
|
+
return expected.byteLength === actual.byteLength && timingSafeEqual(expected, actual);
|
|
4728
|
+
}
|
|
4729
|
+
function aggregateErrors(errors, message) {
|
|
4730
|
+
if (errors.length === 1) return errors[0];
|
|
4731
|
+
return new AggregateError(errors, message);
|
|
4732
|
+
}
|
|
4733
|
+
async function startStreamableHttpServer(options, apiKey, createMcpServer) {
|
|
4734
|
+
const wildcard = options.host === "0.0.0.0" || options.host === "::" || options.host === "[::]";
|
|
4735
|
+
const loopback = isLoopbackHost(options.host);
|
|
4736
|
+
const bearerToken = process.env[HTTP_BEARER_TOKEN];
|
|
4737
|
+
if (!loopback && !bearerToken) throw new Error(`Non-loopback HTTP mode requires ${HTTP_BEARER_TOKEN} to protect the shared Hevy account.`);
|
|
4738
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4739
|
+
const cleanupErrors = [];
|
|
4740
|
+
const server = createServer((request, response) => {
|
|
4741
|
+
handleRequest(request, response).catch((error) => {
|
|
4742
|
+
if (response.headersSent) {
|
|
4743
|
+
if (!response.writableEnded) response.destroy();
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
if (error instanceof HttpRequestError) {
|
|
4747
|
+
writeJson(response, error.statusCode, error.message);
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4750
|
+
console.error(`HTTP request failed: ${safeDiagnostic(error)}`);
|
|
4751
|
+
writeJson(response, 500, "Internal server error.");
|
|
4752
|
+
});
|
|
4753
|
+
});
|
|
4754
|
+
async function closeSession(session, failureCategory) {
|
|
4755
|
+
if (session.closed) return;
|
|
4756
|
+
session.closed = true;
|
|
4757
|
+
for (const [id, current] of sessions) if (current === session) sessions.delete(id);
|
|
4758
|
+
for (const response of session.responses) if (!response.writableEnded) response.destroy();
|
|
4759
|
+
const errors = (await Promise.allSettled([Promise.resolve().then(() => session.transport.close()), Promise.resolve().then(() => session.server.close())])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
4760
|
+
const succeeded = errors.length === 0;
|
|
4761
|
+
recordMcpSessionTermination(failureCategory && succeeded ? failureCategory : resolveSessionTerminationCategory(succeeded, session.context), session.context);
|
|
4762
|
+
if (errors.length > 0) throw aggregateErrors(errors, "MCP session cleanup failed.");
|
|
4763
|
+
}
|
|
4764
|
+
async function closeUnregistered(transport, mcpServer) {
|
|
4765
|
+
const errors = (await Promise.allSettled([Promise.resolve().then(() => transport.close()), ...mcpServer ? [Promise.resolve().then(() => mcpServer.close())] : []])).flatMap((result) => result.status === "rejected" ? [result.reason] : []);
|
|
4766
|
+
if (errors.length > 0) throw aggregateErrors(errors, "MCP cleanup failed.");
|
|
4767
|
+
}
|
|
4768
|
+
async function handleRequest(request, response) {
|
|
4769
|
+
if (request.url?.split("?", 1)[0] !== MCP_PATH) {
|
|
4770
|
+
writeJson(response, 404, "Not found");
|
|
4771
|
+
return;
|
|
4772
|
+
}
|
|
4773
|
+
if (!validateHostHeader(request, hostNamesFor(options), expectedPort(options, server), wildcard)) {
|
|
4774
|
+
writeJson(response, 403, "Invalid Host header");
|
|
4775
|
+
return;
|
|
4776
|
+
}
|
|
4777
|
+
if (!loopback && !isBearerAuthorized(request, bearerToken)) {
|
|
4778
|
+
writeJson(response, 401, "Authorization required");
|
|
4779
|
+
return;
|
|
4780
|
+
}
|
|
4781
|
+
const body = request.method === "POST" ? await readBody(request) : void 0;
|
|
4782
|
+
const initializing = request.method === "POST" && isInitializeRequest(body);
|
|
4783
|
+
const sessionHeader = request.headers["mcp-session-id"];
|
|
4784
|
+
const sessionId = typeof sessionHeader === "string" ? sessionHeader : void 0;
|
|
4785
|
+
if (initializing) {
|
|
4786
|
+
if (sessionId) {
|
|
4787
|
+
writeJson(response, 400, "Initialization must not include Mcp-Session-Id.");
|
|
4788
|
+
return;
|
|
4789
|
+
}
|
|
4790
|
+
const context = createMcpSessionContext(body, "http");
|
|
4791
|
+
recordMcpSessionStart(body, "http", context);
|
|
4792
|
+
let session;
|
|
4793
|
+
let mcpServer;
|
|
4794
|
+
let connected = false;
|
|
4795
|
+
const transport = new StreamableHTTPServerTransport({
|
|
4796
|
+
sessionIdGenerator: randomUUID,
|
|
4797
|
+
onsessioninitialized: (id) => {
|
|
4798
|
+
if (session) sessions.set(id, session);
|
|
4799
|
+
}
|
|
4800
|
+
});
|
|
4801
|
+
transport.onclose = () => {
|
|
4802
|
+
if (session) closeSession(session).catch((error) => {
|
|
4803
|
+
cleanupErrors.push(error);
|
|
4804
|
+
});
|
|
4805
|
+
};
|
|
4806
|
+
try {
|
|
4807
|
+
mcpServer = await createMcpServer({ apiKey });
|
|
4808
|
+
session = {
|
|
4809
|
+
transport,
|
|
4810
|
+
server: mcpServer,
|
|
4811
|
+
context,
|
|
4812
|
+
responses: /* @__PURE__ */ new Set(),
|
|
4813
|
+
closed: false
|
|
4814
|
+
};
|
|
4815
|
+
await mcpServer.connect(transport);
|
|
4816
|
+
connected = true;
|
|
4817
|
+
session.responses.add(response);
|
|
4818
|
+
response.once("close", () => session?.responses.delete(response));
|
|
4819
|
+
await runWithMcpSessionContext(context, () => transport.handleRequest(request, response, body));
|
|
4820
|
+
} catch (error) {
|
|
4821
|
+
console.error(`HTTP session request failed: ${safeDiagnostic(error)}`);
|
|
4822
|
+
let cleanupError;
|
|
4823
|
+
try {
|
|
4824
|
+
if (session) await closeSession(session, connected ? "unknown" : "connect_failure");
|
|
4825
|
+
else {
|
|
4826
|
+
await closeUnregistered(transport, mcpServer);
|
|
4827
|
+
recordMcpSessionTermination("startup_failure", context);
|
|
4828
|
+
}
|
|
4829
|
+
} catch (cleanupFailure) {
|
|
4830
|
+
cleanupError = cleanupFailure;
|
|
4831
|
+
if (!session) recordMcpSessionTermination("unknown", context);
|
|
4832
|
+
}
|
|
4833
|
+
if (cleanupError) console.error(`HTTP cleanup failed: ${safeDiagnostic(cleanupError)}`);
|
|
4834
|
+
if (!response.writableEnded) writeJson(response, 500, "Internal server error.");
|
|
4835
|
+
}
|
|
4836
|
+
return;
|
|
4837
|
+
}
|
|
4838
|
+
if (!sessionId) {
|
|
4839
|
+
writeJson(response, 400, "Mcp-Session-Id header is required.");
|
|
4840
|
+
return;
|
|
4841
|
+
}
|
|
4842
|
+
const session = sessions.get(sessionId);
|
|
4843
|
+
if (!session) {
|
|
4844
|
+
writeJson(response, 404, "Unknown Mcp-Session-Id.");
|
|
4845
|
+
return;
|
|
4846
|
+
}
|
|
4847
|
+
if (request.method !== "DELETE") {
|
|
4848
|
+
session.responses.add(response);
|
|
4849
|
+
response.once("close", () => session.responses.delete(response));
|
|
4850
|
+
}
|
|
4851
|
+
await runWithMcpSessionContext(session.context, () => session.transport.handleRequest(request, response, body));
|
|
4852
|
+
}
|
|
4853
|
+
await new Promise((resolve, reject) => {
|
|
4854
|
+
server.once("error", reject);
|
|
4855
|
+
server.listen(options.port, options.host, () => {
|
|
4856
|
+
server.removeListener("error", reject);
|
|
4857
|
+
resolve();
|
|
4858
|
+
});
|
|
4859
|
+
});
|
|
4860
|
+
let closePromise;
|
|
4861
|
+
return {
|
|
4862
|
+
server,
|
|
4863
|
+
close: () => {
|
|
4864
|
+
if (closePromise) return closePromise;
|
|
4865
|
+
closePromise = (async () => {
|
|
4866
|
+
const sessionsToClose = [...sessions.values()];
|
|
4867
|
+
const results = await Promise.allSettled([...sessionsToClose.map((session) => closeSession(session)), closeServer(server)]);
|
|
4868
|
+
sessions.clear();
|
|
4869
|
+
const errors = [...cleanupErrors, ...results.flatMap((result) => result.status === "rejected" ? [result.reason] : [])];
|
|
4870
|
+
if (errors.length > 0) throw aggregateErrors(errors, "HTTP server cleanup failed.");
|
|
4871
|
+
})();
|
|
4872
|
+
return closePromise;
|
|
4873
|
+
}
|
|
4874
|
+
};
|
|
4875
|
+
}
|
|
4876
|
+
//#endregion
|
|
4435
4877
|
//#region src/utils/graceful-shutdown.ts
|
|
4436
4878
|
const shutdownSignals = ["SIGINT", "SIGTERM"];
|
|
4437
4879
|
const FORCED_EXIT_TIMEOUT_MS = 5e3;
|
|
@@ -4599,99 +5041,6 @@ function createNodeHevyClientOptions() {
|
|
|
4599
5041
|
};
|
|
4600
5042
|
}
|
|
4601
5043
|
//#endregion
|
|
4602
|
-
//#region src/utils/mcp-session-observability.ts
|
|
4603
|
-
const UNKNOWN_METADATA = "unknown";
|
|
4604
|
-
const MAX_METADATA_LENGTH = 64;
|
|
4605
|
-
const SAFE_METADATA_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+:/@-]{0,63}$/u;
|
|
4606
|
-
let currentSession;
|
|
4607
|
-
function isRecord(value) {
|
|
4608
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4609
|
-
}
|
|
4610
|
-
function normalizeMetadata(value) {
|
|
4611
|
-
if (typeof value !== "string") return UNKNOWN_METADATA;
|
|
4612
|
-
const normalized = value.trim();
|
|
4613
|
-
if (normalized.length === 0 || normalized.length > MAX_METADATA_LENGTH || !SAFE_METADATA_PATTERN.test(normalized)) return UNKNOWN_METADATA;
|
|
4614
|
-
return normalized;
|
|
4615
|
-
}
|
|
4616
|
-
function getInitializeParams(message) {
|
|
4617
|
-
if (!isRecord(message) || message.method !== "initialize") return {};
|
|
4618
|
-
return isRecord(message.params) ? message.params : {};
|
|
4619
|
-
}
|
|
4620
|
-
function extractMcpClientMetadata(message) {
|
|
4621
|
-
const params = getInitializeParams(message);
|
|
4622
|
-
const clientInfo = isRecord(params.clientInfo) ? params.clientInfo : {};
|
|
4623
|
-
return {
|
|
4624
|
-
name: normalizeMetadata(clientInfo.name),
|
|
4625
|
-
version: normalizeMetadata(clientInfo.version),
|
|
4626
|
-
protocolVersion: normalizeMetadata(params.protocolVersion)
|
|
4627
|
-
};
|
|
4628
|
-
}
|
|
4629
|
-
function metadataAttributes(metadata) {
|
|
4630
|
-
return {
|
|
4631
|
-
client_name: metadata.name,
|
|
4632
|
-
client_version: metadata.version,
|
|
4633
|
-
protocol_version: metadata.protocolVersion,
|
|
4634
|
-
transport: "stdio"
|
|
4635
|
-
};
|
|
4636
|
-
}
|
|
4637
|
-
function durationBucket(durationMs) {
|
|
4638
|
-
if (durationMs < 1e3) return "<1s";
|
|
4639
|
-
if (durationMs < 1e4) return "1-10s";
|
|
4640
|
-
if (durationMs < 6e4) return "10-60s";
|
|
4641
|
-
if (durationMs < 3e5) return "1-5m";
|
|
4642
|
-
return "5m+";
|
|
4643
|
-
}
|
|
4644
|
-
function recordMcpSessionStart(message) {
|
|
4645
|
-
const metadata = extractMcpClientMetadata(message);
|
|
4646
|
-
currentSession = {
|
|
4647
|
-
metadata,
|
|
4648
|
-
startedAt: Date.now(),
|
|
4649
|
-
toolCalls: 0,
|
|
4650
|
-
hadToolFailure: false
|
|
4651
|
-
};
|
|
4652
|
-
const attributes = metadataAttributes(metadata);
|
|
4653
|
-
sessionStarted.add(1, attributes);
|
|
4654
|
-
return metadata;
|
|
4655
|
-
}
|
|
4656
|
-
function recordMcpToolInvocation() {
|
|
4657
|
-
if (currentSession) currentSession.toolCalls += 1;
|
|
4658
|
-
return metadataAttributes(currentSession?.metadata ?? {
|
|
4659
|
-
name: UNKNOWN_METADATA,
|
|
4660
|
-
version: UNKNOWN_METADATA,
|
|
4661
|
-
protocolVersion: UNKNOWN_METADATA
|
|
4662
|
-
});
|
|
4663
|
-
}
|
|
4664
|
-
function recordMcpToolFailure() {
|
|
4665
|
-
if (currentSession) currentSession.hadToolFailure = true;
|
|
4666
|
-
}
|
|
4667
|
-
function getCurrentMcpClientMetadata() {
|
|
4668
|
-
return currentSession?.metadata ?? {
|
|
4669
|
-
name: UNKNOWN_METADATA,
|
|
4670
|
-
version: UNKNOWN_METADATA,
|
|
4671
|
-
protocolVersion: UNKNOWN_METADATA
|
|
4672
|
-
};
|
|
4673
|
-
}
|
|
4674
|
-
function recordMcpSessionTermination(category) {
|
|
4675
|
-
const session = currentSession;
|
|
4676
|
-
const durationMs = session ? Math.max(0, Date.now() - session.startedAt) : 0;
|
|
4677
|
-
const attributes = {
|
|
4678
|
-
...metadataAttributes(session?.metadata ?? {
|
|
4679
|
-
name: UNKNOWN_METADATA,
|
|
4680
|
-
version: UNKNOWN_METADATA,
|
|
4681
|
-
protocolVersion: UNKNOWN_METADATA
|
|
4682
|
-
}),
|
|
4683
|
-
termination_category: category,
|
|
4684
|
-
session_duration_bucket: durationBucket(durationMs),
|
|
4685
|
-
tool_calls_bucket: bucketCount(session?.toolCalls ?? 0)
|
|
4686
|
-
};
|
|
4687
|
-
sessionEnded.add(1, attributes);
|
|
4688
|
-
currentSession = void 0;
|
|
4689
|
-
}
|
|
4690
|
-
function resolveSessionTerminationCategory(shutdownSucceeded) {
|
|
4691
|
-
if (!shutdownSucceeded) return "unknown";
|
|
4692
|
-
return currentSession?.hadToolFailure ? "tool_failure" : "clean";
|
|
4693
|
-
}
|
|
4694
|
-
//#endregion
|
|
4695
5044
|
//#region src/utils/tool-observer.ts
|
|
4696
5045
|
const WORKFLOW_PAGINATION_RESOURCES = /* @__PURE__ */ new Set([
|
|
4697
5046
|
"workouts",
|
|
@@ -4722,7 +5071,7 @@ function createAttributes(invocation) {
|
|
|
4722
5071
|
"mcp.client.name": clientMetadata.name,
|
|
4723
5072
|
"mcp.client.version": clientMetadata.version,
|
|
4724
5073
|
"mcp.protocol.version": clientMetadata.protocolVersion,
|
|
4725
|
-
"mcp.transport":
|
|
5074
|
+
"mcp.transport": getCurrentMcpTransport(),
|
|
4726
5075
|
"mcp.tool.args.key_count_bucket": invocation.argumentKeyCountBucket ?? "unknown",
|
|
4727
5076
|
"mcp.tool.args.keys": invocation.argumentKeys?.join(",") ?? ""
|
|
4728
5077
|
};
|
|
@@ -5185,13 +5534,18 @@ const HELP_TEXT = [
|
|
|
5185
5534
|
"Options:",
|
|
5186
5535
|
" -h, --help Show this help message and exit",
|
|
5187
5536
|
" -v, --version Show version and exit",
|
|
5537
|
+
" --transport stdio|http Select the transport (default: stdio)",
|
|
5538
|
+
" --host <host> HTTP bind host (default: 127.0.0.1)",
|
|
5539
|
+
" --port <port> HTTP bind port (default: 3000)",
|
|
5188
5540
|
"",
|
|
5189
5541
|
"Environment:",
|
|
5190
5542
|
" HEVY_API_KEY=<api-key> Hevy API key from Hevy app settings",
|
|
5191
5543
|
" HEVY_MCP_DEBUG=1 Enable verbose diagnostics on stderr",
|
|
5544
|
+
" HEVY_MCP_HTTP_BEARER_TOKEN Protect non-loopback HTTP deployments",
|
|
5192
5545
|
"",
|
|
5193
5546
|
"Examples:",
|
|
5194
|
-
" HEVY_API_KEY=your-key npx hevy-mcp"
|
|
5547
|
+
" HEVY_API_KEY=your-key npx hevy-mcp",
|
|
5548
|
+
" HEVY_API_KEY=your-key npx hevy-mcp --transport http --port 3000"
|
|
5195
5549
|
].join("\n");
|
|
5196
5550
|
function getCliAction(args) {
|
|
5197
5551
|
for (const arg of args) {
|
|
@@ -5251,11 +5605,11 @@ async function validateApiKey(apiKey) {
|
|
|
5251
5605
|
console.error(diagnostic ? `${API_KEY_VALIDATION_WARNING} Diagnostic: ${diagnostic}.` : API_KEY_VALIDATION_WARNING);
|
|
5252
5606
|
}
|
|
5253
5607
|
}
|
|
5254
|
-
function buildServer(apiKey) {
|
|
5608
|
+
function buildServer(apiKey, transport = "stdio") {
|
|
5255
5609
|
return tracer.startActiveSpan("mcp.server.build", { attributes: {
|
|
5256
5610
|
"mcp.server.name": name,
|
|
5257
5611
|
"mcp.server.version": version,
|
|
5258
|
-
"mcp.transport":
|
|
5612
|
+
"mcp.transport": transport
|
|
5259
5613
|
} }, (span) => {
|
|
5260
5614
|
try {
|
|
5261
5615
|
const server = createHevyMcpServer({
|
|
@@ -5282,13 +5636,13 @@ function buildServer(apiKey) {
|
|
|
5282
5636
|
}
|
|
5283
5637
|
});
|
|
5284
5638
|
}
|
|
5285
|
-
async function createNodeMcpServer({ apiKey }) {
|
|
5639
|
+
async function createNodeMcpServer({ apiKey }, transport = "stdio") {
|
|
5286
5640
|
const { apiKey: validatedApiKey } = serverConfigSchema.parse({ apiKey });
|
|
5287
5641
|
const userHash = fingerprintApiKey(validatedApiKey);
|
|
5288
5642
|
setCurrentUserHash(userHash);
|
|
5289
5643
|
Sentry.setUser({ id: userHash });
|
|
5290
5644
|
await validateApiKey(validatedApiKey);
|
|
5291
|
-
return buildServer(validatedApiKey);
|
|
5645
|
+
return buildServer(validatedApiKey, transport);
|
|
5292
5646
|
}
|
|
5293
5647
|
async function runStdioServer() {
|
|
5294
5648
|
const cliAction = getCliAction(process.argv.slice(2));
|
|
@@ -5348,5 +5702,51 @@ async function runStdioServer() {
|
|
|
5348
5702
|
}
|
|
5349
5703
|
});
|
|
5350
5704
|
}
|
|
5705
|
+
async function runServer() {
|
|
5706
|
+
const args = process.argv.slice(2);
|
|
5707
|
+
const cliAction = getCliAction(args);
|
|
5708
|
+
if (cliAction === "version") {
|
|
5709
|
+
console.error(`${name} v${version}`);
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
if (cliAction === "help") {
|
|
5713
|
+
console.log(HELP_TEXT);
|
|
5714
|
+
return;
|
|
5715
|
+
}
|
|
5716
|
+
const options = parseNodeCliOptions(args);
|
|
5717
|
+
if (options.transport === "stdio") {
|
|
5718
|
+
await runStdioServer();
|
|
5719
|
+
return;
|
|
5720
|
+
}
|
|
5721
|
+
await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "http" } }, async (span) => {
|
|
5722
|
+
let listening = false;
|
|
5723
|
+
serverStartups.add(1, { version });
|
|
5724
|
+
try {
|
|
5725
|
+
const cfg = parseConfig(process.env);
|
|
5726
|
+
assertApiKey(cfg.apiKey);
|
|
5727
|
+
await validateApiKey(cfg.apiKey);
|
|
5728
|
+
const handle = await startStreamableHttpServer(options, cfg.apiKey, (params) => Promise.resolve(buildServer(params.apiKey, "http")));
|
|
5729
|
+
listening = true;
|
|
5730
|
+
console.error(`Starting MCP server in HTTP mode at ${options.host}:${options.port}/mcp`);
|
|
5731
|
+
scheduleUpdateCheck({
|
|
5732
|
+
packageName: name$1,
|
|
5733
|
+
currentVersion: version$1
|
|
5734
|
+
});
|
|
5735
|
+
installGracefulShutdown({
|
|
5736
|
+
target: handle,
|
|
5737
|
+
onComplete: async () => {
|
|
5738
|
+
await flushTelemetry();
|
|
5739
|
+
}
|
|
5740
|
+
});
|
|
5741
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
5742
|
+
} catch (error) {
|
|
5743
|
+
recordMcpSessionTermination(listening ? "unknown" : "startup_failure");
|
|
5744
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
5745
|
+
throw error;
|
|
5746
|
+
} finally {
|
|
5747
|
+
span.end();
|
|
5748
|
+
}
|
|
5749
|
+
});
|
|
5750
|
+
}
|
|
5351
5751
|
//#endregion
|
|
5352
|
-
export {
|
|
5752
|
+
export { createSafeErrorDiagnostic as a, MissingHevyApiKeyError as i, runServer as n, flushTelemetry as o, runStdioServer as r, createNodeMcpServer as t };
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/chrisdoc/hevy-mcp",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.0
|
|
10
|
+
"version": "4.1.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "hevy-mcp",
|
|
15
|
-
"version": "4.0
|
|
15
|
+
"version": "4.1.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|