mcp-grocy 1.9.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 +23 -0
- package/README.md +285 -0
- package/build/api/client.js +154 -0
- package/build/config/environment.js +142 -0
- package/build/main.js +29 -0
- package/build/resources/CHANGELOG.md +352 -0
- package/build/resources/DOCS.md +62 -0
- package/build/resources/README.md +285 -0
- package/build/resources/api-reference.md +92 -0
- package/build/resources/config.md +282 -0
- package/build/resources/examples.md +319 -0
- package/build/resources/installation.md +117 -0
- package/build/resources/response-format.md +165 -0
- package/build/server/http-server.js +214 -0
- package/build/server/mcp-server.js +173 -0
- package/build/server/resources.js +60 -0
- package/build/tools/base.js +56 -0
- package/build/tools/index.js +494 -0
- package/build/tools/products/definitions.js +57 -0
- package/build/tools/products/handlers.js +78 -0
- package/build/tools/products/index.js +13 -0
- package/build/tools/recipes/definitions.js +191 -0
- package/build/tools/recipes/handlers.js +258 -0
- package/build/tools/recipes/index.js +18 -0
- package/build/tools/shopping/index.js +107 -0
- package/build/tools/stock/definitions.js +234 -0
- package/build/tools/stock/handlers.js +391 -0
- package/build/tools/stock/index.js +19 -0
- package/build/tools/system/index.js +230 -0
- package/build/tools/types.js +1 -0
- package/build/version.js +4 -0
- package/package.json +87 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
import { stockModule } from './stock/index.js';
|
|
2
|
+
import { productModule } from './products/index.js';
|
|
3
|
+
import { recipeModule } from './recipes/index.js';
|
|
4
|
+
import { shoppingModule } from './shopping/index.js';
|
|
5
|
+
import { systemModule } from './system/index.js';
|
|
6
|
+
// Import additional tool modules for completeness
|
|
7
|
+
import { BaseToolHandler } from './base.js';
|
|
8
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
9
|
+
import apiClient from '../api/client.js';
|
|
10
|
+
// Meal Plan Tools
|
|
11
|
+
const mealPlanToolDefinitions = [
|
|
12
|
+
{
|
|
13
|
+
name: 'get_meal_plan',
|
|
14
|
+
description: 'Get your meal plan data from Grocy instance with corresponding recipe details. Returns planned meals for the requested date plus surrounding days for context. Use this to find out what recipes/meals are planned for a specific date (e.g., "what\'s for dinner tomorrow", "recipes for today", "meal plan for next week"). The returned data includes the id field (meal plan entry ID) which can be used with delete_recipe_from_meal_plan.',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
date: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Date in YYYY-MM-DD format (e.g., "2024-12-25"). The tool will return meal plans for this date plus the previous and next day for better context.'
|
|
21
|
+
},
|
|
22
|
+
weekly: {
|
|
23
|
+
type: 'boolean',
|
|
24
|
+
description: 'If true, returns the entire calendar week containing the specified date.'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
required: ['date']
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'get_meal_plan_sections',
|
|
32
|
+
description: 'Get all available meal plan sections from your Grocy instance (e.g., Breakfast, Lunch, Dinner, Snacks). Use this to find valid section IDs for add_recipe_to_meal_plan.',
|
|
33
|
+
inputSchema: {
|
|
34
|
+
type: 'object',
|
|
35
|
+
properties: {},
|
|
36
|
+
required: []
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'add_recipe_to_meal_plan',
|
|
41
|
+
description: 'Add a recipe to the meal plan for a specific date and meal section. Use get_recipes to find recipe IDs and get_meal_plan_sections to find valid section IDs and their names.',
|
|
42
|
+
inputSchema: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
properties: {
|
|
45
|
+
recipeId: {
|
|
46
|
+
type: 'number',
|
|
47
|
+
description: 'ID of the recipe to add to the meal plan. Use get_recipes tool to find valid recipe IDs and their names.'
|
|
48
|
+
},
|
|
49
|
+
day: {
|
|
50
|
+
type: 'string',
|
|
51
|
+
description: 'Day to add the recipe to in YYYY-MM-DD format (e.g., "2024-12-25").'
|
|
52
|
+
},
|
|
53
|
+
servings: {
|
|
54
|
+
type: 'number',
|
|
55
|
+
description: 'Number of servings for this meal plan entry (e.g., 2 for a family of two, 4 for a family of four).'
|
|
56
|
+
},
|
|
57
|
+
sectionId: {
|
|
58
|
+
type: 'number',
|
|
59
|
+
description: 'ID of the meal plan section that defines when this meal will be consumed (e.g., breakfast, lunch, dinner, snacks). Use get_meal_plan_sections tool to discover what sections are available in your Grocy instance and get their specific IDs and names.'
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
required: ['recipeId', 'day', 'servings', 'sectionId']
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'delete_recipe_from_meal_plan',
|
|
67
|
+
description: 'Delete a specific recipe entry from the meal plan. Use get_meal_plan to find the mealPlanEntryId of the entry you want to remove.',
|
|
68
|
+
inputSchema: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
date: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Date of the meal plan entry in YYYY-MM-DD format (e.g., "2024-12-25").'
|
|
74
|
+
},
|
|
75
|
+
mealPlanEntryId: {
|
|
76
|
+
type: 'number',
|
|
77
|
+
description: 'ID of the specific meal plan entry to delete. Use get_meal_plan to find the correct entry ID.'
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
required: ['date', 'mealPlanEntryId']
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
class MealPlanToolHandlers extends BaseToolHandler {
|
|
85
|
+
getMealPlan = async (args) => {
|
|
86
|
+
const requestedDate = args?.date || new Date().toISOString().split('T')[0];
|
|
87
|
+
const weekly = args?.weekly || false;
|
|
88
|
+
let datesToFetch = [];
|
|
89
|
+
let startDate, endDate;
|
|
90
|
+
if (weekly) {
|
|
91
|
+
// Calculate the calendar week (Monday to Sunday) for the requested date
|
|
92
|
+
const date = new Date(requestedDate);
|
|
93
|
+
const dayOfWeek = date.getDay(); // 0 = Sunday, 1 = Monday, etc.
|
|
94
|
+
// Find Monday of the week (start of calendar week)
|
|
95
|
+
const monday = new Date(date);
|
|
96
|
+
const daysFromMonday = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Handle Sunday case
|
|
97
|
+
monday.setDate(date.getDate() - daysFromMonday);
|
|
98
|
+
// Add one day before Monday and one day after Sunday
|
|
99
|
+
startDate = new Date(monday);
|
|
100
|
+
startDate.setDate(monday.getDate() - 1); // Day before Monday
|
|
101
|
+
endDate = new Date(monday);
|
|
102
|
+
endDate.setDate(monday.getDate() + 8); // Day after Sunday (Monday + 7 days + 1)
|
|
103
|
+
// Generate all dates from startDate to endDate
|
|
104
|
+
const currentDate = new Date(startDate);
|
|
105
|
+
while (currentDate <= endDate) {
|
|
106
|
+
datesToFetch.push(currentDate.toISOString().split('T')[0]);
|
|
107
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
// Original behavior: previous + requested + next day
|
|
112
|
+
const date = new Date(requestedDate);
|
|
113
|
+
const previousDay = new Date(date);
|
|
114
|
+
previousDay.setDate(date.getDate() - 1);
|
|
115
|
+
const nextDay = new Date(date);
|
|
116
|
+
nextDay.setDate(date.getDate() + 1);
|
|
117
|
+
startDate = previousDay;
|
|
118
|
+
endDate = nextDay;
|
|
119
|
+
datesToFetch = [
|
|
120
|
+
previousDay.toISOString().split('T')[0],
|
|
121
|
+
requestedDate,
|
|
122
|
+
nextDay.toISOString().split('T')[0]
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
// Get meal plan entries for all requested days
|
|
127
|
+
const mealPlanResponses = await Promise.allSettled(datesToFetch.map(dateStr => apiClient.get('/objects/meal_plan', {
|
|
128
|
+
queryParams: { 'query[]': `day=${dateStr}`, limit: '100' }
|
|
129
|
+
})));
|
|
130
|
+
// Combine all meal plan data
|
|
131
|
+
const allMealPlanData = [];
|
|
132
|
+
const dayResults = {};
|
|
133
|
+
datesToFetch.forEach((dateStr, index) => {
|
|
134
|
+
const response = mealPlanResponses[index];
|
|
135
|
+
const entries = response.status === 'fulfilled' ? (response.value.data || []) : [];
|
|
136
|
+
dayResults[dateStr] = entries;
|
|
137
|
+
if (Array.isArray(entries)) {
|
|
138
|
+
allMealPlanData.push(...entries);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
if (allMealPlanData.length === 0) {
|
|
142
|
+
const dateRange = weekly
|
|
143
|
+
? {
|
|
144
|
+
start_date: startDate.toISOString().split('T')[0],
|
|
145
|
+
end_date: endDate.toISOString().split('T')[0],
|
|
146
|
+
week_dates: datesToFetch
|
|
147
|
+
}
|
|
148
|
+
: {
|
|
149
|
+
previous_day: datesToFetch[0],
|
|
150
|
+
requested_day: requestedDate,
|
|
151
|
+
next_day: datesToFetch[2]
|
|
152
|
+
};
|
|
153
|
+
return this.createSuccessResult({
|
|
154
|
+
message: weekly
|
|
155
|
+
? 'No meals planned for the requested week or surrounding days'
|
|
156
|
+
: 'No meals planned for the requested date or surrounding days',
|
|
157
|
+
requested_date: requestedDate,
|
|
158
|
+
weekly,
|
|
159
|
+
date_range: dateRange,
|
|
160
|
+
meal_plan_by_date: dayResults
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// Extract unique recipe IDs from all meal plan entries
|
|
164
|
+
const recipeIds = [...new Set(allMealPlanData.map((entry) => entry.recipe_id).filter(id => id))];
|
|
165
|
+
// Fetch recipe details and sections in parallel
|
|
166
|
+
const [recipeResponses, sectionsResponse] = await Promise.allSettled([
|
|
167
|
+
Promise.allSettled(recipeIds.map(async (recipeId) => {
|
|
168
|
+
try {
|
|
169
|
+
const recipe = await apiClient.get(`/objects/recipes/${recipeId}`);
|
|
170
|
+
return { id: recipeId, ...recipe.data };
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
return { id: recipeId, name: `Recipe ${recipeId} (details unavailable)`, error: error.message };
|
|
174
|
+
}
|
|
175
|
+
})),
|
|
176
|
+
apiClient.get('/objects/meal_plan_sections')
|
|
177
|
+
]);
|
|
178
|
+
const recipes = recipeResponses.status === 'fulfilled' ?
|
|
179
|
+
recipeResponses.value.filter(r => r.status === 'fulfilled').map(r => r.value) : [];
|
|
180
|
+
const sections = sectionsResponse.status === 'fulfilled' ?
|
|
181
|
+
(Array.isArray(sectionsResponse.value.data) ? sectionsResponse.value.data : []) : [];
|
|
182
|
+
const recipesMap = recipes.reduce((acc, recipe) => {
|
|
183
|
+
acc[recipe.id] = recipe;
|
|
184
|
+
return acc;
|
|
185
|
+
}, {});
|
|
186
|
+
const sectionsMap = sections.reduce((acc, section) => {
|
|
187
|
+
acc[section.id] = section;
|
|
188
|
+
return acc;
|
|
189
|
+
}, {});
|
|
190
|
+
// Enhance meal plan entries with recipe and section details for each day
|
|
191
|
+
const enhancedMealPlanByDate = {};
|
|
192
|
+
Object.entries(dayResults).forEach(([date, entries]) => {
|
|
193
|
+
if (Array.isArray(entries)) {
|
|
194
|
+
enhancedMealPlanByDate[date] = entries.map((entry) => ({
|
|
195
|
+
...entry,
|
|
196
|
+
recipe_details: recipesMap[entry.recipe_id] || { name: `Recipe ${entry.recipe_id} (not found)` },
|
|
197
|
+
section_details: sectionsMap[entry.section_id] || { name: `Section ${entry.section_id}` }
|
|
198
|
+
}));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
enhancedMealPlanByDate[date] = [];
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const dateRange = weekly
|
|
205
|
+
? {
|
|
206
|
+
start_date: startDate.toISOString().split('T')[0],
|
|
207
|
+
end_date: endDate.toISOString().split('T')[0],
|
|
208
|
+
week_dates: datesToFetch
|
|
209
|
+
}
|
|
210
|
+
: {
|
|
211
|
+
previous_day: datesToFetch[0],
|
|
212
|
+
requested_day: requestedDate,
|
|
213
|
+
next_day: datesToFetch[2]
|
|
214
|
+
};
|
|
215
|
+
return this.createSuccessResult({
|
|
216
|
+
requested_date: requestedDate,
|
|
217
|
+
weekly,
|
|
218
|
+
date_range: dateRange,
|
|
219
|
+
meal_plan_by_date: enhancedMealPlanByDate,
|
|
220
|
+
all_available_meal_sections: sections.map((section) => ({
|
|
221
|
+
id: section.id,
|
|
222
|
+
name: section.name,
|
|
223
|
+
sort_number: section.sort_number
|
|
224
|
+
}))
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (error) {
|
|
228
|
+
return this.createErrorResult(`Failed to get meal plan: ${error.message}`, { requested_date: requestedDate, weekly });
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
getMealPlanSections = async (_args) => {
|
|
232
|
+
return this.handleApiCall('/objects/meal_plan_sections', 'Get all meal plan sections');
|
|
233
|
+
};
|
|
234
|
+
addRecipeToMealPlan = async (args) => {
|
|
235
|
+
const { recipeId, day, servings, sectionId } = args || {};
|
|
236
|
+
if (!recipeId) {
|
|
237
|
+
throw new McpError(ErrorCode.InvalidParams, 'recipeId is required. Use get_recipes tool to find valid recipe IDs.');
|
|
238
|
+
}
|
|
239
|
+
if (!day) {
|
|
240
|
+
throw new McpError(ErrorCode.InvalidParams, 'day is required. Specify the date in YYYY-MM-DD format (e.g., "2024-12-25").');
|
|
241
|
+
}
|
|
242
|
+
if (!servings) {
|
|
243
|
+
throw new McpError(ErrorCode.InvalidParams, 'servings is required. Specify how many servings to plan for this meal (e.g., 2, 4).');
|
|
244
|
+
}
|
|
245
|
+
if (!sectionId) {
|
|
246
|
+
throw new McpError(ErrorCode.InvalidParams, 'sectionId is required. Use get_meal_plan_sections tool to find valid section IDs and their names.');
|
|
247
|
+
}
|
|
248
|
+
const body = {
|
|
249
|
+
day,
|
|
250
|
+
recipe_id: recipeId,
|
|
251
|
+
recipe_servings: servings,
|
|
252
|
+
section_id: sectionId,
|
|
253
|
+
type: "recipe"
|
|
254
|
+
};
|
|
255
|
+
return this.handleApiCall('/objects/meal_plan', 'Add recipe to meal plan', {
|
|
256
|
+
method: 'POST',
|
|
257
|
+
body
|
|
258
|
+
});
|
|
259
|
+
};
|
|
260
|
+
deleteRecipeFromMealPlan = async (args) => {
|
|
261
|
+
const { date, mealPlanEntryId } = args || {};
|
|
262
|
+
if (!date) {
|
|
263
|
+
throw new McpError(ErrorCode.InvalidParams, 'date is required. Specify the date in YYYY-MM-DD format (e.g., "2024-12-25").');
|
|
264
|
+
}
|
|
265
|
+
if (!mealPlanEntryId) {
|
|
266
|
+
throw new McpError(ErrorCode.InvalidParams, 'mealPlanEntryId is required. Use get_meal_plan to find the correct entry ID.');
|
|
267
|
+
}
|
|
268
|
+
return this.handleApiCall(`/objects/meal_plan/${mealPlanEntryId}`, 'Delete recipe from meal plan', {
|
|
269
|
+
method: 'DELETE'
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
// Action Tools (chores, tasks, batteries, undo)
|
|
274
|
+
const actionToolDefinitions = [
|
|
275
|
+
{
|
|
276
|
+
name: 'track_chore_execution',
|
|
277
|
+
description: 'Track execution of a chore in your Grocy instance. Use get_chores to find chore IDs and get_users to find user IDs.',
|
|
278
|
+
inputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
choreId: {
|
|
282
|
+
type: 'number',
|
|
283
|
+
description: 'ID of the chore that was executed. Use get_chores tool to find the correct chore ID by name.'
|
|
284
|
+
},
|
|
285
|
+
executedBy: {
|
|
286
|
+
type: 'number',
|
|
287
|
+
description: 'ID of the user who executed the chore (optional). Use get_users tool to find available user IDs and names.'
|
|
288
|
+
},
|
|
289
|
+
trackedTime: {
|
|
290
|
+
type: 'string',
|
|
291
|
+
description: 'When the chore was executed in YYYY-MM-DD HH:MM:SS format (default: now)'
|
|
292
|
+
},
|
|
293
|
+
note: {
|
|
294
|
+
type: 'string',
|
|
295
|
+
description: 'Optional note'
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
required: ['choreId']
|
|
299
|
+
}
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
name: 'complete_task',
|
|
303
|
+
description: 'Mark a task as completed in your Grocy instance. Use get_tasks first to find the task ID.',
|
|
304
|
+
inputSchema: {
|
|
305
|
+
type: 'object',
|
|
306
|
+
properties: {
|
|
307
|
+
taskId: {
|
|
308
|
+
type: 'number',
|
|
309
|
+
description: 'ID of the task to complete. Use get_tasks tool to find the correct task ID by name or description.'
|
|
310
|
+
},
|
|
311
|
+
note: {
|
|
312
|
+
type: 'string',
|
|
313
|
+
description: 'Optional note'
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
required: ['taskId']
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'charge_battery',
|
|
321
|
+
description: 'Track charging of a battery in your Grocy instance. Use get_batteries first to find the battery ID.',
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
batteryId: {
|
|
326
|
+
type: 'number',
|
|
327
|
+
description: 'ID of the battery that was charged. Use get_batteries tool to find the correct battery ID by name.'
|
|
328
|
+
},
|
|
329
|
+
trackedTime: {
|
|
330
|
+
type: 'string',
|
|
331
|
+
description: 'When the battery was charged in YYYY-MM-DD HH:MM:SS format (default: now)'
|
|
332
|
+
},
|
|
333
|
+
note: {
|
|
334
|
+
type: 'string',
|
|
335
|
+
description: 'Optional note'
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
required: ['batteryId']
|
|
339
|
+
}
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'undo_action',
|
|
343
|
+
description: 'Undo an action for different entity types (chores, batteries, tasks).',
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: 'object',
|
|
346
|
+
properties: {
|
|
347
|
+
entityType: {
|
|
348
|
+
type: 'string',
|
|
349
|
+
description: 'Type of entity (chores, batteries, tasks)',
|
|
350
|
+
enum: ['chores', 'batteries', 'tasks']
|
|
351
|
+
},
|
|
352
|
+
id: {
|
|
353
|
+
type: 'string',
|
|
354
|
+
description: 'ID of the execution, charge cycle, or task'
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
required: ['entityType', 'id']
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
];
|
|
361
|
+
class ActionToolHandlers extends BaseToolHandler {
|
|
362
|
+
trackChoreExecution = async (args) => {
|
|
363
|
+
const { choreId, executedBy, trackedTime, note } = args || {};
|
|
364
|
+
if (!choreId) {
|
|
365
|
+
throw new McpError(ErrorCode.InvalidParams, 'choreId is required');
|
|
366
|
+
}
|
|
367
|
+
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
368
|
+
const body = {
|
|
369
|
+
tracked_time: trackedTime || timestamp,
|
|
370
|
+
...(executedBy ? { done_by: executedBy } : {}),
|
|
371
|
+
...(note ? { note } : {})
|
|
372
|
+
};
|
|
373
|
+
return this.handleApiCall(`/chores/${choreId}/execute`, 'Track chore execution', {
|
|
374
|
+
method: 'POST',
|
|
375
|
+
body
|
|
376
|
+
});
|
|
377
|
+
};
|
|
378
|
+
completeTask = async (args) => {
|
|
379
|
+
const { taskId, note } = args || {};
|
|
380
|
+
if (!taskId) {
|
|
381
|
+
throw new McpError(ErrorCode.InvalidParams, 'taskId is required');
|
|
382
|
+
}
|
|
383
|
+
return this.handleApiCall(`/tasks/${taskId}/complete`, 'Complete task', {
|
|
384
|
+
method: 'POST',
|
|
385
|
+
body: note ? { note } : {}
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
chargeBattery = async (args) => {
|
|
389
|
+
const { batteryId, trackedTime, note } = args || {};
|
|
390
|
+
if (!batteryId) {
|
|
391
|
+
throw new McpError(ErrorCode.InvalidParams, 'batteryId is required');
|
|
392
|
+
}
|
|
393
|
+
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
394
|
+
const body = {
|
|
395
|
+
tracked_time: trackedTime || timestamp,
|
|
396
|
+
...(note ? { note } : {})
|
|
397
|
+
};
|
|
398
|
+
return this.handleApiCall(`/batteries/${batteryId}/charge`, 'Charge battery', {
|
|
399
|
+
method: 'POST',
|
|
400
|
+
body
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
undoAction = async (args) => {
|
|
404
|
+
const { entityType, id } = args;
|
|
405
|
+
let endpoint;
|
|
406
|
+
switch (entityType.toLowerCase()) {
|
|
407
|
+
case 'chore':
|
|
408
|
+
case 'chores':
|
|
409
|
+
endpoint = `/chores/executions/${id}/undo`;
|
|
410
|
+
break;
|
|
411
|
+
case 'battery':
|
|
412
|
+
case 'batteries':
|
|
413
|
+
endpoint = `/batteries/charge-cycles/${id}/undo`;
|
|
414
|
+
break;
|
|
415
|
+
case 'task':
|
|
416
|
+
case 'tasks':
|
|
417
|
+
endpoint = `/tasks/${id}/undo`;
|
|
418
|
+
break;
|
|
419
|
+
default:
|
|
420
|
+
return this.createErrorResult(`Unsupported entity type: ${entityType}`);
|
|
421
|
+
}
|
|
422
|
+
return this.handleApiCall(endpoint, `Undo ${entityType} action`, {
|
|
423
|
+
method: 'POST'
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// Create handler instances
|
|
428
|
+
const mealPlanHandlers = new MealPlanToolHandlers();
|
|
429
|
+
const actionHandlers = new ActionToolHandlers();
|
|
430
|
+
// Additional modules
|
|
431
|
+
const mealPlanModule = {
|
|
432
|
+
definitions: mealPlanToolDefinitions,
|
|
433
|
+
handlers: {
|
|
434
|
+
get_meal_plan: mealPlanHandlers.getMealPlan,
|
|
435
|
+
get_meal_plan_sections: mealPlanHandlers.getMealPlanSections,
|
|
436
|
+
add_recipe_to_meal_plan: mealPlanHandlers.addRecipeToMealPlan,
|
|
437
|
+
delete_recipe_from_meal_plan: mealPlanHandlers.deleteRecipeFromMealPlan
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const actionModule = {
|
|
441
|
+
definitions: actionToolDefinitions,
|
|
442
|
+
handlers: {
|
|
443
|
+
track_chore_execution: actionHandlers.trackChoreExecution,
|
|
444
|
+
complete_task: actionHandlers.completeTask,
|
|
445
|
+
charge_battery: actionHandlers.chargeBattery,
|
|
446
|
+
undo_action: actionHandlers.undoAction
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
// Registry of all tool modules
|
|
450
|
+
const toolModules = [
|
|
451
|
+
stockModule,
|
|
452
|
+
productModule,
|
|
453
|
+
recipeModule,
|
|
454
|
+
shoppingModule,
|
|
455
|
+
systemModule,
|
|
456
|
+
mealPlanModule,
|
|
457
|
+
actionModule
|
|
458
|
+
];
|
|
459
|
+
// Tool Registry class
|
|
460
|
+
export class ToolRegistry {
|
|
461
|
+
definitions = [];
|
|
462
|
+
handlers = {};
|
|
463
|
+
constructor() {
|
|
464
|
+
this.registerModules();
|
|
465
|
+
}
|
|
466
|
+
registerModules() {
|
|
467
|
+
for (const module of toolModules) {
|
|
468
|
+
// Add definitions
|
|
469
|
+
this.definitions.push(...module.definitions);
|
|
470
|
+
// Add handlers
|
|
471
|
+
Object.assign(this.handlers, module.handlers);
|
|
472
|
+
}
|
|
473
|
+
console.error(`[TOOLS] Registered ${this.definitions.length} tools`);
|
|
474
|
+
}
|
|
475
|
+
getDefinitions() {
|
|
476
|
+
return this.definitions;
|
|
477
|
+
}
|
|
478
|
+
getHandler(name) {
|
|
479
|
+
return this.handlers[name];
|
|
480
|
+
}
|
|
481
|
+
hasHandler(name) {
|
|
482
|
+
return name in this.handlers;
|
|
483
|
+
}
|
|
484
|
+
getToolNames() {
|
|
485
|
+
return this.definitions.map(def => def.name);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Export singleton instance
|
|
489
|
+
export const toolRegistry = new ToolRegistry();
|
|
490
|
+
export default toolRegistry;
|
|
491
|
+
// Export types and modules for testing
|
|
492
|
+
export * from './types.js';
|
|
493
|
+
export * from './base.js';
|
|
494
|
+
export { stockModule, productModule, recipeModule, shoppingModule, systemModule, mealPlanModule, actionModule };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export const productToolDefinitions = [
|
|
2
|
+
{
|
|
3
|
+
name: 'get_products',
|
|
4
|
+
description: 'Get specific fields for all products from your Grocy instance. You must specify which fields to retrieve.',
|
|
5
|
+
inputSchema: {
|
|
6
|
+
type: 'object',
|
|
7
|
+
properties: {
|
|
8
|
+
fields: {
|
|
9
|
+
type: 'array',
|
|
10
|
+
items: {
|
|
11
|
+
type: 'string',
|
|
12
|
+
enum: ['id', 'name', 'description', 'product_group_id', 'active', 'location_id', 'shopping_location_id', 'qu_id_purchase', 'qu_id_stock', 'qu_factor_purchase_to_stock', 'min_stock_amount', 'default_best_before_days', 'default_best_before_days_after_open', 'default_best_before_days_after_freezing', 'default_best_before_days_after_thawing', 'picture_file_name', 'allow_label_per_unit', 'energy_per_stock_unit', 'calories_per_stock_unit', 'default_stock_label_type', 'should_not_be_frozen', 'treat_opened_as_out_of_stock', 'no_own_stock', 'cumulate_min_stock_amount_of_sub_products', 'parent_product_id', 'calories_per_unit_factor', 'quick_consume_amount', 'hide_on_stock_overview']
|
|
13
|
+
},
|
|
14
|
+
description: 'Array of field names to retrieve. For basic lookup use ["id", "name"]. For detailed info include ["id", "name", "description", "active"]. Available fields: id, name, description, product_group_id, active, location_id, shopping_location_id, qu_id_purchase, qu_id_stock, qu_factor_purchase_to_stock, min_stock_amount, default_best_before_days, default_best_before_days_after_open, default_best_before_days_after_freezing, default_best_before_days_after_thawing, picture_file_name, allow_label_per_unit, energy_per_stock_unit, calories_per_stock_unit, default_stock_label_type, should_not_be_frozen, treat_opened_as_out_of_stock, no_own_stock, cumulate_min_stock_amount_of_sub_products, parent_product_id, calories_per_unit_factor, quick_consume_amount, hide_on_stock_overview'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
required: ['fields']
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'get_stock_by_product',
|
|
22
|
+
description: 'Get stock entries for a specific product in your Grocy instance with filtered essential information.',
|
|
23
|
+
inputSchema: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
productId: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
description: 'ID of the product to get stock entries for. Use get_products tool to find the correct product ID by name.'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
required: ['productId']
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'get_price_history',
|
|
36
|
+
description: 'Get the price history of a product from your Grocy instance.',
|
|
37
|
+
inputSchema: {
|
|
38
|
+
type: 'object',
|
|
39
|
+
properties: {
|
|
40
|
+
productId: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
description: 'ID of the product to get price history for. Use get_products tool to find the correct product ID by name.'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
required: ['productId']
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'get_product_groups',
|
|
50
|
+
description: 'Get all product groups from your Grocy instance.',
|
|
51
|
+
inputSchema: {
|
|
52
|
+
type: 'object',
|
|
53
|
+
properties: {},
|
|
54
|
+
required: []
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base.js';
|
|
2
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import apiClient from '../../api/client.js';
|
|
4
|
+
export class ProductToolHandlers extends BaseToolHandler {
|
|
5
|
+
getProducts = async (args) => {
|
|
6
|
+
const { fields } = args || {};
|
|
7
|
+
if (!fields || !Array.isArray(fields) || fields.length === 0) {
|
|
8
|
+
throw new McpError(ErrorCode.InvalidParams, 'fields parameter is required and must be a non-empty array of field names');
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const response = await apiClient.get('/objects/products');
|
|
12
|
+
const products = response.data || [];
|
|
13
|
+
// Filter the response to only include requested fields
|
|
14
|
+
const filteredData = Array.isArray(products)
|
|
15
|
+
? products.map((item) => {
|
|
16
|
+
const filtered = {};
|
|
17
|
+
fields.forEach(field => {
|
|
18
|
+
if (item.hasOwnProperty(field)) {
|
|
19
|
+
filtered[field] = item[field];
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
return filtered;
|
|
23
|
+
})
|
|
24
|
+
: products;
|
|
25
|
+
return this.createSuccessResult(filteredData);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return this.createErrorResult(`Failed to get products: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
getStockByProduct = async (args) => {
|
|
32
|
+
const { productId } = args || {};
|
|
33
|
+
if (!productId) {
|
|
34
|
+
throw new McpError(ErrorCode.InvalidParams, 'productId is required');
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const response = await apiClient.get(`/stock/products/${productId}/entries`);
|
|
38
|
+
const stockEntries = response.data || [];
|
|
39
|
+
// Define the fields we want to keep for each stock entry
|
|
40
|
+
const entryFields = [
|
|
41
|
+
'id',
|
|
42
|
+
'amount',
|
|
43
|
+
'best_before_date',
|
|
44
|
+
'purchased_date',
|
|
45
|
+
'stock_id',
|
|
46
|
+
'note',
|
|
47
|
+
'location_id'
|
|
48
|
+
];
|
|
49
|
+
// Filter entries to only include essential fields
|
|
50
|
+
const filteredEntries = Array.isArray(stockEntries)
|
|
51
|
+
? stockEntries.map((entry) => {
|
|
52
|
+
const filteredEntry = entryFields.reduce((filtered, field) => {
|
|
53
|
+
if (entry.hasOwnProperty(field)) {
|
|
54
|
+
filtered[field] = entry[field];
|
|
55
|
+
}
|
|
56
|
+
return filtered;
|
|
57
|
+
}, {});
|
|
58
|
+
return filteredEntry;
|
|
59
|
+
})
|
|
60
|
+
: [];
|
|
61
|
+
return this.createSuccessResult(filteredEntries);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return this.createErrorResult(`Failed to get stock by product: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
getPriceHistory = async (args) => {
|
|
68
|
+
const { productId } = args || {};
|
|
69
|
+
if (!productId) {
|
|
70
|
+
throw new McpError(ErrorCode.InvalidParams, 'productId is required');
|
|
71
|
+
}
|
|
72
|
+
return this.handleApiCall(`/stock/products/${productId}/price-history`, 'Get product price history');
|
|
73
|
+
};
|
|
74
|
+
getProductGroups = async (args) => {
|
|
75
|
+
return this.handleApiCall('/objects/product_groups', 'Get all product groups');
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export const productHandlers = new ProductToolHandlers();
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { productToolDefinitions } from './definitions.js';
|
|
2
|
+
import { productHandlers } from './handlers.js';
|
|
3
|
+
export const productModule = {
|
|
4
|
+
definitions: productToolDefinitions,
|
|
5
|
+
handlers: {
|
|
6
|
+
get_products: productHandlers.getProducts,
|
|
7
|
+
get_stock_by_product: productHandlers.getStockByProduct,
|
|
8
|
+
get_price_history: productHandlers.getPriceHistory,
|
|
9
|
+
get_product_groups: productHandlers.getProductGroups
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
export * from './definitions.js';
|
|
13
|
+
export * from './handlers.js';
|