search-exercise-templates
Search fitness exercise templates by name and filter by primary muscle group. Results are cached for fast repeated access, with option to refresh the cache.
Instructions
Search exercise templates by name with optional muscle group filter. Fetches all templates from the Hevy API on first call and caches them in memory for subsequent searches. Use refresh:true to force a re-fetch.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | Case-insensitive substring to match against exercise template titles | |
| primaryMuscleGroup | No | Optional filter to restrict results to a specific primary muscle group | |
| refresh | No | Set to true to bust the in-memory cache and re-fetch all templates from the API |
Implementation Reference
- src/tools/templates.ts:287-355 (handler)The handler function for the search-exercise-templates tool. It fetches all exercise templates from the Hevy API with pagination (caching them in memory), then filters by case-insensitive query and optional primary muscle group. Supports a refresh flag to bust the cache.
server.tool( "search-exercise-templates", "Search exercise templates by name with optional muscle group filter. Fetches all templates from the Hevy API on first call and caches them in memory for subsequent searches. Use refresh:true to force a re-fetch.", searchExerciseTemplatesSchema, withErrorHandling(async (args: SearchExerciseTemplatesParams) => { if (!hevyClient) { throw new Error( "API client not initialized. Please provide HEVY_API_KEY.", ); } const { query, primaryMuscleGroup, refresh } = args; // Populate cache if empty or refresh requested. // Use an in-flight promise to prevent concurrent duplicate fetches. if (exerciseTemplateCache === null || refresh) { if (refresh) exerciseTemplateFetch = null; if (exerciseTemplateFetch === null) { exerciseTemplateFetch = (async () => { const allTemplates: ExerciseTemplate[] = []; let page = 1; let pageCount = 1; do { const data: GetV1ExerciseTemplates200 = await hevyClient.getExerciseTemplates({ page, pageSize: 100, }); const templates = data?.exercise_templates ?? []; allTemplates.push(...templates); pageCount = data?.page_count ?? 1; page++; } while (page <= pageCount); exerciseTemplateCache = allTemplates; exerciseTemplateFetch = null; return allTemplates; })(); } await exerciseTemplateFetch; } // Filter by query (case-insensitive title substring match) const queryLower = query.toLowerCase(); if (exerciseTemplateCache === null) { throw new Error("Failed to populate exercise template cache."); } let results = exerciseTemplateCache.filter((t) => (t.title ?? "").toLowerCase().includes(queryLower), ); // Optional primary muscle group filter if (primaryMuscleGroup !== undefined) { results = results.filter( (t) => t.primary_muscle_group === primaryMuscleGroup, ); } if (results.length === 0) { return createEmptyResponse( `No exercise templates found matching "${query}"${primaryMuscleGroup ? ` with primary muscle group "${primaryMuscleGroup}"` : ""}`, ); } return createJsonResponse(results.map(formatExerciseTemplate)); }, "search-exercise-templates"), - src/tools/templates.ts:262-285 (schema)Zod schema defining the input parameters for search-exercise-templates: query (string, required), primaryMuscleGroup (enum from MUSCLE_GROUPS, optional), and refresh (boolean, default false).
const searchExerciseTemplatesSchema = { query: z .string() .min(1) .describe( "Case-insensitive substring to match against exercise template titles", ), primaryMuscleGroup: z .enum(MUSCLE_GROUPS) .optional() .describe( "Optional filter to restrict results to a specific primary muscle group", ), refresh: z .boolean() .optional() .default(false) .describe( "Set to true to bust the in-memory cache and re-fetch all templates from the API", ), } as const; type SearchExerciseTemplatesParams = InferToolParams< typeof searchExerciseTemplatesSchema >; - src/index.ts:47-77 (registration)The tool is registered via registerTemplateTools() in the main server setup, which is called from src/index.ts.
import { registerTemplateTools } from "./tools/templates.js"; import { registerWebhookTools } from "./tools/webhooks.js"; import { registerWorkoutTools } from "./tools/workouts.js"; import { assertApiKey, parseConfig } from "./utils/config.js"; import { createClient } from "./utils/hevyClient.js"; const HEVY_API_BASEURL = "https://api.hevyapp.com"; const serverConfigSchema = z.object({ apiKey: z .string() .min(1, "Hevy API key is required") .describe("Your Hevy API key (available in the Hevy app settings)."), }); export const configSchema = serverConfigSchema; type ServerConfig = z.infer<typeof serverConfigSchema>; function buildServer(apiKey: string) { const baseServer = new McpServer({ name, version, }); const server = Sentry.wrapMcpServerWithSentry(baseServer); const hevyClient = createClient(apiKey, HEVY_API_BASEURL); console.error("Hevy client initialized with API key"); registerWorkoutTools(server, hevyClient); registerRoutineTools(server, hevyClient); registerTemplateTools(server, hevyClient); - src/utils/formatters.ts:266-277 (helper)Helper function that formats exercise template objects from the API into a standardized shape (id, title, type, primaryMuscleGroup, secondaryMuscleGroups, isCustom) before returning results.
export function formatExerciseTemplate( template: ExerciseTemplate, ): FormattedExerciseTemplate { return { id: template.id, title: template.title, type: template.type, primaryMuscleGroup: template.primary_muscle_group, secondaryMuscleGroups: template.secondary_muscle_groups, isCustom: template.is_custom, }; }