mcp-grocy 2.5.0 → 2.7.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/CHANGELOG.md +28 -4
- package/README.md +13 -0
- package/build/resources/CHANGELOG.md +30 -4
- package/build/resources/README.md +13 -0
- package/build/resources/config.md +21 -4
- package/build/resources/response-format.md +12 -8
- package/package.json +14 -2
- package/build/api/client.js +0 -119
- package/build/config/index.js +0 -259
- package/build/main.js +0 -44
- package/build/server/http-server.js +0 -330
- package/build/server/mcp-server.js +0 -210
- package/build/server/resources.js +0 -73
- package/build/server/tool-input-zod.js +0 -86
- package/build/tools/base.js +0 -145
- package/build/tools/household/definitions.js +0 -161
- package/build/tools/household/handlers.js +0 -109
- package/build/tools/household/index.js +0 -25
- package/build/tools/index.js +0 -2
- package/build/tools/inventory/definitions.js +0 -416
- package/build/tools/inventory/handlers.js +0 -443
- package/build/tools/inventory/index.js +0 -32
- package/build/tools/module-loader.js +0 -154
- package/build/tools/recipes/definitions.js +0 -278
- package/build/tools/recipes/handlers.js +0 -477
- package/build/tools/recipes/index.js +0 -33
- package/build/tools/recipes/validations.js +0 -29
- package/build/tools/shopping/definitions.js +0 -73
- package/build/tools/shopping/handlers.js +0 -43
- package/build/tools/shopping/index.js +0 -14
- package/build/tools/system/definitions.js +0 -89
- package/build/tools/system/handlers.js +0 -156
- package/build/tools/system/index.js +0 -16
- package/build/tools/types.js +0 -1
- package/build/tools/validation-helpers.js +0 -39
- package/build/types/index.js +0 -64
- package/build/utils/errors.js +0 -143
- package/build/utils/logger.js +0 -142
|
@@ -1,477 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simplified recipe tool handlers
|
|
3
|
-
* Demonstrates the new simplified pattern
|
|
4
|
-
*/
|
|
5
|
-
import { BaseToolHandler } from '../base.js';
|
|
6
|
-
import { InventoryToolHandlers } from '../inventory/handlers.js';
|
|
7
|
-
import { ValidationError } from '../../utils/errors.js';
|
|
8
|
-
const RECIPES_COOKING_COMPLETE_TOOL = 'recipes_cooking_complete';
|
|
9
|
-
export class RecipeToolHandlers extends BaseToolHandler {
|
|
10
|
-
inventoryHandlers = new InventoryToolHandlers();
|
|
11
|
-
/**
|
|
12
|
-
* Helper method to get meal plan for multiple days
|
|
13
|
-
*/
|
|
14
|
-
async getMealPlanForDays(dates) {
|
|
15
|
-
const allResults = [];
|
|
16
|
-
for (const date of dates) {
|
|
17
|
-
const dayString = date.toISOString().split('T')[0];
|
|
18
|
-
const dayResult = await this.apiCall(`/objects/meal_plan`, 'GET', undefined, {
|
|
19
|
-
queryParams: {
|
|
20
|
-
'query[]': `day=${dayString}`,
|
|
21
|
-
order: 'day',
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
if (Array.isArray(dayResult)) {
|
|
25
|
-
allResults.push(...dayResult);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return allResults;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Get recipes with specified fields
|
|
32
|
-
*/
|
|
33
|
-
getRecipes = async (args) => {
|
|
34
|
-
return this.executeToolHandler(async () => {
|
|
35
|
-
const { fields } = args || {};
|
|
36
|
-
// Validate required parameters
|
|
37
|
-
this.validateRequired({ fields }, ['fields']);
|
|
38
|
-
const fieldList = this.parseArrayParam(fields, 'fields');
|
|
39
|
-
// Fetch recipes
|
|
40
|
-
const recipes = await this.apiCall('/objects/recipes', 'GET', undefined, {
|
|
41
|
-
queryParams: { 'query[]': 'type=normal' },
|
|
42
|
-
});
|
|
43
|
-
if (!Array.isArray(recipes)) {
|
|
44
|
-
return this.createSuccess([]);
|
|
45
|
-
}
|
|
46
|
-
// Filter to requested fields
|
|
47
|
-
const filteredRecipes = this.filterFields(recipes, fieldList);
|
|
48
|
-
return this.createSuccess(filteredRecipes);
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Get recipe by ID
|
|
53
|
-
*/
|
|
54
|
-
getRecipeById = async (args) => {
|
|
55
|
-
return this.executeToolHandler(async () => {
|
|
56
|
-
const { recipeId } = args || {};
|
|
57
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
58
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
59
|
-
const recipe = await this.apiCall(`/objects/recipes/${id}`);
|
|
60
|
-
return this.createSuccess(recipe);
|
|
61
|
-
});
|
|
62
|
-
};
|
|
63
|
-
/**
|
|
64
|
-
* Get recipe fulfillment information
|
|
65
|
-
*/
|
|
66
|
-
getRecipeFulfillment = async (args) => {
|
|
67
|
-
return this.executeToolHandler(async () => {
|
|
68
|
-
const { recipeId } = args || {};
|
|
69
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
70
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
71
|
-
const fulfillment = await this.apiCall(`/recipes/${id}/fulfillment`);
|
|
72
|
-
return this.createSuccess(fulfillment);
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
/**
|
|
76
|
-
* Add recipe to meal plan
|
|
77
|
-
*/
|
|
78
|
-
addRecipeToMealPlan = async (args) => {
|
|
79
|
-
return this.executeToolHandler(async () => {
|
|
80
|
-
const { recipeId, day, mealType } = args || {};
|
|
81
|
-
this.validateRequired({ recipeId, day }, ['recipeId', 'day']);
|
|
82
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
83
|
-
const mealPlanData = {
|
|
84
|
-
day,
|
|
85
|
-
type: mealType || 'lunch',
|
|
86
|
-
recipe_id: id,
|
|
87
|
-
};
|
|
88
|
-
const result = await this.apiCall('/objects/meal_plan', 'POST', mealPlanData);
|
|
89
|
-
return this.createSuccess(result, 'Recipe added to meal plan successfully');
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
/**
|
|
93
|
-
* Cook recipe - consume ingredients from stock
|
|
94
|
-
*/
|
|
95
|
-
cookRecipe = async (args) => {
|
|
96
|
-
return this.executeToolHandler(async () => {
|
|
97
|
-
const { recipeId, servings } = args || {};
|
|
98
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
99
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
100
|
-
const servingCount = this.parseNumberParam(servings, 'servings', false) || 1;
|
|
101
|
-
// Get recipe details first
|
|
102
|
-
const recipe = await this.apiCall(`/objects/recipes/${id}`);
|
|
103
|
-
// Cook the recipe
|
|
104
|
-
const cookData = {
|
|
105
|
-
recipe_id: id,
|
|
106
|
-
servings: servingCount,
|
|
107
|
-
};
|
|
108
|
-
const result = await this.apiCall('/recipes/cook', 'POST', cookData);
|
|
109
|
-
return this.createSuccess({
|
|
110
|
-
recipe: recipe.name,
|
|
111
|
-
servings: servingCount,
|
|
112
|
-
result,
|
|
113
|
-
}, `Recipe "${recipe.name}" cooked successfully`);
|
|
114
|
-
});
|
|
115
|
-
};
|
|
116
|
-
/**
|
|
117
|
-
* Get recipe nutrition information
|
|
118
|
-
*/
|
|
119
|
-
getRecipeNutrition = async (args) => {
|
|
120
|
-
return this.executeToolHandler(async () => {
|
|
121
|
-
const { recipeId } = args || {};
|
|
122
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
123
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
124
|
-
const nutrition = await this.apiCall(`/recipes/${id}/nutrition`);
|
|
125
|
-
return this.createSuccess(nutrition);
|
|
126
|
-
});
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Search recipes by name or ingredients
|
|
130
|
-
*/
|
|
131
|
-
searchRecipes = async (args) => {
|
|
132
|
-
return this.executeToolHandler(async () => {
|
|
133
|
-
const { query, fields } = args || {};
|
|
134
|
-
this.validateRequired({ query }, ['query']);
|
|
135
|
-
const fieldList = this.parseArrayParam(fields || ['id', 'name'], 'fields');
|
|
136
|
-
// Get all recipes and filter locally
|
|
137
|
-
// Note: This could be optimized with server-side search if Grocy supports it
|
|
138
|
-
const recipes = await this.apiCall('/objects/recipes', 'GET', undefined, {
|
|
139
|
-
queryParams: { 'query[]': 'type=normal' },
|
|
140
|
-
});
|
|
141
|
-
if (!Array.isArray(recipes)) {
|
|
142
|
-
return this.createSuccess([]);
|
|
143
|
-
}
|
|
144
|
-
const searchTerm = query.toLowerCase();
|
|
145
|
-
const filtered = recipes.filter((recipe) => recipe.name?.toLowerCase().includes(searchTerm) ||
|
|
146
|
-
recipe.description?.toLowerCase().includes(searchTerm));
|
|
147
|
-
const result = this.filterFields(filtered, fieldList);
|
|
148
|
-
return this.createSuccess(result);
|
|
149
|
-
});
|
|
150
|
-
};
|
|
151
|
-
// ==================== MEAL PLANNING METHODS ====================
|
|
152
|
-
/**
|
|
153
|
-
* Get meal plan data with context
|
|
154
|
-
*/
|
|
155
|
-
getMealPlan = async (args) => {
|
|
156
|
-
return this.executeToolHandler(async () => {
|
|
157
|
-
const { date, weekly } = args || {};
|
|
158
|
-
// Date parameter is mandatory
|
|
159
|
-
this.validateRequired({ date }, ['date']);
|
|
160
|
-
const targetDate = new Date(date);
|
|
161
|
-
if (isNaN(targetDate.getTime())) {
|
|
162
|
-
throw new ValidationError('Invalid date format. Use YYYY-MM-DD.', 'getMealPlan');
|
|
163
|
-
}
|
|
164
|
-
const datesToQuery = [];
|
|
165
|
-
if (weekly) {
|
|
166
|
-
// For weekly view, get the start of the week (Monday)
|
|
167
|
-
const dayOfWeek = targetDate.getDay();
|
|
168
|
-
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
|
169
|
-
const startDate = new Date(targetDate);
|
|
170
|
-
startDate.setDate(targetDate.getDate() + mondayOffset);
|
|
171
|
-
// Get all 7 days of the week with ±1 day buffer for timezone issues
|
|
172
|
-
for (let i = -1; i <= 7; i++) {
|
|
173
|
-
const day = new Date(startDate);
|
|
174
|
-
day.setDate(startDate.getDate() + i);
|
|
175
|
-
datesToQuery.push(day);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
else {
|
|
179
|
-
// Get the specific date with ±1 day buffer for timezone issues
|
|
180
|
-
const dayBefore = new Date(targetDate);
|
|
181
|
-
dayBefore.setDate(targetDate.getDate() - 1);
|
|
182
|
-
const dayAfter = new Date(targetDate);
|
|
183
|
-
dayAfter.setDate(targetDate.getDate() + 1);
|
|
184
|
-
datesToQuery.push(dayBefore, targetDate, dayAfter);
|
|
185
|
-
}
|
|
186
|
-
const result = await this.getMealPlanForDays(datesToQuery);
|
|
187
|
-
if (result.length === 0) {
|
|
188
|
-
return this.createSuccess({
|
|
189
|
-
message: weekly
|
|
190
|
-
? 'No meals planned for the requested week'
|
|
191
|
-
: 'No meals planned for the requested date',
|
|
192
|
-
meal_plan_by_date: {},
|
|
193
|
-
}, 'Meal plan retrieved successfully');
|
|
194
|
-
}
|
|
195
|
-
// Extract unique recipe IDs
|
|
196
|
-
const recipeIds = [...new Set(result.map((entry) => entry.recipe_id).filter((id) => id))];
|
|
197
|
-
// Fetch recipe details and all sections in parallel
|
|
198
|
-
const [recipeDetails, allSections] = await Promise.all([
|
|
199
|
-
Promise.all(recipeIds.map((recipeId) => this.apiCall(`/objects/recipes/${recipeId}`))),
|
|
200
|
-
this.apiCall('/objects/meal_plan_sections'),
|
|
201
|
-
]);
|
|
202
|
-
// Simplify meal plan entries - don't merge recipe/section details
|
|
203
|
-
const simplifiedMealPlanByDate = {};
|
|
204
|
-
result.forEach((entry) => {
|
|
205
|
-
const entryDate = entry.day;
|
|
206
|
-
if (!simplifiedMealPlanByDate[entryDate]) {
|
|
207
|
-
simplifiedMealPlanByDate[entryDate] = [];
|
|
208
|
-
}
|
|
209
|
-
simplifiedMealPlanByDate[entryDate].push({
|
|
210
|
-
id: entry.id,
|
|
211
|
-
day: entry.day,
|
|
212
|
-
section_id: entry.section_id,
|
|
213
|
-
recipe_id: entry.recipe_id,
|
|
214
|
-
recipe_servings: entry.recipe_servings,
|
|
215
|
-
note: entry.note,
|
|
216
|
-
done: entry.done,
|
|
217
|
-
});
|
|
218
|
-
});
|
|
219
|
-
return this.createSuccess({
|
|
220
|
-
meal_plan_by_date: simplifiedMealPlanByDate,
|
|
221
|
-
recipes: recipeDetails.map((recipe) => ({
|
|
222
|
-
id: recipe.id,
|
|
223
|
-
name: recipe.name,
|
|
224
|
-
product_id: recipe.product_id,
|
|
225
|
-
})),
|
|
226
|
-
sections: Array.isArray(allSections)
|
|
227
|
-
? allSections.map((section) => ({
|
|
228
|
-
id: section.id,
|
|
229
|
-
name: section.name,
|
|
230
|
-
time_info: section.time_info,
|
|
231
|
-
}))
|
|
232
|
-
: [],
|
|
233
|
-
}, 'Meal plan retrieved successfully');
|
|
234
|
-
});
|
|
235
|
-
};
|
|
236
|
-
/**
|
|
237
|
-
* Get meal plan sections
|
|
238
|
-
*/
|
|
239
|
-
getMealPlanSections = async () => {
|
|
240
|
-
return this.executeToolHandler(async () => {
|
|
241
|
-
const result = await this.apiCall('/objects/meal_plan_sections');
|
|
242
|
-
return this.createSuccess(result, 'Meal plan sections retrieved successfully');
|
|
243
|
-
});
|
|
244
|
-
};
|
|
245
|
-
/**
|
|
246
|
-
* Delete recipe from meal plan
|
|
247
|
-
*/
|
|
248
|
-
deleteRecipeFromMealPlan = async (args) => {
|
|
249
|
-
return this.executeToolHandler(async () => {
|
|
250
|
-
const { mealPlanEntryId } = args || {};
|
|
251
|
-
this.validateRequired({ mealPlanEntryId }, ['mealPlanEntryId']);
|
|
252
|
-
const result = await this.apiCall(`/objects/meal_plan/${mealPlanEntryId}`, 'DELETE');
|
|
253
|
-
return this.createSuccess(result, 'Recipe deleted from meal plan successfully');
|
|
254
|
-
});
|
|
255
|
-
};
|
|
256
|
-
// ==================== RECIPE CREATION ====================
|
|
257
|
-
/**
|
|
258
|
-
* Create a new recipe
|
|
259
|
-
*/
|
|
260
|
-
printRecipeLabel = async (args) => {
|
|
261
|
-
return this.executeToolHandler(async () => {
|
|
262
|
-
const { recipeId } = args || {};
|
|
263
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
264
|
-
const result = await this.apiCall(`/recipes/${recipeId}/printlabel`);
|
|
265
|
-
return this.createSuccess(result, 'Recipe label printed successfully');
|
|
266
|
-
});
|
|
267
|
-
};
|
|
268
|
-
createRecipe = async (args) => {
|
|
269
|
-
return this.executeToolHandler(async () => {
|
|
270
|
-
const { name, description, baseServings, instructions } = args || {};
|
|
271
|
-
this.validateRequired({ name }, ['name']);
|
|
272
|
-
const recipeData = {
|
|
273
|
-
name,
|
|
274
|
-
description: description || '',
|
|
275
|
-
base_servings: baseServings || 1,
|
|
276
|
-
type: 'normal',
|
|
277
|
-
instructions: instructions || '',
|
|
278
|
-
};
|
|
279
|
-
const result = await this.apiCall('/objects/recipes', 'POST', recipeData);
|
|
280
|
-
return this.createSuccess(result, `Recipe "${name}" created successfully`);
|
|
281
|
-
});
|
|
282
|
-
};
|
|
283
|
-
// ==================== RECIPE FULFILLMENT ====================
|
|
284
|
-
/**
|
|
285
|
-
* Get fulfillment status for all recipes
|
|
286
|
-
*/
|
|
287
|
-
getAllRecipeFulfillment = async () => {
|
|
288
|
-
return this.executeToolHandler(async () => {
|
|
289
|
-
const fulfillment = await this.apiCall('/recipes/fulfillment');
|
|
290
|
-
return this.createSuccess(fulfillment);
|
|
291
|
-
});
|
|
292
|
-
};
|
|
293
|
-
// ==================== RECIPE CONSUMPTION ====================
|
|
294
|
-
/**
|
|
295
|
-
* Consume/cook a recipe (simple version)
|
|
296
|
-
*/
|
|
297
|
-
consumeRecipe = async (args) => {
|
|
298
|
-
return this.executeToolHandler(async () => {
|
|
299
|
-
const { recipeId, servings } = args || {};
|
|
300
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
301
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
302
|
-
const servingCount = this.parseNumberParam(servings, 'servings', false) || 1;
|
|
303
|
-
const consumeData = {
|
|
304
|
-
recipe_id: id,
|
|
305
|
-
servings: servingCount,
|
|
306
|
-
};
|
|
307
|
-
const result = await this.apiCall('/recipes/consume', 'POST', consumeData);
|
|
308
|
-
return this.createSuccess(result, `Recipe consumed (${servingCount} servings)`);
|
|
309
|
-
});
|
|
310
|
-
};
|
|
311
|
-
// ==================== RECIPE SHOPPING INTEGRATION ====================
|
|
312
|
-
/**
|
|
313
|
-
* Add all products from a recipe to shopping list
|
|
314
|
-
*/
|
|
315
|
-
addAllProductsToShopping = async (args) => {
|
|
316
|
-
return this.executeToolHandler(async () => {
|
|
317
|
-
const { recipeId } = args || {};
|
|
318
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
319
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
320
|
-
const result = await this.apiCall(`/recipes/${id}/add-all-ingredients-to-shopping-list`, 'POST');
|
|
321
|
-
return this.createSuccess(result, 'All recipe products added to shopping list');
|
|
322
|
-
});
|
|
323
|
-
};
|
|
324
|
-
/**
|
|
325
|
-
* Add missing products from a recipe to shopping list
|
|
326
|
-
*/
|
|
327
|
-
addMissingProductsToShopping = async (args) => {
|
|
328
|
-
return this.executeToolHandler(async () => {
|
|
329
|
-
const { recipeId } = args || {};
|
|
330
|
-
this.validateRequired({ recipeId }, ['recipeId']);
|
|
331
|
-
const id = this.parseNumberParam(recipeId, 'recipeId');
|
|
332
|
-
const result = await this.apiCall(`/recipes/${id}/add-not-fulfilled-products-to-shopping-list`, 'POST');
|
|
333
|
-
return this.createSuccess(result, 'Missing recipe products added to shopping list');
|
|
334
|
-
});
|
|
335
|
-
};
|
|
336
|
-
// ==================== COOKING METHODS ====================
|
|
337
|
-
/**
|
|
338
|
-
* Mark recipe from meal plan entry as cooked - modernized version
|
|
339
|
-
*/
|
|
340
|
-
cookedSomething = async (args) => {
|
|
341
|
-
return this.executeToolHandler(async () => {
|
|
342
|
-
const { mealPlanEntryId, recipeId, stockAmounts } = args || {};
|
|
343
|
-
// Get configuration from unified config
|
|
344
|
-
const { config } = await import('../../config/index.js');
|
|
345
|
-
const { toolSubConfigs } = config.parseToolConfiguration();
|
|
346
|
-
const subConfigs = toolSubConfigs?.get(RECIPES_COOKING_COMPLETE_TOOL);
|
|
347
|
-
const allowMealPlanEntryAlreadyDone = subConfigs?.get('allow_meal_plan_entry_already_done') ?? false;
|
|
348
|
-
const printLabels = subConfigs?.get('print_labels') ?? true;
|
|
349
|
-
const allowNoMealPlan = subConfigs?.get('allow_no_meal_plan') ?? false;
|
|
350
|
-
// Validate parameters based on configuration
|
|
351
|
-
if (!allowNoMealPlan && !mealPlanEntryId) {
|
|
352
|
-
throw new ValidationError('mealPlanEntryId is required when allow_no_meal_plan is false.', 'recipes_cooking_complete');
|
|
353
|
-
}
|
|
354
|
-
if (allowNoMealPlan && !recipeId) {
|
|
355
|
-
throw new ValidationError('recipeId is required when allow_no_meal_plan is true.', 'recipes_cooking_complete');
|
|
356
|
-
}
|
|
357
|
-
if (allowNoMealPlan && mealPlanEntryId) {
|
|
358
|
-
throw new ValidationError('mealPlanEntryId should not be provided when allow_no_meal_plan is true. Use recipeId instead.', 'recipes_cooking_complete');
|
|
359
|
-
}
|
|
360
|
-
this.validateRequired({ stockAmounts }, ['stockAmounts']);
|
|
361
|
-
if (!Array.isArray(stockAmounts) || stockAmounts.length === 0) {
|
|
362
|
-
throw new ValidationError('stockAmounts must be a non-empty array of serving amounts.', 'recipes_cooking_complete');
|
|
363
|
-
}
|
|
364
|
-
// Validate all stock amounts are positive numbers
|
|
365
|
-
for (let i = 0; i < stockAmounts.length; i++) {
|
|
366
|
-
const amount = stockAmounts[i];
|
|
367
|
-
if (typeof amount !== 'number' || amount <= 0) {
|
|
368
|
-
throw new ValidationError(`stockAmounts[${i}] must be a positive number, got: ${amount}`, 'recipes_cooking_complete');
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
const completedSteps = [];
|
|
372
|
-
let actualRecipeId;
|
|
373
|
-
let totalServings;
|
|
374
|
-
let mealPlanDate;
|
|
375
|
-
let mealplanShadow;
|
|
376
|
-
if (allowNoMealPlan) {
|
|
377
|
-
// Direct recipe mode - no meal plan entry involved
|
|
378
|
-
actualRecipeId = recipeId;
|
|
379
|
-
totalServings = stockAmounts.reduce((sum, amount) => sum + amount, 0);
|
|
380
|
-
mealPlanDate = new Date().toISOString().split('T')[0];
|
|
381
|
-
mealplanShadow = `${mealPlanDate}#direct-recipe-${actualRecipeId}`;
|
|
382
|
-
completedSteps.push('Using direct recipe mode (no meal plan entry)');
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
// Meal plan mode - traditional workflow
|
|
386
|
-
const mealPlanEntry = await this.apiCall(`/objects/meal_plan/${mealPlanEntryId}`);
|
|
387
|
-
if (!mealPlanEntry) {
|
|
388
|
-
throw new ValidationError(`Meal plan entry ${mealPlanEntryId} not found.`, 'recipes_cooking_complete');
|
|
389
|
-
}
|
|
390
|
-
if (mealPlanEntry.done == 1 && !allowMealPlanEntryAlreadyDone) {
|
|
391
|
-
throw new ValidationError(`Meal plan entry ${mealPlanEntryId} is already marked as done. Cannot mark as cooked again.`, 'recipes_cooking_complete');
|
|
392
|
-
}
|
|
393
|
-
actualRecipeId = mealPlanEntry.recipe_id;
|
|
394
|
-
totalServings = stockAmounts.reduce((sum, amount) => sum + amount, 0);
|
|
395
|
-
mealPlanDate = mealPlanEntry.day || new Date().toISOString().split('T')[0];
|
|
396
|
-
mealplanShadow = `${mealPlanDate}#${mealPlanEntryId}`;
|
|
397
|
-
// Mark the meal plan entry as done and update recipe_servings
|
|
398
|
-
await this.apiCall(`/objects/meal_plan/${mealPlanEntryId}`, 'PUT', {
|
|
399
|
-
done: 1,
|
|
400
|
-
recipe_servings: totalServings,
|
|
401
|
-
});
|
|
402
|
-
completedSteps.push('Meal plan entry marked as done');
|
|
403
|
-
}
|
|
404
|
-
// Consume recipe ingredients
|
|
405
|
-
if (allowNoMealPlan) {
|
|
406
|
-
// Direct consumption using the recipe ID
|
|
407
|
-
await this.apiCall(`/recipes/${actualRecipeId}/consume`, 'POST');
|
|
408
|
-
completedSteps.push('Recipe consumed directly');
|
|
409
|
-
}
|
|
410
|
-
else {
|
|
411
|
-
// Query for the mealplan shadow recipe by name
|
|
412
|
-
const shadowRecipes = await this.apiCall('/objects/recipes', 'GET', undefined, {
|
|
413
|
-
queryParams: { 'query[]': `name=${mealplanShadow}` },
|
|
414
|
-
});
|
|
415
|
-
if (!Array.isArray(shadowRecipes) || shadowRecipes.length === 0) {
|
|
416
|
-
throw new ValidationError(`Mealplan shadow recipe '${mealplanShadow}' not found. Cannot consume ingredients.`, 'recipes_cooking_complete');
|
|
417
|
-
}
|
|
418
|
-
const shadowRecipeId = shadowRecipes[0].id;
|
|
419
|
-
// Consume ingredients using the shadow recipe ID
|
|
420
|
-
await this.apiCall(`/recipes/${shadowRecipeId}/consume`, 'POST');
|
|
421
|
-
completedSteps.push('Recipe consumed via meal plan entry');
|
|
422
|
-
}
|
|
423
|
-
// Handle stock entry splitting and label printing
|
|
424
|
-
const stockEntries = { splitEntries: [], labelsPrinted: 0 };
|
|
425
|
-
const recipe = await this.apiCall(`/objects/recipes/${actualRecipeId}`);
|
|
426
|
-
if (recipe && recipe.product_id) {
|
|
427
|
-
// Get product details and most recent stock entry
|
|
428
|
-
const [product, entries] = await Promise.all([
|
|
429
|
-
this.apiCall(`/objects/products/${recipe.product_id}`),
|
|
430
|
-
this.apiCall(`/stock/products/${recipe.product_id}/entries`, 'GET', undefined, {
|
|
431
|
-
queryParams: { order: 'row_created_timestamp:desc', limit: '1' },
|
|
432
|
-
}),
|
|
433
|
-
]);
|
|
434
|
-
// Get quantity unit info
|
|
435
|
-
let quantityUnit = null;
|
|
436
|
-
if (product.qu_id_stock) {
|
|
437
|
-
try {
|
|
438
|
-
quantityUnit = await this.apiCall(`/objects/quantity_units/${product.qu_id_stock}`);
|
|
439
|
-
}
|
|
440
|
-
catch (error) {
|
|
441
|
-
console.warn('Failed to fetch quantity unit:', error);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
// Helper function to get correct unit form
|
|
445
|
-
const getUnitForm = (amount) => {
|
|
446
|
-
if (!quantityUnit || !quantityUnit.name)
|
|
447
|
-
return '';
|
|
448
|
-
if (amount === 1)
|
|
449
|
-
return quantityUnit.name;
|
|
450
|
-
return quantityUnit.name_plural || quantityUnit.name;
|
|
451
|
-
};
|
|
452
|
-
if (Array.isArray(entries) && entries.length > 0) {
|
|
453
|
-
const originalEntry = entries[0];
|
|
454
|
-
// Use the real stock splitting functionality
|
|
455
|
-
stockEntries.splitEntries = await this.inventoryHandlers.splitStockEntry(originalEntry, stockAmounts, getUnitForm);
|
|
456
|
-
// Print labels for all entries (if enabled)
|
|
457
|
-
if (printLabels) {
|
|
458
|
-
for (const entry of stockEntries.splitEntries) {
|
|
459
|
-
try {
|
|
460
|
-
await this.apiCall(`/stock/entry/${originalEntry.id}/printlabel`);
|
|
461
|
-
stockEntries.labelsPrinted++;
|
|
462
|
-
}
|
|
463
|
-
catch (error) {
|
|
464
|
-
console.error(`Failed to print label for stock entry ${entry.stockId}:`, error);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return this.createSuccess({
|
|
471
|
-
message: `Recipe ${actualRecipeId} cooked (${totalServings} servings consumed, ${stockEntries.splitEntries.length} stock entries created, ${stockEntries.labelsPrinted} labels printed)`,
|
|
472
|
-
stockEntries,
|
|
473
|
-
completedSteps,
|
|
474
|
-
});
|
|
475
|
-
});
|
|
476
|
-
};
|
|
477
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { recipeToolDefinitions } from './definitions.js';
|
|
2
|
-
import { RecipeToolHandlers } from './handlers.js';
|
|
3
|
-
import { validateCompleteSubConfigs } from './validations.js';
|
|
4
|
-
// Use simplified handlers
|
|
5
|
-
const handlers = new RecipeToolHandlers();
|
|
6
|
-
export const recipeModule = {
|
|
7
|
-
definitions: recipeToolDefinitions,
|
|
8
|
-
handlers: {
|
|
9
|
-
// Recipe Management
|
|
10
|
-
recipes_management_get: handlers.getRecipes,
|
|
11
|
-
recipes_management_get_by_id: handlers.getRecipeById,
|
|
12
|
-
recipes_management_create: handlers.createRecipe,
|
|
13
|
-
recipes_management_print_label: handlers.printRecipeLabel,
|
|
14
|
-
// Recipe Fulfillment
|
|
15
|
-
recipes_fulfillment_get: handlers.getRecipeFulfillment,
|
|
16
|
-
recipes_fulfillment_get_all: handlers.getAllRecipeFulfillment,
|
|
17
|
-
// Meal Planning
|
|
18
|
-
recipes_mealplan_get: handlers.getMealPlan,
|
|
19
|
-
recipes_mealplan_get_sections: handlers.getMealPlanSections,
|
|
20
|
-
recipes_mealplan_add_recipe: handlers.addRecipeToMealPlan,
|
|
21
|
-
recipes_mealplan_delete_entry: handlers.deleteRecipeFromMealPlan,
|
|
22
|
-
// Recipe Cooking
|
|
23
|
-
recipes_cooking_consume: handlers.consumeRecipe,
|
|
24
|
-
recipes_cooking_complete: handlers.cookedSomething,
|
|
25
|
-
// Shopping Integration
|
|
26
|
-
recipes_shopping_add_all_products: handlers.addAllProductsToShopping,
|
|
27
|
-
recipes_shopping_add_missing_products: handlers.addMissingProductsToShopping,
|
|
28
|
-
},
|
|
29
|
-
validators: {
|
|
30
|
-
recipes_cooking_complete: validateCompleteSubConfigs,
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
export * from './definitions.js';
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Recipe tools sub-configuration validation functions
|
|
3
|
-
*/
|
|
4
|
-
import { ValidationHelpers } from '../validation-helpers.js';
|
|
5
|
-
import { ValidationError } from '../../utils/errors.js';
|
|
6
|
-
/**
|
|
7
|
-
* Validation function for complete tool sub-configurations
|
|
8
|
-
*/
|
|
9
|
-
export const validateCompleteSubConfigs = (subConfigs) => {
|
|
10
|
-
const allowMealPlanEntryAlreadyDone = subConfigs.get('allow_meal_plan_entry_already_done');
|
|
11
|
-
const allowNoMealPlan = subConfigs.get('allow_no_meal_plan');
|
|
12
|
-
const printLabels = subConfigs.get('print_labels');
|
|
13
|
-
// Validate types
|
|
14
|
-
ValidationHelpers.validateBoolean(allowMealPlanEntryAlreadyDone, 'allow_meal_plan_entry_already_done');
|
|
15
|
-
ValidationHelpers.validateBoolean(allowNoMealPlan, 'allow_no_meal_plan');
|
|
16
|
-
ValidationHelpers.validateBoolean(printLabels, 'print_labels');
|
|
17
|
-
// Business logic validation
|
|
18
|
-
if (allowNoMealPlan && allowMealPlanEntryAlreadyDone) {
|
|
19
|
-
throw new ValidationError('allow_no_meal_plan and allow_meal_plan_entry_already_done cannot both be true - they are mutually exclusive modes', 'complete sub-config');
|
|
20
|
-
}
|
|
21
|
-
// Check for unknown options
|
|
22
|
-
const knownOptions = new Set([
|
|
23
|
-
'allow_meal_plan_entry_already_done',
|
|
24
|
-
'allow_no_meal_plan',
|
|
25
|
-
'print_labels',
|
|
26
|
-
'ack_token',
|
|
27
|
-
]);
|
|
28
|
-
ValidationHelpers.validateKnownOptions(subConfigs, knownOptions, 'complete');
|
|
29
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
export const shoppingToolDefinitions = [
|
|
2
|
-
{
|
|
3
|
-
name: 'shopping_list_get',
|
|
4
|
-
description: '[SHOPPING/LIST] Get your current shopping list items.',
|
|
5
|
-
annotations: { readOnlyHint: true },
|
|
6
|
-
inputSchema: {
|
|
7
|
-
type: 'object',
|
|
8
|
-
properties: {},
|
|
9
|
-
required: [],
|
|
10
|
-
},
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
name: 'shopping_list_add_item',
|
|
14
|
-
description: '[SHOPPING/LIST] Add an item to your shopping list. Use inventory_products_get first to find the product ID you want to add.',
|
|
15
|
-
inputSchema: {
|
|
16
|
-
type: 'object',
|
|
17
|
-
properties: {
|
|
18
|
-
productId: {
|
|
19
|
-
type: 'number',
|
|
20
|
-
description: 'ID of the product to add. Use inventory_products_get tool to find the correct product ID by searching for the product name in the results.',
|
|
21
|
-
},
|
|
22
|
-
amount: {
|
|
23
|
-
type: 'number',
|
|
24
|
-
description: "Amount to add to shopping list in the product's stock unit (e.g., 2 pieces, 1.5 kg, 750 ml). Ensure you know the product's unit before specifying amount. Default: 1",
|
|
25
|
-
default: 1,
|
|
26
|
-
},
|
|
27
|
-
shoppingListId: {
|
|
28
|
-
type: 'number',
|
|
29
|
-
description: 'ID of the shopping list to add to (default: 1). Most users have only one shopping list with ID 1.',
|
|
30
|
-
default: 1,
|
|
31
|
-
},
|
|
32
|
-
note: {
|
|
33
|
-
type: 'string',
|
|
34
|
-
description: 'Optional note for the shopping list item',
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
required: ['productId'],
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
name: 'shopping_list_remove_item',
|
|
42
|
-
description: '[SHOPPING/LIST] Remove an item from your shopping list. Use shopping_list_get first to find the shopping list item ID.',
|
|
43
|
-
inputSchema: {
|
|
44
|
-
type: 'object',
|
|
45
|
-
properties: {
|
|
46
|
-
shoppingListItemId: {
|
|
47
|
-
type: 'number',
|
|
48
|
-
description: 'ID of the shopping list item to remove. Use shopping_list_get tool to find the correct shopping list item ID by looking at the "id" field in the results.',
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
required: ['shoppingListItemId'],
|
|
52
|
-
},
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
name: 'shopping_locations_get',
|
|
56
|
-
description: '[SHOPPING/LOCATIONS] Get **retail store / shop** locations where you buy groceries (Grocy shopping locations). NOT pantry or home storage—use system_locations_get for storage location IDs (locationId). Use shopping_locations_get for storeId when adding shopping-list items or store-specific workflows.',
|
|
57
|
-
annotations: { readOnlyHint: true },
|
|
58
|
-
inputSchema: {
|
|
59
|
-
type: 'object',
|
|
60
|
-
properties: {},
|
|
61
|
-
required: [],
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: 'shopping_list_print_thermal',
|
|
66
|
-
description: '[SHOPPING/PRINTING] Print the shopping list with a thermal printer. This creates a physical shopping list for store visits.',
|
|
67
|
-
inputSchema: {
|
|
68
|
-
type: 'object',
|
|
69
|
-
properties: {},
|
|
70
|
-
required: [],
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
];
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { BaseToolHandler } from '../base.js';
|
|
2
|
-
export class ShoppingToolHandlers extends BaseToolHandler {
|
|
3
|
-
getShoppingList = async () => {
|
|
4
|
-
return this.executeToolHandler(async () => {
|
|
5
|
-
const result = await this.apiCall('/objects/shopping_list');
|
|
6
|
-
return this.createSuccess(result, 'Shopping list retrieved successfully');
|
|
7
|
-
});
|
|
8
|
-
};
|
|
9
|
-
addShoppingListItem = async (args) => {
|
|
10
|
-
return this.executeToolHandler(async () => {
|
|
11
|
-
const { productId, amount = 1, shoppingListId = 1, note = '' } = args || {};
|
|
12
|
-
this.validateRequired({ productId }, ['productId']);
|
|
13
|
-
const body = {
|
|
14
|
-
product_id: productId,
|
|
15
|
-
amount,
|
|
16
|
-
shopping_list_id: shoppingListId,
|
|
17
|
-
note,
|
|
18
|
-
};
|
|
19
|
-
const result = await this.apiCall('/objects/shopping_list', 'POST', body);
|
|
20
|
-
return this.createSuccess(result, 'Shopping list item added successfully');
|
|
21
|
-
});
|
|
22
|
-
};
|
|
23
|
-
removeShoppingListItem = async (args) => {
|
|
24
|
-
return this.executeToolHandler(async () => {
|
|
25
|
-
const { shoppingListItemId } = args || {};
|
|
26
|
-
this.validateRequired({ shoppingListItemId }, ['shoppingListItemId']);
|
|
27
|
-
const result = await this.apiCall(`/objects/shopping_list/${shoppingListItemId}`, 'DELETE');
|
|
28
|
-
return this.createSuccess(result, 'Shopping list item removed successfully');
|
|
29
|
-
});
|
|
30
|
-
};
|
|
31
|
-
getShoppingLocations = async () => {
|
|
32
|
-
return this.executeToolHandler(async () => {
|
|
33
|
-
const result = await this.apiCall('/objects/shopping_locations');
|
|
34
|
-
return this.createSuccess(result, 'Shopping locations retrieved successfully');
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
printShoppingListThermal = async () => {
|
|
38
|
-
return this.executeToolHandler(async () => {
|
|
39
|
-
const result = await this.apiCall('/print/shoppinglist/thermal');
|
|
40
|
-
return this.createSuccess(result, 'Shopping list sent to thermal printer successfully');
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { shoppingToolDefinitions } from './definitions.js';
|
|
2
|
-
import { ShoppingToolHandlers } from './handlers.js';
|
|
3
|
-
const handlers = new ShoppingToolHandlers();
|
|
4
|
-
export const shoppingModule = {
|
|
5
|
-
definitions: shoppingToolDefinitions,
|
|
6
|
-
handlers: {
|
|
7
|
-
shopping_list_get: handlers.getShoppingList,
|
|
8
|
-
shopping_list_add_item: handlers.addShoppingListItem,
|
|
9
|
-
shopping_list_remove_item: handlers.removeShoppingListItem,
|
|
10
|
-
shopping_locations_get: handlers.getShoppingLocations,
|
|
11
|
-
shopping_list_print_thermal: handlers.printShoppingListThermal,
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
export * from './definitions.js';
|