Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This server implements the [Model Context Protocol](https://modelcontextprotocol

## Key Features

- **8 Powerful Tools**: Comprehensive exercise discovery and workout management capabilities
- **12 Powerful Tools**: Comprehensive exercise discovery and workout management capabilities
- **Type-Safe**: Built with TypeScript in strict mode with full type definitions
- **Intelligent Caching**: Automatic caching of static data to minimize API calls
- **Robust Authentication**: JWT-based auth with automatic token refresh
Expand All @@ -41,7 +41,7 @@ This server implements the [Model Context Protocol](https://modelcontextprotocol
**Option 1: Using Claude Code CLI (Recommended)**

```bash
claude mcp add wger -e WGER_API_KEY=your_key_here -- npx -y @juxsta/wger-mcp
claude mcp add wger -e WGER_API_KEY=your_key_here -- npx -y @juxsta/wger-mcp@1.1.3
```

**Option 2: Install globally via npm**
Expand Down Expand Up @@ -107,7 +107,10 @@ For detailed setup instructions, see [SETUP.md](docs/SETUP.md).
### Workout Management (Authentication Required)

- **`create_workout`** - Create a new workout routine
- **`add_exercise_to_routine`** - Add exercises to a routine with sets, reps, and weights
- **`get_routine_details`** - Fetch full routine structure (days, slots, exercises)
- **`add_day_to_routine`** / **`update_day`** / **`delete_day`** - Manage scheduling
- **`add_exercise_to_routine`** / **`update_exercise_in_routine`** - Manage exercises and their sets/reps
- **`delete_slot`** - Remove exercises from days
- **`get_user_routines`** - Retrieve all workout routines for the authenticated user

For complete tool documentation, see [API.md](docs/API.md).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@
"ts-jest": "^29.1.0",
"typescript": "^5.2.0"
}
}
}
20 changes: 20 additions & 0 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ import {
addExerciseToRoutineHandler,
} from './tools/add-exercise-to-routine';
import { getUserRoutinesTool, getUserRoutinesHandler } from './tools/get-user-routines';
import { getRoutineDetailsTool, getRoutineDetailsHandler } from './tools/get-routine-details';
import {
addDayToRoutineTool,
addDayToRoutineHandler,
updateDayTool,
updateDayHandler,
deleteDayTool,
deleteDayHandler,
} from './tools/manage-day';
import {
updateExerciseInRoutineTool,
updateExerciseInRoutineHandler,
} from './tools/update-exercise';
import { deleteSlotTool, deleteSlotHandler } from './tools/delete-slot';

// Import diagnostic tool
import { diagnoseTool, diagnoseHandler } from './tools/diagnose';
Expand Down Expand Up @@ -201,6 +215,12 @@ export function createServer(): WgerMCPServer {
server.registerTool(createWorkoutTool, createWorkoutHandler);
server.registerTool(addExerciseToRoutineTool, addExerciseToRoutineHandler);
server.registerTool(getUserRoutinesTool, getUserRoutinesHandler);
server.registerTool(getRoutineDetailsTool, getRoutineDetailsHandler);
server.registerTool(addDayToRoutineTool, addDayToRoutineHandler);
server.registerTool(updateDayTool, updateDayHandler);
server.registerTool(deleteDayTool, deleteDayHandler);
server.registerTool(updateExerciseInRoutineTool, updateExerciseInRoutineHandler);
server.registerTool(deleteSlotTool, deleteSlotHandler);

// Register diagnostic tool
server.registerTool(diagnoseTool, diagnoseHandler);
Expand Down
4 changes: 3 additions & 1 deletion src/tools/add-exercise-to-routine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ export async function addExerciseToRoutineHandler(
params: { routine: routineId },
});

const existingDays = daysResponse.results.map((d) => DaySchema.parse(d));
const existingDays = daysResponse.results
.map((d) => DaySchema.parse(d))
.filter((d) => d.routine === routineId);
const existingDay = existingDays.find((d) => d.name === dayNameToUse);

if (existingDay) {
Expand Down
40 changes: 40 additions & 0 deletions src/tools/delete-slot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* MCP tool for deleting a slot (exercise) from a routine
*/

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { wgerClient } from '../client/wger-client';
import { authManager } from '../client/auth';
import { z } from 'zod';
import { AuthenticationError } from '../utils/errors';
import { logger } from '../utils/logger';

const DeleteSlotSchema = z.object({
slotId: z.number().describe('ID of the slot to delete'),
});

export const deleteSlotTool: Tool = {
name: 'delete_slot',
description: 'Remove a slot (containing an exercise) from a routine day.',
inputSchema: {
type: 'object',
properties: {
slotId: { type: 'number', description: 'ID of the slot to delete' },
},
required: ['slotId'],
},
};

export async function deleteSlotHandler(
args: Record<string, unknown>
): Promise<{ success: boolean; id: number }> {
if (!authManager.hasCredentials()) throw new AuthenticationError('Authentication required.');
const input = DeleteSlotSchema.parse(args);
await authManager.getToken();

// Note: In wger API v2, deleting the slot removes the slot entries associated with it.
await wgerClient.delete(`/slot/${input.slotId}/`);

logger.info(`Deleted slot ${input.slotId}`);
return { success: true, id: input.slotId };
}
163 changes: 163 additions & 0 deletions src/tools/get-routine-details.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
/**
* MCP tool for retrieving detailed routine information
* Fetches the routine, its days, slots, and slot entries in a nested structure
*/

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { wgerClient } from '../client/wger-client';
import { authManager } from '../client/auth';
import { z } from 'zod';
import {
Routine,
Day,
Slot,
SlotEntry,
SetsConfig,
RepetitionsConfig,
WeightConfig,
} from '../types/wger';
import { AuthenticationError } from '../utils/errors';
import { logger } from '../utils/logger';

// Schema for input validation
const GetRoutineDetailsSchema = z.object({
routineId: z.number().describe('ID of the routine to fetch details for'),
});

export const getRoutineDetailsTool: Tool = {
name: 'get_routine_details',
description:
'Fetch detailed information about a routine, including its days, slots, and exercises (slot entries). Useful for understanding the full schedule of a routine.',
inputSchema: {
type: 'object',
properties: {
routineId: {
type: 'number',
description: 'ID of the routine to fetch details for',
},
},
required: ['routineId'],
},
};

// Interface for the detailed response
interface DetailedRoutine extends Routine {
days: Array<
Day & {
slots: Array<
Slot & {
entries: Array<
SlotEntry & {
sets?: number;
reps?: string;
weight?: string;
}
>;
}
>;
}
>;
}

export async function getRoutineDetailsHandler(
args: Record<string, unknown>
): Promise<DetailedRoutine> {
logger.info('Executing get_routine_details tool');

if (!authManager.hasCredentials()) {
throw new AuthenticationError('Authentication required to view routine details.');
}

const { routineId } = GetRoutineDetailsSchema.parse(args);

try {
await authManager.getToken();

// 1. Fetch Routine
const routine = await wgerClient.get<Routine>(`/routine/${routineId}/`);

// 2. Fetch Days
const daysResponse = await wgerClient.get<{ results: Day[] }>('/day/', {
params: { routine: routineId, limit: 100 },
});
// Manually filter by routine ID as API might return all days
const days = daysResponse.results
.filter((d) => d.routine === routineId)
.sort((a, b) => a.order - b.order);

// 3. Fetch all Slots, Entries, and Configs in parallel to minimize latency
// Note: In a production app with huge data, we might want to batch this differently,
// but for individual users, fetching all usually fits within limits.
// However, wger API filtering is limited. We'll fetch by day IDs if possible,
// or we have to fetch all for the routine if the API supports it.
// Looking at wger API, /slot/ filters by day.

// To avoid N+1 problem (looping through days to get slots), we check if we can filter/expand.
// wger API doesn't seem to support deep expansion standardly.
// We will iterate days for now as routines rarely have >7 days.

const populatedDays = await Promise.all(
days.map(async (day) => {
// Get Slots for Day
const slotsResponse = await wgerClient.get<{ results: Slot[] }>('/slot/', {
params: { day: day.id, limit: 100 },
});
const slots = slotsResponse.results.sort((a, b) => a.order - b.order);

const populatedSlots = await Promise.all(
slots.map(async (slot) => {
// Get Entries for Slot
const entriesResponse = await wgerClient.get<{ results: SlotEntry[] }>('/slot-entry/', {
params: { slot: slot.id, limit: 100 },
});
const entries = entriesResponse.results.sort((a, b) => a.order - b.order);

// Fetch configs for entries
const populatedEntries = await Promise.all(
entries.map(async (entry) => {
// We need sets, reps, weight configs.
// These endpoints filter by slot_entry
const [setsRes, repsRes, weightRes] = await Promise.all([
wgerClient.get<{ results: SetsConfig[] }>('/sets-config/', {
params: { slot_entry: entry.id },
}),
wgerClient.get<{ results: RepetitionsConfig[] }>('/repetitions-config/', {
params: { slot_entry: entry.id },
}),
wgerClient.get<{ results: WeightConfig[] }>('/weight-config/', {
params: { slot_entry: entry.id },
}),
]);

return {
...entry,
sets: setsRes.results[0]?.value,
reps: repsRes.results[0]?.value,
weight: weightRes.results[0]?.value,
};
})
);

return {
...slot,
entries: populatedEntries,
};
})
);

return {
...day,
slots: populatedSlots,
};
})
);

return {
...routine,
days: populatedDays,
};
} catch (error) {
logger.error('Failed to get routine details', error instanceof Error ? error : undefined);
throw error;
}
}
Loading
Loading