Skip to content

Commit f78f35b

Browse files
committed
feat: improve type safety with Zod schema inference
- Add InferToolParams type utility for inferring types from Zod schemas - Update withErrorHandling to preserve parameter types generically - Refactor all tool handlers to use inferred types instead of manual assertions - Eliminate all 'args as {...}' type assertions in tool implementations - Update AGENTS.md with comprehensive type-safe development patterns - Add type inference examples and best practices documentation This ensures single source of truth for types (Zod schemas) and eliminates redundant type definitions while maintaining full type safety.
1 parent 5cbff80 commit f78f35b

8 files changed

Lines changed: 589 additions & 405 deletions

File tree

AGENTS.md

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- **hevy-mcp** is a Model Context Protocol (MCP) server for the Hevy Fitness API, enabling AI agents to manage workouts, routines, exercise templates, and folders via the Hevy API.
77
- The codebase is TypeScript (Node.js v20+), with a clear separation between tool implementations (`src/tools/`), generated API clients (`src/generated/`), and utility logic (`src/utils/`).
88
- API client code is generated from the OpenAPI spec using [Kubb](https://kubb.dev/). **Do not manually edit generated files.**
9+
- **Type Safety:** The project uses Zod schema inference for type-safe tool parameters, eliminating manual type assertions and ensuring compile-time type safety.
910

1011
## Working Effectively
1112

@@ -127,17 +128,27 @@ Always perform these validation steps after making changes:
127128
pnpm run check
128129
```
129130
- Must complete without errors (warnings about Biome schema are acceptable).
131+
- **EXPECTED:** Warnings about `any` usage in `webhooks.ts` are acceptable (API methods not yet available).
130132

131-
4. **MCP tool functionality validation (if API key available):**
133+
4. **Type checking validation:**
134+
```bash
135+
npx tsc --noEmit
136+
```
137+
- Must complete without errors.
138+
- Verifies all type inference is working correctly.
139+
140+
5. **MCP tool functionality validation (if API key available):**
132141
- Start development server: `pnpm run dev`
133142
- Test MCP tool endpoints with a client
134143
- Verify tool responses are correctly formatted
135144

136145
### Critical Validation Notes
137146
- **ALWAYS** run unit tests after any source code changes
138147
- **ALWAYS** run build validation before committing changes
148+
- **ALWAYS** use type inference (`InferToolParams`) instead of manual type assertions
139149
- **DO NOT** attempt to fix TypeScript errors in `src/generated/` - these are auto-generated files
140150
- **DO NOT** commit `.env` files containing real API keys
151+
- **DO NOT** use `as any` or `as unknown` type assertions in tool handlers
141152

142153
## Project Structure and Key Files
143154

@@ -155,8 +166,14 @@ src/
155166
│ ├── client/ # Kubb-generated client code
156167
│ └── schemas/ # Zod validation schemas
157168
└── utils/ # Shared helper functions
158-
├── formatters.ts # Data formatting helpers
159-
└── hevyClient.ts # API client configuration
169+
├── tool-helpers.ts # Type inference utilities (InferToolParams)
170+
├── error-handler.ts # Centralized error handling (withErrorHandling)
171+
├── response-formatter.ts # MCP response utilities
172+
├── formatters.ts # Data formatting helpers
173+
├── hevyClient.ts # API client factory
174+
├── hevyClientKubb.ts # Kubb client wrapper
175+
├── config.ts # Configuration parsing
176+
└── httpServer.ts # HTTP server utilities (deprecated)
160177
```
161178

162179
### Testing Structure
@@ -168,22 +185,73 @@ tests/
168185

169186
## Development Patterns
170187

188+
### Type-Safe Tool Implementation
189+
190+
The project uses **Zod schema inference** for type-safe tool parameters. This eliminates manual type assertions and ensures types match schemas automatically.
191+
192+
#### Pattern: Using Type Inference
193+
194+
**Always** extract Zod schemas and use `InferToolParams` for type safety:
195+
196+
```typescript
197+
import type { InferToolParams } from "../utils/tool-helpers.js";
198+
import { withErrorHandling } from "../utils/error-handler.js";
199+
200+
// 1. Define schema as const
201+
const getRoutinesSchema = {
202+
page: z.coerce.number().int().gte(1).default(1),
203+
pageSize: z.coerce.number().int().gte(1).lte(10).default(5),
204+
} as const;
205+
206+
// 2. Infer types from schema
207+
type GetRoutinesParams = InferToolParams<typeof getRoutinesSchema>;
208+
209+
// 3. Use inferred type in handler
210+
server.tool(
211+
"get-routines",
212+
"Description...",
213+
getRoutinesSchema, // Use the schema constant
214+
withErrorHandling(async (args: GetRoutinesParams) => {
215+
// args is fully typed - no manual assertions needed!
216+
const { page, pageSize } = args;
217+
// ...
218+
}, "get-routines"),
219+
);
220+
```
221+
222+
**Key Benefits:**
223+
- ✅ Single source of truth (Zod schema defines both validation and types)
224+
- ✅ No manual type assertions (`args as {...}`)
225+
- ✅ Automatic type updates when schemas change
226+
- ✅ Full IDE autocomplete and type checking
227+
228+
**DO NOT:**
229+
- ❌ Use `args as { ... }` type assertions
230+
- ❌ Define parameter types separately from Zod schemas
231+
- ❌ Use `Record<string, unknown>` in handler signatures (use inferred types)
232+
171233
### Adding New MCP Tools
172-
1. Create new tool file in `src/tools/`
173-
2. Implement tool functions using existing patterns
174-
3. Validate inputs with Zod schemas from `src/generated/schemas/`
175-
4. Format outputs using helpers in `src/utils/formatters.ts`
176-
5. Register tools in `src/index.ts`
177-
6. Add unit tests co-located with implementation
234+
235+
1. **Create new tool file** in `src/tools/`
236+
2. **Define Zod schema** with `as const` assertion
237+
3. **Infer parameter types** using `InferToolParams<typeof schema>`
238+
4. **Implement handler** with typed parameters (no manual assertions)
239+
5. **Wrap with error handling** using `withErrorHandling` from `src/utils/error-handler.ts`
240+
6. **Format outputs** using helpers in `src/utils/formatters.ts`
241+
7. **Register tools** in `src/index.ts`
242+
8. **Add unit tests** co-located with implementation
178243

179244
### Working with Generated Code
180245
- **NEVER** edit files in `src/generated/` directly
181246
- Regenerate API client: `pnpm run build:client`
182247
- If OpenAPI spec changes, update `openapi-spec.json` first
248+
- Generated types are available in `src/generated/client/types/index.ts`
183249

184250
### Error Handling
185251
- Use centralized error handling from `src/utils/error-handler.ts`
252+
- Wrap handlers with `withErrorHandling(fn, "context-name")`
186253
- Follow existing error response patterns in tool implementations
254+
- Error responses automatically include `isError: true` flag
187255

188256
## Troubleshooting
189257

@@ -193,12 +261,30 @@ tests/
193261
3. **TypeScript errors in generated code:** Expected - ignore these
194262
4. **Build failures:** Run `pnpm run check` to identify formatting/linting issues
195263
5. **Network errors in export-specs:** Expected in sandboxed environments
264+
6. **Type errors in tool handlers:** Use `InferToolParams<typeof schema>` instead of manual type assertions
265+
7. **Linter warnings about `any`:** Expected in `webhooks.ts` where API methods don't exist yet (see TODOs)
196266

197267
### Performance Expectations
198268
- **Build time:** 3-5 seconds
199269
- **Unit test time:** 1-2 seconds
200270
- **Dependency installation:** 30 seconds
201271
- **API client generation:** 4-5 seconds
272+
- **Type checking:** < 1 second
273+
274+
## Key Utilities Reference
275+
276+
### Type Inference (`src/utils/tool-helpers.ts`)
277+
- **`InferToolParams<T>`**: Infers TypeScript types from Zod schema objects
278+
- **`createTypedToolHandler`**: Optional wrapper for automatic validation (MCP SDK already validates)
279+
280+
### Error Handling (`src/utils/error-handler.ts`)
281+
- **`withErrorHandling<TParams>(fn, context)`**: Wraps handlers with error handling while preserving parameter types
282+
- **`createErrorResponse(error, context?)`**: Creates standardized error responses
283+
284+
### Response Formatting (`src/utils/response-formatter.ts`)
285+
- **`createJsonResponse(data, options?)`**: Creates JSON-formatted MCP responses
286+
- **`createTextResponse(text)`**: Creates text-formatted MCP responses
287+
- **`createEmptyResponse(message)`**: Creates empty responses with messages
202288

203289
---
204290

src/tools/folders.ts

Lines changed: 51 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
createEmptyResponse,
99
createJsonResponse,
1010
} from "../utils/response-formatter.js";
11+
import type { InferToolParams } from "../utils/tool-helpers.js";
1112

1213
// Type definitions for the folder operations
1314
type HevyClient = ReturnType<
@@ -22,56 +23,63 @@ export function registerFolderTools(
2223
hevyClient: HevyClient | null,
2324
) {
2425
// Get routine folders
26+
const getRoutineFoldersSchema = {
27+
page: z.coerce.number().int().gte(1).default(1),
28+
pageSize: z.coerce.number().int().gte(1).lte(10).default(5),
29+
} as const;
30+
type GetRoutineFoldersParams = InferToolParams<
31+
typeof getRoutineFoldersSchema
32+
>;
33+
2534
server.tool(
2635
"get-routine-folders",
2736
"Get a paginated list of your routine folders, including both default and custom folders. Useful for organizing and browsing your workout routines.",
28-
{
29-
page: z.coerce.number().int().gte(1).default(1),
30-
pageSize: z.coerce.number().int().gte(1).lte(10).default(5),
31-
},
32-
withErrorHandling(
33-
async ({ page, pageSize }: { page: number; pageSize: number }) => {
34-
if (!hevyClient) {
35-
throw new Error(
36-
"API client not initialized. Please provide HEVY_API_KEY.",
37-
);
38-
}
39-
const data = await hevyClient.getRoutineFolders({
40-
page,
41-
pageSize,
42-
});
37+
getRoutineFoldersSchema,
38+
withErrorHandling(async (args: GetRoutineFoldersParams) => {
39+
if (!hevyClient) {
40+
throw new Error(
41+
"API client not initialized. Please provide HEVY_API_KEY.",
42+
);
43+
}
44+
const { page, pageSize } = args;
45+
const data = await hevyClient.getRoutineFolders({
46+
page,
47+
pageSize,
48+
});
4349

44-
// Process routine folders to extract relevant information
45-
const folders =
46-
data?.routine_folders?.map((folder: RoutineFolder) =>
47-
formatRoutineFolder(folder),
48-
) || [];
50+
// Process routine folders to extract relevant information
51+
const folders =
52+
data?.routine_folders?.map((folder: RoutineFolder) =>
53+
formatRoutineFolder(folder),
54+
) || [];
4955

50-
if (folders.length === 0) {
51-
return createEmptyResponse(
52-
"No routine folders found for the specified parameters",
53-
);
54-
}
56+
if (folders.length === 0) {
57+
return createEmptyResponse(
58+
"No routine folders found for the specified parameters",
59+
);
60+
}
5561

56-
return createJsonResponse(folders);
57-
},
58-
"get-routine-folders",
59-
),
62+
return createJsonResponse(folders);
63+
}, "get-routine-folders"),
6064
);
6165

6266
// Get single routine folder by ID
67+
const getRoutineFolderSchema = {
68+
folderId: z.string().min(1),
69+
} as const;
70+
type GetRoutineFolderParams = InferToolParams<typeof getRoutineFolderSchema>;
71+
6372
server.tool(
6473
"get-routine-folder",
6574
"Get complete details of a specific routine folder by its ID, including name, creation date, and associated routines.",
66-
{
67-
folderId: z.string().min(1),
68-
},
69-
withErrorHandling(async ({ folderId }: { folderId: string }) => {
75+
getRoutineFolderSchema,
76+
withErrorHandling(async (args: GetRoutineFolderParams) => {
7077
if (!hevyClient) {
7178
throw new Error(
7279
"API client not initialized. Please provide HEVY_API_KEY.",
7380
);
7481
}
82+
const { folderId } = args;
7583
const data = await hevyClient.getRoutineFolder(folderId);
7684

7785
if (!data) {
@@ -86,18 +94,24 @@ export function registerFolderTools(
8694
);
8795

8896
// Create new routine folder
97+
const createRoutineFolderSchema = {
98+
name: z.string().min(1),
99+
} as const;
100+
type CreateRoutineFolderParams = InferToolParams<
101+
typeof createRoutineFolderSchema
102+
>;
103+
89104
server.tool(
90105
"create-routine-folder",
91106
"Create a new routine folder in your Hevy account. Requires a name for the folder. Returns the full folder details including the new folder ID.",
92-
{
93-
name: z.string().min(1),
94-
},
95-
withErrorHandling(async ({ name }: { name: string }) => {
107+
createRoutineFolderSchema,
108+
withErrorHandling(async (args: CreateRoutineFolderParams) => {
96109
if (!hevyClient) {
97110
throw new Error(
98111
"API client not initialized. Please provide HEVY_API_KEY.",
99112
);
100113
}
114+
const { name } = args;
101115
const data = await hevyClient.createRoutineFolder({
102116
routine_folder: {
103117
title: name,

0 commit comments

Comments
 (0)