hevy-mcp 3.2.1 → 3.4.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 CHANGED
@@ -367,9 +367,28 @@ The bearer value is your Hevy API key, not an OAuth token. The Worker validates
367
367
  the key with Hevy on each request, does not store it, and forwards it upstream
368
368
  only as Hevy's required `api-key` header.
369
369
 
370
- The endpoint does not expose legacy SSE or a `GET` event stream. Clients that
371
- require OAuth discovery, dynamic registration, token refresh, or legacy SSE are
372
- not compatible unless they can send the fixed custom header above.
370
+ ### OAuth for Claude.ai and other remote MCP clients
371
+
372
+ Workers deployed with an `OAUTH_KV` namespace binding (see
373
+ [CONTRIBUTING.md](./CONTRIBUTING.md)) additionally expose a full OAuth 2.1
374
+ layer for clients that cannot send a fixed header, such as Claude.ai custom
375
+ connectors:
376
+
377
+ - RFC 8414 / RFC 9728 discovery metadata under `/.well-known/`
378
+ - Dynamic client registration (`/register`) and PKCE token exchange (`/token`)
379
+ - An `/authorize` page where you paste your Hevy API key once; the key is
380
+ validated with Hevy and stored encrypted inside the OAuth grant
381
+
382
+ Add the Worker URL ending in `/mcp` as a Claude.ai custom connector and
383
+ complete the authorization flow in the browser. Direct
384
+ `Authorization: Bearer <hevy-api-key>` requests keep working unchanged — the
385
+ OAuth layer is purely additive — and rotating your Hevy API key invalidates
386
+ every OAuth grant created with it.
387
+
388
+ The endpoint does not expose legacy SSE or a `GET` event stream. Without the
389
+ opt-in OAuth layer, clients that require OAuth discovery, dynamic
390
+ registration, or token refresh are not compatible unless they can send the
391
+ fixed custom header above.
373
392
 
374
393
  ### Self-host the Worker
375
394
 
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@3.2.1" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.4.0" };
8
8
  var n = new e.Error().stack;
9
9
  n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "47a79c69-c982-44cf-abda-d010f962b955", e._sentryDebugIdIdentifier = "sentry-dbid-47a79c69-c982-44cf-abda-d010f962b955");
10
10
  } catch (e) {}
11
11
  })();
12
- import { i as createSafeErrorDiagnostic, r as runServer } from "./src-D1O3O9f2.mjs";
12
+ import { i as createSafeErrorDiagnostic, r as runServer } from "./src-86dub6ti.mjs";
13
13
  //#region src/cli.ts
14
14
  runServer().catch((error) => {
15
15
  console.error("Fatal error in main()", createSafeErrorDiagnostic(error));
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@3.2.1" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.4.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-D1O3O9f2.mjs";
12
+ import { n as createServer, r as runServer, t as configSchema } from "./src-86dub6ti.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@3.2.1" };
7
+ e.SENTRY_RELEASE = { id: "hevy-mcp@3.4.0" };
8
8
  var n = new e.Error().stack;
9
- n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "8ce801d0-ad38-40ee-a7f3-dc829e212db8", e._sentryDebugIdIdentifier = "sentry-dbid-8ce801d0-ad38-40ee-a7f3-dc829e212db8");
9
+ n && (e._sentryDebugIds = e._sentryDebugIds || {}, e._sentryDebugIds[n] = "6d0f4903-ba12-4a80-99f1-595d5d373fb9", e._sentryDebugIdIdentifier = "sentry-dbid-6d0f4903-ba12-4a80-99f1-595d5d373fb9");
10
10
  } catch (e) {}
11
11
  })();
12
12
  import * as Sentry from "@sentry/node";
@@ -40,10 +40,10 @@ import semver from "semver";
40
40
  * OTel Collector → Honeycomb: performance traces, metrics
41
41
  */
42
42
  const name$1 = "hevy-mcp";
43
- const version$1 = "3.2.1";
43
+ const version$1 = "3.4.0";
44
44
  const collectorToken = "NH9vOela-HYreQxAbJa68cjEmORoEKM57EvneUDVcOo";
45
45
  const COLLECTOR_ENDPOINT = "https://otel.chrisdoc.dev/v1";
46
- const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@3.2.1`;
46
+ const sentryRelease = process.env.SENTRY_RELEASE ?? `hevy-mcp@3.4.0`;
47
47
  const resource = resourceFromAttributes({
48
48
  "service.name": name$1,
49
49
  "service.version": version$1
@@ -59,7 +59,16 @@ const sentryClient = Sentry.init({
59
59
  registerEsmLoaderHooks: false,
60
60
  ignoreErrors: ["EPIPE", "broken pipe"]
61
61
  });
62
- const spanProcessors = [new SentrySpanProcessor()];
62
+ let currentUserHash;
63
+ var UserHashSpanProcessor = class {
64
+ onStart(span) {
65
+ if (currentUserHash) span.setAttribute("user.hash", currentUserHash);
66
+ }
67
+ onEnd(_span) {}
68
+ async forceFlush() {}
69
+ async shutdown() {}
70
+ };
71
+ const spanProcessors = [new UserHashSpanProcessor(), new SentrySpanProcessor()];
63
72
  spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({
64
73
  url: `${COLLECTOR_ENDPOINT}/traces`,
65
74
  headers: { Authorization: `Bearer ${collectorToken}` }
@@ -90,12 +99,8 @@ trace.setGlobalTracerProvider(tracerProvider);
90
99
  Sentry.validateOpenTelemetrySetup();
91
100
  const tracer = trace.getTracer(name$1);
92
101
  const meter = metrics.getMeter(name$1);
93
- let currentUserId;
94
- function setCurrentUserId(id) {
95
- currentUserId = id;
96
- }
97
- function getCurrentUserId() {
98
- return currentUserId;
102
+ function setCurrentUserHash(hash) {
103
+ currentUserHash = hash;
99
104
  }
100
105
  //#endregion
101
106
  //#region src/utils/metrics.ts
@@ -1121,7 +1126,7 @@ function registerHevyResources(server, runtime) {
1121
1126
  //#endregion
1122
1127
  //#region src/server-metadata.ts
1123
1128
  const SERVER_NAME = "hevy-mcp";
1124
- const SERVER_VERSION = "3.2.1";
1129
+ const SERVER_VERSION = "3.4.0";
1125
1130
  const SERVER_INSTRUCTIONS = [
1126
1131
  "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.",
1127
1132
  "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.",
@@ -3692,8 +3697,7 @@ function createNodeHevyClientOptions() {
3692
3697
  const span = tracer.startSpan(`hevy.api.${observation.method}`, { attributes: {
3693
3698
  "http.method": observation.method,
3694
3699
  "http.status_code": observation.status,
3695
- "hevy.api.endpoint": observation.endpoint,
3696
- ...getCurrentUserId() ? { "user.id": getCurrentUserId() } : {}
3700
+ "hevy.api.endpoint": observation.endpoint
3697
3701
  } });
3698
3702
  span.setStatus({ code: observation.error ? SpanStatusCode.ERROR : SpanStatusCode.OK });
3699
3703
  if (observation.error) {
@@ -3789,7 +3793,6 @@ function withTelemetry(fn, context) {
3789
3793
  const startTime = Date.now();
3790
3794
  let isError = false;
3791
3795
  toolInvocations.add(1, { tool_name: context });
3792
- const userId = getCurrentUserId();
3793
3796
  const safeArgs = extractSafeArgs(args);
3794
3797
  const whitelistedKeys = Object.keys(safeArgs).map((key) => key.replace("mcp.tool.args.", ""));
3795
3798
  return tracer.startActiveSpan(`mcp.tool.${context}`, { attributes: {
@@ -3797,7 +3800,6 @@ function withTelemetry(fn, context) {
3797
3800
  "workflow.name": context,
3798
3801
  "mcp.tool.args.key_count": argumentKeyCount,
3799
3802
  "mcp.tool.args.keys": whitelistedKeys.join(","),
3800
- ...userId ? { "user.id": userId } : {},
3801
3803
  ...safeArgs
3802
3804
  } }, async (span) => {
3803
3805
  try {
@@ -3853,14 +3855,27 @@ function withTelemetry(fn, context) {
3853
3855
  * Wrap an MCP tool handler with telemetry inside error response handling.
3854
3856
  */
3855
3857
  function withObservability(fn, context) {
3856
- return withErrorHandling(withTelemetry(fn, context), context, (error, _toolContext, argumentKeyCount) => {
3858
+ return withErrorHandling(withTelemetry(fn, context), context, (error, toolContext, argumentKeyCount) => {
3857
3859
  const { diagnostic } = resolveErrorPolicy(error, "");
3858
3860
  Sentry.withScope((scope) => {
3861
+ scope.setTag("mcp.tool.context", toolContext);
3859
3862
  scope.setTag("error.category", diagnostic.category);
3860
3863
  if (diagnostic.code) scope.setTag("error.code", diagnostic.code);
3861
3864
  if (diagnostic.status !== void 0) scope.setTag("http.status_code", String(diagnostic.status));
3862
- scope.setContext("mcpTool", { argumentKeyCount });
3865
+ if (diagnostic.endpoint) scope.setTag("hevy.api.endpoint", diagnostic.endpoint);
3866
+ scope.setContext("mcpTool", {
3867
+ context: toolContext,
3868
+ argumentKeyCount
3869
+ });
3863
3870
  scope.setContext("safeError", { ...diagnostic });
3871
+ scope.setFingerprint([
3872
+ "mcp-tool-failure",
3873
+ toolContext,
3874
+ diagnostic.category,
3875
+ diagnostic.code ?? "none",
3876
+ String(diagnostic.status ?? "none"),
3877
+ diagnostic.endpoint ?? "none"
3878
+ ]);
3864
3879
  Sentry.captureMessage("MCP tool failure", "error");
3865
3880
  });
3866
3881
  });
@@ -4251,16 +4266,12 @@ async function validateApiKey(apiKey) {
4251
4266
  }
4252
4267
  }
4253
4268
  function buildServer(apiKey) {
4254
- const userId = fingerprintApiKey(apiKey);
4255
4269
  return tracer.startActiveSpan("mcp.server.build", { attributes: {
4256
4270
  "mcp.server.name": name,
4257
4271
  "mcp.server.version": version,
4258
- "mcp.transport": "stdio",
4259
- "user.id": userId
4272
+ "mcp.transport": "stdio"
4260
4273
  } }, (span) => {
4261
4274
  try {
4262
- Sentry.setUser({ id: userId });
4263
- setCurrentUserId(userId);
4264
4275
  const server = createSharedMcpServer({
4265
4276
  apiKey,
4266
4277
  clientOptions: createNodeHevyClientOptions(),
@@ -4281,6 +4292,9 @@ function buildServer(apiKey) {
4281
4292
  }
4282
4293
  async function createServer({ config }) {
4283
4294
  const { apiKey } = serverConfigSchema.parse(config);
4295
+ const userHash = fingerprintApiKey(apiKey);
4296
+ setCurrentUserHash(userHash);
4297
+ Sentry.setUser({ id: userHash });
4284
4298
  await validateApiKey(apiKey);
4285
4299
  return buildServer(apiKey);
4286
4300
  }
@@ -4295,6 +4309,12 @@ async function runServer() {
4295
4309
  return;
4296
4310
  }
4297
4311
  serverStartups.add(1, { version });
4312
+ const configuredApiKey = process.env.HEVY_API_KEY;
4313
+ const initialUserHash = configuredApiKey ? fingerprintApiKey(configuredApiKey) : void 0;
4314
+ if (initialUserHash) {
4315
+ setCurrentUserHash(initialUserHash);
4316
+ Sentry.setUser({ id: initialUserHash });
4317
+ }
4298
4318
  await tracer.startActiveSpan("mcp.server.run", { attributes: { "mcp.transport": "stdio" } }, async (span) => {
4299
4319
  try {
4300
4320
  const apiKey = parseConfig(process.env).apiKey;
@@ -4330,4 +4350,4 @@ async function runServer() {
4330
4350
  //#endregion
4331
4351
  export { createSafeErrorDiagnostic as i, createServer as n, runServer as r, configSchema as t };
4332
4352
 
4333
- //# sourceMappingURL=src-D1O3O9f2.mjs.map
4353
+ //# sourceMappingURL=src-86dub6ti.mjs.map