exercisebank-mcp 0.1.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/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TotalCoaching
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ This licence covers the example code in this repository. The exercise
24
+ library, its videos, stills and metadata are licensed separately:
25
+ https://exercisebank.net/license
package/README.md ADDED
@@ -0,0 +1,69 @@
1
+ # exercisebank-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server for the
4
+ [ExerciseBank API](https://exercisebank.net): 1,492 studio-filmed exercises with a hand-checked
5
+ classification and a ranked map of which exercises can replace which. It lets Claude, Cursor and other
6
+ MCP clients search real exercises by muscle, equipment, movement pattern, difficulty and joints to
7
+ avoid, so a plan is built from exercises that exist, with videos your app is licensed to show.
8
+
9
+ Use it while you build on the API (your coding agent explores the catalogue and the vocabulary as it
10
+ writes your integration) or to prototype a workout generator before writing any code.
11
+
12
+ ## Install
13
+
14
+ Get a free sandbox key (no card) at https://exercisebank.net/signup, then:
15
+
16
+ **Claude Code**
17
+
18
+ ```bash
19
+ claude mcp add exercisebank --env EB_KEY=eb_live_… -- npx -y exercisebank-mcp
20
+ ```
21
+
22
+ **Claude Desktop, Cursor, Windsurf and other clients** (`claude_desktop_config.json`, `.cursor/mcp.json`, …)
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "exercisebank": {
28
+ "command": "npx",
29
+ "args": ["-y", "exercisebank-mcp"],
30
+ "env": { "EB_KEY": "eb_live_…" }
31
+ }
32
+ }
33
+ }
34
+ ```
35
+
36
+ It runs on your machine over stdio; the key stays in your client's configuration. Node 18 or later.
37
+
38
+ ## Tools
39
+
40
+ | Tool | What it does |
41
+ |---|---|
42
+ | `get_vocabulary` | The exact filter values: muscles, equipment, movement patterns, joints, impact; the catalogue version; the ids a sandbox key gets video for |
43
+ | `search_exercises` | Search and filter: `q`, `muscle`, `equipment` (the gear the person has), `pattern`, `minDifficulty`, `maxDifficulty`, `excludeJoints`, `impact`, `unilateral`, `limit`, `offset` |
44
+ | `get_exercise` | The full record: description, coaching cues, French, classification |
45
+ | `get_substitutes` | Ranked replacements with a score and a reason each, filtered by gear and joints to avoid |
46
+ | `get_media_urls` | Signed video and still URLs. Only when `EB_END_USER` is set (below) |
47
+
48
+ ### Video
49
+
50
+ ExerciseBank signs media only for a named person, and that person counts as an end user on the plan.
51
+ To get `get_media_urls`, add an opaque id for whoever is at this machine:
52
+
53
+ ```json
54
+ "env": { "EB_KEY": "eb_live_…", "EB_END_USER": "dev-laptop-anna" }
55
+ ```
56
+
57
+ URLs expire after 15 minutes. A sandbox key gets media for the 50-exercise sample listed by
58
+ `get_vocabulary`; a paid plan gets all 1,492.
59
+
60
+ ## In your product
61
+
62
+ This server is for the developer's desk. A product calls the API from its own backend, with its own
63
+ users' ids: use the [`exercisebank`](https://www.npmjs.com/package/exercisebank) client and see the
64
+ guide, [an AI workout generator with Claude](https://exercisebank.net/guides/ai-workout-generator).
65
+
66
+ - API reference: https://exercisebank.net/docs · for language models: https://exercisebank.net/llms.txt
67
+ - Pricing, by monthly end users: https://exercisebank.net/pricing
68
+
69
+ MIT licensed. The library itself is licensed through a plan: https://exercisebank.net/license
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,96 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP server for the ExerciseBank API (https://exercisebank.net), over stdio.
4
+ * EB_KEY your key (free sandbox key: https://exercisebank.net/signup)
5
+ * EB_END_USER optional: an opaque id for the person at this machine. Set it to get the get_media_urls tool;
6
+ * media is only ever signed for a named person, and that person counts as an end user on the plan.
7
+ */
8
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
+ import { ExerciseBank, ExerciseBankError } from "exercisebank";
11
+ import { z } from "zod";
12
+ let eb;
13
+ try {
14
+ eb = new ExerciseBank();
15
+ }
16
+ catch (e) {
17
+ console.error(`exercisebank-mcp: ${e.message}`);
18
+ process.exit(1);
19
+ }
20
+ const END_USER = process.env.EB_END_USER?.trim();
21
+ const server = new McpServer({ name: "exercisebank", version: "0.1.0" });
22
+ const text = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }] });
23
+ const guard = (run) => async (args) => {
24
+ try {
25
+ return text(await run(args));
26
+ }
27
+ catch (e) {
28
+ const message = e instanceof ExerciseBankError ? `${e.message}${e.retryAfter ? ` (retry in ${e.retryAfter} s)` : ""}` : String(e);
29
+ return { content: [{ type: "text", text: message }], isError: true };
30
+ }
31
+ };
32
+ /** What a model needs to choose between exercises; get_exercise has the description and cues. */
33
+ const brief = (e) => ({
34
+ id: e.id, name: e.name, ...(e.nameFr ? { nameFr: e.nameFr } : {}),
35
+ primaryMuscles: e.primaryMuscles, secondaryMuscles: e.secondaryMuscles, equipment: e.equipment,
36
+ movementPattern: e.movementPattern, difficulty: e.difficulty, loadedJoints: e.loadedJoints, impact: e.impact, unilateral: e.unilateral,
37
+ });
38
+ const names = (what) => z.array(z.string()).optional().describe(`${what}. Values from get_vocabulary.`);
39
+ server.registerTool("get_vocabulary", {
40
+ title: "ExerciseBank vocabulary",
41
+ description: "The exact values the ExerciseBank filters accept: muscles, equipment, movement patterns, joints, impact. Call this once before search_exercises or get_substitutes. Also returns the catalogue version and the ids a sandbox key gets video for.",
42
+ inputSchema: {},
43
+ annotations: { readOnlyHint: true },
44
+ }, guard(async () => {
45
+ const m = await eb.meta();
46
+ return { version: m.version, count: m.count, plan: m.plan, enums: m.enums, sandboxMediaSample: m.sample };
47
+ }));
48
+ server.registerTool("search_exercises", {
49
+ title: "Search ExerciseBank exercises",
50
+ description: "Search 1,492 studio-filmed exercises. Every filter is optional and they combine. Only exercises returned here exist: never invent an exercise or an id. Without q the order is alphabetical, so filter rather than page.",
51
+ inputSchema: {
52
+ q: z.string().optional().describe("Text in the name, French name or aliases, e.g. 'goblet squat'"),
53
+ muscle: names("Primary or secondary muscles"),
54
+ equipment: names("The gear the person HAS (bodyweight is always allowed); exercises needing anything else are left out"),
55
+ pattern: names("Movement patterns"),
56
+ minDifficulty: z.number().int().min(1).max(5).optional(),
57
+ maxDifficulty: z.number().int().min(1).max(5).optional().describe("1 beginner to 5 advanced"),
58
+ excludeJoints: names("Leave out anything that loads these joints, e.g. a sore knee"),
59
+ impact: z.array(z.enum(["low", "moderate", "high"])).optional(),
60
+ unilateral: z.boolean().optional(),
61
+ limit: z.number().int().min(1).max(100).optional().describe("Default 25"),
62
+ offset: z.number().int().min(0).optional(),
63
+ },
64
+ annotations: { readOnlyHint: true },
65
+ }, guard(async (args) => {
66
+ const r = await eb.search(args);
67
+ return { total: r.total, next: r.next, exercises: r.data.map(brief) };
68
+ }));
69
+ server.registerTool("get_exercise", {
70
+ title: "Get one ExerciseBank exercise",
71
+ description: "The full record for one exercise id: description, coaching cues, French, classification, which media exists.",
72
+ inputSchema: { id: z.string().describe("An id from search_exercises, e.g. tc-2000") },
73
+ annotations: { readOnlyHint: true },
74
+ }, guard(({ id }) => eb.get(id)));
75
+ server.registerTool("get_substitutes", {
76
+ title: "Substitutes for an exercise",
77
+ description: "Ranked replacements for one exercise, each with a score and the reason it works as a swap. Filter by the gear the person has and by joints to avoid.",
78
+ inputSchema: {
79
+ id: z.string().describe("The exercise to replace"),
80
+ equipment: names("The gear the person has"),
81
+ excludeJoints: names("Leave out substitutes that load these joints"),
82
+ limit: z.number().int().min(1).max(50).optional().describe("Default 10"),
83
+ },
84
+ annotations: { readOnlyHint: true },
85
+ }, guard(async ({ id, ...params }) => (await eb.substitutes(id, params)).map((s) => ({ score: s.score, reason: s.reason, exercise: brief(s.exercise) }))));
86
+ if (END_USER)
87
+ server.registerTool("get_media_urls", {
88
+ title: "Signed video and still URLs",
89
+ description: "Short-lived signed URLs (video clip and still) for up to 50 exercise ids, issued for the person named in EB_END_USER. They expire, 15 minutes by default: show them now, store exercise ids. A sandbox key gets media only for the ids in get_vocabulary.sandboxMediaSample.",
90
+ inputSchema: {
91
+ ids: z.array(z.string()).min(1).max(50),
92
+ kinds: z.array(z.enum(["mp4", "still"])).optional().describe("Default both"),
93
+ },
94
+ annotations: { readOnlyHint: true },
95
+ }, guard(({ ids, kinds }) => eb.mediaUrls({ endUser: END_USER, ids, ...(kinds ? { kinds } : {}) })));
96
+ await server.connect(new StdioServerTransport());
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "exercisebank-mcp",
3
+ "version": "0.1.0",
4
+ "description": "Model Context Protocol server for the ExerciseBank API (exercisebank.net): search 1,492 filmed exercises by muscle, equipment, movement pattern, difficulty and joints to avoid; get substitutes with reasons.",
5
+ "keywords": [
6
+ "mcp",
7
+ "model-context-protocol",
8
+ "mcp-server",
9
+ "exercisebank",
10
+ "exercise api",
11
+ "exercise video api",
12
+ "fitness api",
13
+ "workout",
14
+ "claude"
15
+ ],
16
+ "homepage": "https://exercisebank.net",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/exercisebank/examples.git",
20
+ "directory": "packages/exercisebank-mcp"
21
+ },
22
+ "bugs": "https://github.com/exercisebank/examples/issues",
23
+ "mcpName": "io.github.exercisebank/exercisebank-mcp",
24
+ "license": "MIT",
25
+ "type": "module",
26
+ "bin": {
27
+ "exercisebank-mcp": "dist/index.js"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "engines": {
35
+ "node": ">=18"
36
+ },
37
+ "scripts": {
38
+ "build": "tsc -p tsconfig.json && chmod +x dist/index.js",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "prepublishOnly": "npm run build"
41
+ },
42
+ "dependencies": {
43
+ "@modelcontextprotocol/sdk": "^1.30.0",
44
+ "exercisebank": "^0.1.0",
45
+ "zod": "^3.25.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^24.0.0",
49
+ "typescript": "^5.9.0"
50
+ }
51
+ }