Add tool annotations and fix body measurement payload handling#342
Conversation
Six @kubb/* packages were declared as runtime dependencies but are only used by `npm run build:client` (kubb generate). Keeping them in dependencies made `npx hevy-mcp` installs resolve their caret ranges directly, which broke whenever an unpublished @kubb version was referenced (#322). Only @kubb/plugin-client is imported at runtime by the generated client, so it stays in dependencies. Also syncs package-lock.json, which was missing yaml@2.9.0. Fixes #322 https://claude.ai/code/session_01CuJWqXKY94msBi4cUgn1D8
Two bugs prevented valid body measurement writes (#341): - Measurement fields used strict z.number(), rejecting numeric strings like "81.5" that some MCP clients send. They now use z.coerce.number() like the other tools, while null/undefined still short-circuit before coercion. - buildMeasurementPayload() forwarded every omitted field as null, which the Hevy API rejects. Nullish fields are now omitted from the payload entirely, for both create and update. Fixes #341 https://claude.ai/code/session_01CuJWqXKY94msBi4cUgn1D8
Exposes the Hevy API's GET /v1/user/info endpoint, which existed in the generated client but had no MCP tool. Returns the authenticated user's ID, display name, and public profile URL — useful for verifying which account an API key belongs to. Also refreshes the README tools table, which was stale (get-routine-by-id, missing body measurement and exercise template tools). https://claude.ai/code/session_01CuJWqXKY94msBi4cUgn1D8
Every tool now declares title, readOnlyHint, destructiveHint, idempotentHint, and openWorldHint so MCP clients can render tools properly and decide when calls are safe to auto-approve: - get-*/search-* tools are marked read-only - create-* tools are non-destructive, non-idempotent writes - update-* tools are destructive (PUT-style overwrites) and idempotent - delete-webhook-subscription is destructive and idempotent - openWorldHint is false everywhere since the Hevy API is a closed domain limited to the authenticated user's data Shared annotation factories live in src/utils/tool-annotations.ts and a new annotations.test.ts asserts the policy for all 26 tools. Test helpers now extract handlers position-independently so the added registration argument doesn't break them. https://claude.ai/code/session_01CuJWqXKY94msBi4cUgn1D8
📝 WalkthroughWalkthroughAdds shared MCP tool annotation factories, registers a new get-user-info tool and Hevy client getter, refactors body-measurement schema/payload to coerce numeric strings and omit null/undefined fields, applies annotations across tools, standardizes test helpers, and adds validation tests and README/package.json updates. ChangesTool Annotations & User Info Integration
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #342 +/- ##
==========================================
+ Coverage 70.28% 70.63% +0.34%
==========================================
Files 15 17 +2
Lines 589 596 +7
Branches 200 188 -12
==========================================
+ Hits 414 421 +7
- Misses 100 101 +1
+ Partials 75 74 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
✨ PR Review
LGTM
Generated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Review using Guidelines Learn how
There was a problem hiding this comment.
Code Review
This pull request registers a new get-user-info tool, implements shared MCP tool annotations across all tools, refactors body measurement payload construction to omit null/undefined fields, and moves several @kubb dependencies to devDependencies. The review feedback identifies two key issues in src/tools/body-measurements.ts: an inconsistency where explicit null values intended to clear fields are silently omitted from the payload, and a potential bug where z.coerce.number() coorces empty strings to 0 instead of treating them as omitted.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| const value = args[camelKey]; | ||
| if (value != null) { | ||
| payload[apiKey] = value; | ||
| } |
There was a problem hiding this comment.
There is an inconsistency between the Zod schema and the payload construction.
The schema zNullableNumber uses .nullable().optional(), which allows clients to explicitly pass null (presumably to clear a measurement value). However, buildMeasurementPayload uses value != null to filter out both undefined and null values. This means any explicit null passed by the client to clear a field will be silently omitted from the API payload, resulting in no change to that field on the server.
Depending on whether the Hevy API supports clearing fields by setting them to null:
- If the API supports
nullto clear fields:
Change the check tovalue !== undefinedso that explicitnulls are preserved and sent to the API, while omitted (undefined) fields are excluded. - If the API does not support
nullat all:
Remove.nullable()fromzNullableNumberso the schema accurately reflects thatnullis not a valid input.
| const value = args[camelKey]; | |
| if (value != null) { | |
| payload[apiKey] = value; | |
| } | |
| const value = args[camelKey]; | |
| if (value !== undefined) { | |
| payload[apiKey] = value; | |
| } |
There was a problem hiding this comment.
Went with a third option in c71a88f: the Hevy API rejects null field values outright (that's the root cause of #341), so option 1 isn't possible — there is no way to clear a field via the API. Removing .nullable() (option 2) would break the lenient MCP clients that send null for unspecified fields, which is the exact scenario #341 was filed about. So null stays accepted and treated as omitted, and both tool descriptions now state this explicitly instead of leaving it silent. update-body-measurement additionally errors if no non-null field remains, so a null-only call surfaces a diagnostic rather than a misleading success.
Generated by Claude Code
| // Coerce numeric strings ("81.5") since some MCP clients serialize numbers | ||
| // as strings; nullable()/optional() short-circuit before coercion, so null | ||
| // stays null instead of becoming 0. | ||
| const zNullableNumber = z.coerce.number().nullable().optional(); |
There was a problem hiding this comment.
Using z.coerce.number() will coerce empty strings ("") to 0 because Number("") evaluates to 0 in JavaScript. If an MCP client serializes an empty or cleared input field as an empty string, it will be saved as 0 (e.g., 0 kg or 0 cm), which is likely invalid for body measurements.
To prevent this, you can preprocess the input to map empty strings to undefined before coercion, allowing them to be treated as omitted/optional.
const zNullableNumber = z.preprocess(
(val) => (val === "" ? undefined : val),
z.coerce.number().nullable().optional(),
);
Greptile SummaryThis PR fixes a bug (#341) where the body measurement update endpoint was sending
Confidence Score: 4/5Safe to merge — the payload fix addresses a confirmed API rejection, the new user tool follows established patterns, and the dependency reshuffle is consistent with runtime import evidence. The src/tools/body-measurements.ts (null-drop behavior) and src/tools/workouts.ts (mismatched error context tag) Important Files Changed
Sequence DiagramsequenceDiagram
participant MCP as MCP Client
participant Tool as update-body-measurement
participant Builder as buildMeasurementPayload
participant API as Hevy API
MCP->>Tool: "date, weightKg: 80, chestCm: null"
Note over Tool: Zod validates input
Tool->>Builder: "weightKg: 80, chestCm: null"
Note over Builder: Iterates MEASUREMENT_FIELD_TO_API_KEY skipping null values
Builder-->>Tool: "weight_kg: 80"
Tool->>API: "PUT /body-measurements/date {weight_kg: 80}"
API-->>Tool: 200 OK
Tool-->>MCP: "Body measurement updated successfully"
|
| // The Hevy API rejects null for omitted fields, so only fields with actual | ||
| // values are included in the payload (#341). | ||
| function buildMeasurementPayload( | ||
| args: MeasurementFieldArgs, | ||
| ): Omit<BodyMeasurement, "date"> { | ||
| const payload: Omit<BodyMeasurement, "date"> = {}; | ||
| for (const [camelKey, apiKey] of Object.entries( | ||
| MEASUREMENT_FIELD_TO_API_KEY, | ||
| ) as [keyof MeasurementFieldArgs, keyof Omit<BodyMeasurement, "date">][]) { | ||
| const value = args[camelKey]; | ||
| if (value != null) { | ||
| payload[apiKey] = value; | ||
| } | ||
| } | ||
| return payload; | ||
| } |
There was a problem hiding this comment.
Explicit
null inputs are silently dropped rather than surfacing an error
buildMeasurementPayload uses value != null which filters out both undefined (omitted) and null (explicitly provided). A caller who passes {weightKg: null} intending to clear a measurement will receive a 200 success response, but the update payload sent to the API will be empty — the field is not actually cleared and no diagnostic is shown. Since the API rejects null and there is currently no way to delete individual fields, the tool description and/or a guard could make this explicit rather than silently ignoring the value.
There was a problem hiding this comment.
Pull request overview
This PR standardizes MCP tool metadata by adding annotations to every registered tool, and fixes body measurement write behavior so only non-null measurement fields are sent to the Hevy API (addressing partial-update failures). It also adds a new read-only user info tool, updates tests to accommodate the new tool signature, and refreshes docs/dependency placement to match the generation/runtime split.
Changes:
- Added
tool-annotations.tsfactories and applied annotations consistently across all tool registrations. - Refactored body measurement input handling (numeric coercion + omit nullish fields) and expanded regression tests around payload construction/mapping.
- Introduced a new
get-user-infotool with unit tests; updated README and dependency classifications.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/tool-annotations.ts | Adds shared factories for MCP ToolAnnotations (read/create/update/delete). |
| src/utils/hevyClientKubb.ts | Adds getUserInfo() wrapper to the generated client facade. |
| src/tools/workouts.ts | Applies annotations to workout tools. |
| src/tools/workouts.test.ts | Updates test helper to handle the added annotations argument. |
| src/tools/webhooks.ts | Applies annotations to webhook tools. |
| src/tools/webhooks.test.ts | Updates test helper to handle schema + annotations + handler call shape. |
| src/tools/user.ts | Adds new get-user-info read-only tool. |
| src/tools/user.test.ts | Adds unit tests for the new user tool (success/empty/not-initialized). |
| src/tools/templates.ts | Applies annotations to template tools. |
| src/tools/templates.test.ts | Updates test helper to handle the added annotations argument. |
| src/tools/routines.ts | Applies annotations to routine tools. |
| src/tools/routines.test.ts | Updates test helper to handle schema + annotations + handler call shape. |
| src/tools/folders.ts | Applies annotations to folder tools. |
| src/tools/folders.test.ts | Updates test helper to handle the added annotations argument. |
| src/tools/body-measurements.ts | Coerces numeric strings, introduces field-to-API mapping, and omits nullish fields from payloads; applies annotations. |
| src/tools/body-measurements.test.ts | Updates expectations for omitted fields, adds regression coverage for mapping/coercion/null handling. |
| src/tools/annotations.test.ts | Adds a suite asserting every tool is registered with proper annotations/hints. |
| src/index.ts | Registers the new user tool module. |
| README.md | Updates the available tools table to include body measurements and user info. |
| package.json | Moves Kubb generation-only packages to devDependencies. |
| package-lock.json | Updates lockfile to reflect dependency classification changes. |
| "Update an existing body measurement entry for a given date. Only the fields you provide are sent; omitted fields are not included in the request. Returns 404 if no entry exists for the date.", | ||
| updateBodyMeasurementSchema, | ||
| updateAnnotations("Update Body Measurement"), | ||
| withErrorHandling(async (args: UpdateBodyMeasurementParams) => { | ||
| if (!hevyClient) { |
| it("accepts date-only input and explicit nulls in the schema", () => { | ||
| const { server, tool } = createMockServer(); | ||
| registerBodyMeasurementTools(server, {} as unknown as HevyClient); | ||
| const { schema } = getToolRegistration(tool, "update-body-measurement"); | ||
|
|
||
| const dateOnly = z.object(schema).parse({ date: "2025-04-01" }); | ||
| expect(dateOnly).toEqual({ date: "2025-04-01" }); | ||
|
|
||
| const withNull = z.object(schema).parse({ | ||
| date: "2025-04-01", | ||
| weightKg: null, | ||
| }); | ||
| expect(withNull.weightKg).toBeNull(); | ||
| }); |
Addresses PR #342 review feedback: - Empty strings are treated as omitted instead of being coerced to 0 by z.coerce.number(). - update-body-measurement now rejects calls without at least one non-null measurement field instead of sending an empty PUT body. - Tool descriptions document that null values are treated as omitted, since the Hevy API rejects nulls and has no way to clear individual fields. https://claude.ai/code/session_01CuJWqXKY94msBi4cUgn1D8
Summary
This PR adds MCP tool annotations to all registered tools and fixes the body measurement update endpoint to only send non-null fields to the API, resolving an issue where omitted fields were being set to null.
Key Changes
Tool Annotations: Added
tool-annotations.tsutility with factory functions (readOnlyAnnotations,createAnnotations,updateAnnotations,destructiveAnnotations) and applied them to all tools across workouts, routines, templates, folders, webhooks, body measurements, and user info tools. Each annotation includes a title and appropriate hints for read-only, destructive, and idempotent operations.Body Measurement Payload Handling: Refactored
buildMeasurementPayload()to only include fields with actual values (non-null) in the API request, fixing issue body measurement tools reject valid partial inputs #341 where the Hevy API was rejecting null values for omitted fields. The function now iterates through provided fields and only adds them to the payload if they have a value.Numeric Coercion: Updated body measurement field schema to use
z.coerce.number()to handle numeric strings from MCP clients that serialize numbers as strings, while preserving null values.User Tools: Added new
user.tstool module withget-user-infoendpoint to retrieve authenticated user account information (ID, display name, profile URL).Test Coverage: Added comprehensive test suite in
annotations.test.tsverifying all tools have proper annotations with correct hints. Addeduser.test.tsfor user info tool. Enhanced body measurement tests to verify payload construction, field mapping, numeric coercion, and null handling.Test Refactoring: Updated test helper functions across all tool test files to use more flexible extraction patterns (
match.at(-1)) to accommodate the new annotations parameter in tool registration.Documentation: Updated README.md to include body measurements and user info in the available tools table.
Dependencies: Moved Kubb CLI and plugin packages from dependencies to devDependencies since they're only needed for code generation, not runtime.
Notable Implementation Details
MEASUREMENT_FIELD_TO_API_KEYmapping ensures consistent field name translation from camelCase (tool input) to snake_case (API format).openWorldHint: falsesince they operate on a closed, authenticated domain.satisfiesto ensure the mapping covers all measurement fields.https://claude.ai/code/session_01CuJWqXKY94msBi4cUgn1D8
Summary by CodeRabbit
New Features
Documentation
Tests
✨ PR Description
Purpose: Add MCP tool annotations and fix body measurement payload handling to exclude null fields from API requests.
Main changes:
readOnlyAnnotations,createAnnotations,updateAnnotations, anddestructiveAnnotationsfactories for consistent MCP metadatazNullableNumberpreprocessor to coerce numeric strings and treat empty strings as undefined, plus newget-user-infotool and comprehensive annotation test suiteGenerated by LinearB AI and added by gitStream.
AI-generated content may contain inaccuracies. Please verify before using.
💡 Tip: You can customize your AI Description using Guidelines Learn how