search_exercise_templates
Search the Hevy exercise library by name, equipment, or muscle group to find exercise template IDs for creating routines or workouts.
Instructions
Fuzzy-search the Hevy exercise library. Use this before create_routine
or create_workout — it returns the exercise_template_id you need.
query: free-text exercise name. e.g. "barbell back squat", "incline db press".equipment: optional filter, e.g. "barbell", "dumbbell", "cable", "machine", "bodyweight".muscle_group: optional filter onprimary_muscle_group, e.g. "chest", "lats", "quads".
Returns ranked candidates with id, title, equipment, primary_muscle_group, and a match score 0-100. Pick the top hit unless the user disambiguates.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | Yes | ||
| equipment | No | ||
| muscle_group | No | ||
| limit | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/hevy_mcp/tools/templates.py:94-139 (handler)The `search_exercise_templates` handler function. It fetches all templates (cached), applies optional equipment/muscle_group filters, then uses rapidfuzz (fuzz.WRatio) to fuzzy-match the query against template titles. Returns ranked results with match scores 0-100.
async def search_exercise_templates( query: str, equipment: str | None = None, muscle_group: str | None = None, limit: int = 10, ) -> dict[str, Any]: """Fuzzy-search the Hevy exercise library. **Use this before `create_routine` or `create_workout`** — it returns the `exercise_template_id` you need. - `query`: free-text exercise name. e.g. "barbell back squat", "incline db press". - `equipment`: optional filter, e.g. "barbell", "dumbbell", "cable", "machine", "bodyweight". - `muscle_group`: optional filter on `primary_muscle_group`, e.g. "chest", "lats", "quads". Returns ranked candidates with id, title, equipment, primary_muscle_group, and a match score 0-100. Pick the top hit unless the user disambiguates. """ all_t = await _all_templates() candidates = all_t if equipment: eq = equipment.lower() candidates = [t for t in candidates if (t.get("equipment") or "").lower() == eq] if muscle_group: mg = muscle_group.lower() candidates = [t for t in candidates if (t.get("primary_muscle_group") or "").lower() == mg or mg in {m.lower() for m in (t.get("secondary_muscle_groups") or [])}] if not candidates: return { "error": f"No exercises matched the filters (equipment={equipment!r}, muscle_group={muscle_group!r}).", "hint": "Drop one of the filters and search by name only.", "results": [], } choices = {t["id"]: t.get("title", "") for t in candidates if t.get("id")} ranked = process.extract(query, choices, scorer=fuzz.WRatio, limit=limit) by_id = {t["id"]: t for t in candidates if t.get("id")} results = [ {**by_id[tid], "match_score": int(score)} for (_title, score, tid) in ranked ] return { "text": "\n".join(f"{r['match_score']:>3} {format_template(r)}" for r in results), "results": results, } - src/hevy_mcp/tools/templates.py:26-27 (registration)The `register` function in templates.py is called by `register_all` in tools/__init__.py, which is invoked from server.py. This is where `search_exercise_templates` gets registered as an MCP tool via the `@mcp.tool()` decorator on line 92.
def register(mcp, ctx) -> None: client = ctx.client - Caching constants and the `_all_templates()` helper that fetches and caches the full exercise template list from the Hevy API with a 24-hour TTL.
CACHE_TTL_SECONDS = 24 * 60 * 60 CACHE_KEY = "templates:all" - The `_all_templates()` async function that fetches all exercise templates from Hevy API with pagination, caching the result for 24 hours. Used by `search_exercise_templates` as the data source for fuzzy matching.
async def _all_templates() -> list[dict[str, Any]]: cached = cache.get(CACHE_KEY) if cached is not None: return cached async with lock: cached = cache.get(CACHE_KEY) if cached is not None: return cached collected: list[dict[str, Any]] = [] page = 1 while True: data = await client.get("/exercise_templates", params={"page": page, "pageSize": 100}) batch = _items(data) if not batch: break collected.extend(batch) page_count = data.get("page_count") if isinstance(data, dict) else None if page_count is not None and page >= page_count: break if len(batch) < 100: break page += 1 if page > 20: # safety net — Hevy library is finite break cache.set(CACHE_KEY, collected) return collected - src/hevy_mcp/tools/__init__.py:1-13 (registration)The `register_all` function in tools/__init__.py calls `templates.register(mcp, ctx)` on line 10, which in turn registers `search_exercise_templates` as an MCP tool.
"""Tool registration. Each module exposes `register(mcp, ctx)` to attach its tools.""" from . import analytics, folders, routines, templates, webhooks, workouts def register_all(mcp, ctx) -> None: workouts.register(mcp, ctx) routines.register(mcp, ctx) folders.register(mcp, ctx) templates.register(mcp, ctx) webhooks.register(mcp, ctx) analytics.register(mcp, ctx)