mcp-grocy 2.2.0 → 2.5.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +112 -32
  3. package/build/api/client.js +11 -13
  4. package/build/config/index.js +74 -20
  5. package/build/resources/CHANGELOG.md +31 -0
  6. package/build/resources/DOCS.md +8 -3
  7. package/build/resources/README.md +112 -32
  8. package/build/resources/api-reference.md +88 -88
  9. package/build/resources/config.md +30 -17
  10. package/build/resources/examples.md +120 -221
  11. package/build/resources/installation.md +9 -14
  12. package/build/resources/response-format.md +18 -13
  13. package/build/server/http-server.js +154 -42
  14. package/build/server/mcp-server.js +143 -96
  15. package/build/server/resources.js +37 -24
  16. package/build/server/tool-input-zod.js +86 -0
  17. package/build/tools/base.js +19 -15
  18. package/build/tools/household/definitions.js +47 -43
  19. package/build/tools/household/handlers.js +2 -2
  20. package/build/tools/household/index.js +2 -2
  21. package/build/tools/inventory/definitions.js +150 -113
  22. package/build/tools/inventory/handlers.js +55 -42
  23. package/build/tools/inventory/index.js +2 -2
  24. package/build/tools/module-loader.js +15 -12
  25. package/build/tools/recipes/definitions.js +103 -86
  26. package/build/tools/recipes/handlers.js +44 -38
  27. package/build/tools/recipes/index.js +3 -3
  28. package/build/tools/recipes/validations.js +8 -2
  29. package/build/tools/shopping/definitions.js +22 -20
  30. package/build/tools/shopping/handlers.js +1 -1
  31. package/build/tools/shopping/index.js +2 -2
  32. package/build/tools/system/definitions.js +25 -22
  33. package/build/tools/system/handlers.js +74 -12
  34. package/build/tools/system/index.js +2 -2
  35. package/build/tools/validation-helpers.js +10 -7
  36. package/build/types/index.js +12 -10
  37. package/build/utils/errors.js +10 -5
  38. package/build/utils/logger.js +16 -15
  39. package/build/version.js +2 -2
  40. package/package.json +38 -23
@@ -1,9 +1,11 @@
1
1
  import { config } from '../../config/index.js';
2
+ const RECIPES_COOKING_COMPLETE_TOOL = 'recipes_cooking_complete';
2
3
  export const recipeToolDefinitions = [
3
4
  // ==================== RECIPE MANAGEMENT ====================
4
5
  {
5
6
  name: 'recipes_management_get',
6
- description: '[RECIPES/MANAGEMENT] Get specific fields for all recipes from your Grocy instance. You must specify which fields to retrieve.',
7
+ description: '[RECIPES/MANAGEMENT] **List/search many recipes**—you choose which fields (e.g. id+name). For one full recipe by ID use recipes_management_get_by_id.',
8
+ annotations: { readOnlyHint: true },
7
9
  inputSchema: {
8
10
  type: 'object',
9
11
  properties: {
@@ -11,27 +13,39 @@ export const recipeToolDefinitions = [
11
13
  type: 'array',
12
14
  items: {
13
15
  type: 'string',
14
- enum: ['id', 'name', 'description', 'base_servings', 'desired_servings', 'not_check_shoppinglist', 'type', 'picture_file_name', 'ingredients', 'instructions']
16
+ enum: [
17
+ 'id',
18
+ 'name',
19
+ 'description',
20
+ 'base_servings',
21
+ 'desired_servings',
22
+ 'not_check_shoppinglist',
23
+ 'type',
24
+ 'picture_file_name',
25
+ 'ingredients',
26
+ 'instructions',
27
+ ],
15
28
  },
16
- description: 'Array of field names to retrieve. For basic lookup use ["id", "name"]. For recipe planning use ["id", "name", "description", "base_servings"]. Available fields: id, name, description, base_servings, desired_servings, not_check_shoppinglist, type, picture_file_name, ingredients, instructions'
17
- }
29
+ description: 'Array of field names to retrieve. For basic lookup use ["id", "name"]. For recipe planning use ["id", "name", "description", "base_servings"]. Available fields: id, name, description, base_servings, desired_servings, not_check_shoppinglist, type, picture_file_name, ingredients, instructions',
30
+ },
18
31
  },
19
- required: ['fields']
20
- }
32
+ required: ['fields'],
33
+ },
21
34
  },
22
35
  {
23
36
  name: 'recipes_management_get_by_id',
24
- description: '[RECIPES/MANAGEMENT] Get a specific recipe by its ID from your Grocy instance.',
37
+ description: '[RECIPES/MANAGEMENT] **Single recipe** by recipeId (full record). To scan or filter many recipes use recipes_management_get with a fields list.',
38
+ annotations: { readOnlyHint: true },
25
39
  inputSchema: {
26
40
  type: 'object',
27
41
  properties: {
28
42
  recipeId: {
29
43
  type: 'number',
30
- description: 'ID of the recipe to retrieve. Use recipes_management_get tool to find the correct recipe ID by name.'
31
- }
44
+ description: 'ID of the recipe to retrieve. Use recipes_management_get tool to find the correct recipe ID by name.',
45
+ },
32
46
  },
33
- required: ['recipeId']
34
- }
47
+ required: ['recipeId'],
48
+ },
35
49
  },
36
50
  {
37
51
  name: 'recipes_management_create',
@@ -41,24 +55,24 @@ export const recipeToolDefinitions = [
41
55
  properties: {
42
56
  name: {
43
57
  type: 'string',
44
- description: 'Name of the recipe'
58
+ description: 'Name of the recipe',
45
59
  },
46
60
  description: {
47
61
  type: 'string',
48
- description: 'Description of the recipe (optional)'
62
+ description: 'Description of the recipe (optional)',
49
63
  },
50
64
  baseServings: {
51
65
  type: 'number',
52
66
  description: 'Base servings for the recipe (e.g., 4 for a family recipe)',
53
- default: 1
67
+ default: 1,
54
68
  },
55
69
  instructions: {
56
70
  type: 'string',
57
- description: 'Recipe instructions (optional)'
58
- }
71
+ description: 'Recipe instructions (optional)',
72
+ },
59
73
  },
60
- required: ['name']
61
- }
74
+ required: ['name'],
75
+ },
62
76
  },
63
77
  {
64
78
  name: 'recipes_management_print_label',
@@ -68,93 +82,97 @@ export const recipeToolDefinitions = [
68
82
  properties: {
69
83
  recipeId: {
70
84
  type: 'number',
71
- description: 'ID of the recipe to print label for. Use recipes_management_get tool to find the correct recipe ID.'
72
- }
85
+ description: 'ID of the recipe to print label for. Use recipes_management_get tool to find the correct recipe ID.',
86
+ },
73
87
  },
74
- required: ['recipeId']
75
- }
88
+ required: ['recipeId'],
89
+ },
76
90
  },
77
91
  {
78
92
  name: 'recipes_fulfillment_get',
79
- description: '[RECIPES/FULFILLMENT] Check fulfillment status for a specific recipe (what ingredients are available vs needed).',
93
+ description: '[RECIPES/FULFILLMENT] **Single recipe**—can I make this dish? Ingredient coverage vs stock for one recipeId. For an overview of many recipes at once use recipes_fulfillment_get_all.',
94
+ annotations: { readOnlyHint: true },
80
95
  inputSchema: {
81
96
  type: 'object',
82
97
  properties: {
83
98
  recipeId: {
84
99
  type: 'number',
85
- description: 'ID of the recipe to check fulfillment for. Use recipes_management_get tool to find the correct recipe ID by name.'
100
+ description: 'ID of the recipe to check fulfillment for. Use recipes_management_get tool to find the correct recipe ID by name.',
86
101
  },
87
102
  onlyMissing: {
88
103
  type: 'boolean',
89
104
  description: 'If true, only return missing/insufficient ingredients',
90
- default: false
91
- }
105
+ default: false,
106
+ },
92
107
  },
93
- required: ['recipeId']
94
- }
108
+ required: ['recipeId'],
109
+ },
95
110
  },
96
111
  {
97
112
  name: 'recipes_fulfillment_get_all',
98
- description: '[RECIPES/FULFILLMENT] Get fulfillment status for all recipes (overview of which recipes can be made with current stock).',
113
+ description: '[RECIPES/FULFILLMENT] **All recipes**—batch view of which recipes are makeable with current stock (no recipeId). For one named recipe use recipes_fulfillment_get.',
114
+ annotations: { readOnlyHint: true },
99
115
  inputSchema: {
100
116
  type: 'object',
101
117
  properties: {},
102
- required: []
103
- }
118
+ required: [],
119
+ },
104
120
  },
105
121
  // ==================== MEAL PLANNING ====================
106
122
  {
107
123
  name: 'recipes_mealplan_get',
108
124
  description: '[RECIPES/MEALPLAN] 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 recipes_mealplan_delete_entry.',
125
+ annotations: { readOnlyHint: true },
109
126
  inputSchema: {
110
127
  type: 'object',
111
128
  properties: {
112
129
  date: {
113
130
  type: 'string',
114
- 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.'
131
+ 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.',
115
132
  },
116
133
  weekly: {
117
134
  type: 'boolean',
118
- description: 'If true, returns the entire calendar week containing the specified date.'
119
- }
135
+ description: 'If true, returns the entire calendar week containing the specified date.',
136
+ },
120
137
  },
121
- required: ['date']
122
- }
138
+ required: ['date'],
139
+ },
123
140
  },
124
141
  {
125
142
  name: 'recipes_mealplan_get_sections',
126
- description: '[RECIPES/MEALPLAN] Get all available meal plan sections from your Grocy instance (e.g., Breakfast, Lunch, Dinner, Snacks). Use this to find valid section IDs for recipes_mealplan_add_recipe.',
143
+ description: '[RECIPES/MEALPLAN] **Read-only:** list meal plan section names/IDs (Breakfast, Dinner, …). Does not return planned meals or dates—use recipes_mealplan_get for the calendar. Needed before recipes_mealplan_add_recipe to pick sectionId.',
144
+ annotations: { readOnlyHint: true },
127
145
  inputSchema: {
128
146
  type: 'object',
129
147
  properties: {},
130
- required: []
131
- }
148
+ required: [],
149
+ },
132
150
  },
133
151
  {
134
152
  name: 'recipes_mealplan_add_recipe',
135
- description: '[RECIPES/MEALPLAN] Add a recipe to the meal plan for a specific date and meal section. Use recipes_management_get to find recipe IDs and recipes_mealplan_get_sections to find valid section IDs and their names.',
153
+ description: '[RECIPES/MEALPLAN] **Write:** put a recipe on the calendar (date + section + servings). Not for listing sections (recipes_mealplan_get_sections) or viewing the plan (recipes_mealplan_get).',
136
154
  inputSchema: {
137
155
  type: 'object',
138
156
  properties: {
139
157
  recipeId: {
140
158
  type: 'number',
141
- description: 'ID of the recipe to add to the meal plan. Use recipes_management_get tool to find valid recipe IDs and their names.'
159
+ description: 'ID of the recipe to add to the meal plan. Use recipes_management_get tool to find valid recipe IDs and their names.',
142
160
  },
143
161
  day: {
144
162
  type: 'string',
145
- description: 'Day to add the recipe to in YYYY-MM-DD format (e.g., "2024-12-25").'
163
+ description: 'Day to add the recipe to in YYYY-MM-DD format (e.g., "2024-12-25").',
146
164
  },
147
165
  servings: {
148
166
  type: 'number',
149
- description: 'Number of servings for this meal plan entry (e.g., 2 for a family of two, 4 for a family of four).'
167
+ description: 'Number of servings for this meal plan entry (e.g., 2 for a family of two, 4 for a family of four).',
150
168
  },
151
169
  sectionId: {
152
170
  type: 'number',
153
- description: 'ID of the meal plan section that defines when this meal will be consumed (e.g., breakfast, lunch, dinner, snacks). Use recipes_mealplan_get_sections tool to discover what sections are available in your Grocy instance and get their specific IDs and names.'
154
- }
171
+ description: 'ID of the meal plan section that defines when this meal will be consumed (e.g., breakfast, lunch, dinner, snacks). Use recipes_mealplan_get_sections tool to discover what sections are available in your Grocy instance and get their specific IDs and names.',
172
+ },
155
173
  },
156
- required: ['recipeId', 'day', 'servings', 'sectionId']
157
- }
174
+ required: ['recipeId', 'day', 'servings', 'sectionId'],
175
+ },
158
176
  },
159
177
  {
160
178
  name: 'recipes_mealplan_delete_entry',
@@ -164,11 +182,11 @@ export const recipeToolDefinitions = [
164
182
  properties: {
165
183
  mealPlanEntryId: {
166
184
  type: 'number',
167
- description: 'ID of the specific meal plan entry to delete. Use recipes_mealplan_get to find the correct entry ID.'
168
- }
185
+ description: 'ID of the specific meal plan entry to delete. Use recipes_mealplan_get to find the correct entry ID.',
186
+ },
169
187
  },
170
- required: ['mealPlanEntryId']
171
- }
188
+ required: ['mealPlanEntryId'],
189
+ },
172
190
  },
173
191
  // ==================== COOKING ====================
174
192
  {
@@ -179,50 +197,50 @@ export const recipeToolDefinitions = [
179
197
  properties: {
180
198
  recipeId: {
181
199
  type: 'number',
182
- description: 'ID of the recipe to consume. Use recipes_management_get tool to find the correct recipe ID by name.'
200
+ description: 'ID of the recipe to consume. Use recipes_management_get tool to find the correct recipe ID by name.',
183
201
  },
184
202
  servings: {
185
203
  type: 'number',
186
204
  description: 'Number of servings to cook (scales ingredient consumption accordingly)',
187
- default: 1
188
- }
205
+ default: 1,
206
+ },
189
207
  },
190
- required: ['recipeId']
191
- }
208
+ required: ['recipeId'],
209
+ },
192
210
  },
193
211
  // ==================== SHOPPING INTEGRATION ====================
194
212
  {
195
213
  name: 'recipes_shopping_add_all_products',
196
- description: '[RECIPES/SHOPPING] Add all products from a recipe to the shopping list.',
214
+ description: '[RECIPES/SHOPPING] Add **every** recipe ingredient to the shopping list (ignores what you already have). For only what you are short on use recipes_shopping_add_missing_products.',
197
215
  inputSchema: {
198
216
  type: 'object',
199
217
  properties: {
200
218
  recipeId: {
201
219
  type: 'number',
202
- description: 'ID of the recipe whose products should be added to shopping list. Use recipes_management_get tool to find the correct recipe ID by name.'
203
- }
220
+ description: 'ID of the recipe whose products should be added to shopping list. Use recipes_management_get tool to find the correct recipe ID by name.',
221
+ },
204
222
  },
205
- required: ['recipeId']
206
- }
223
+ required: ['recipeId'],
224
+ },
207
225
  },
208
226
  {
209
227
  name: 'recipes_shopping_add_missing_products',
210
- description: '[RECIPES/SHOPPING] Add only the missing/insufficient products from a recipe to the shopping list (based on current stock levels).',
228
+ description: '[RECIPES/SHOPPING] Add **only missing or insufficient** ingredients (compares to stock). To add the full ingredient set regardless of stock use recipes_shopping_add_all_products.',
211
229
  inputSchema: {
212
230
  type: 'object',
213
231
  properties: {
214
232
  recipeId: {
215
233
  type: 'number',
216
- description: 'ID of the recipe to check for missing products. Use recipes_management_get tool to find the correct recipe ID by name.'
217
- }
234
+ description: 'ID of the recipe to check for missing products. Use recipes_management_get tool to find the correct recipe ID by name.',
235
+ },
218
236
  },
219
- required: ['recipeId']
220
- }
237
+ required: ['recipeId'],
238
+ },
221
239
  },
222
240
  // ==================== ADVANCED COOKING ====================
223
241
  (() => {
224
242
  const { toolSubConfigs } = config.parseToolConfiguration();
225
- const subConfigs = toolSubConfigs?.get('complete');
243
+ const subConfigs = toolSubConfigs?.get(RECIPES_COOKING_COMPLETE_TOOL);
226
244
  const allowNoMealPlan = subConfigs?.get('allow_no_meal_plan') ?? false;
227
245
  const allowAlreadyDone = subConfigs?.get('allow_meal_plan_entry_already_done') ?? false;
228
246
  return {
@@ -231,31 +249,30 @@ export const recipeToolDefinitions = [
231
249
  inputSchema: {
232
250
  type: 'object',
233
251
  properties: {
234
- ...(allowNoMealPlan ? {
235
- recipeId: {
236
- type: 'number',
237
- description: 'ID of the recipe to cook directly.'
252
+ ...(allowNoMealPlan
253
+ ? {
254
+ recipeId: {
255
+ type: 'number',
256
+ description: 'ID of the recipe to cook directly.',
257
+ },
238
258
  }
239
- } : {
240
- mealPlanEntryId: {
241
- type: 'number',
242
- description: `ID of the meal plan entry.${allowAlreadyDone ? '' : ' Note: This will fail if the meal plan entry is already marked as done (done=1).'}`
243
- }
244
- }),
259
+ : {
260
+ mealPlanEntryId: {
261
+ type: 'number',
262
+ description: `ID of the meal plan entry.${allowAlreadyDone ? '' : ' Note: This will fail if the meal plan entry is already marked as done (done=1).'}`,
263
+ },
264
+ }),
245
265
  stockAmounts: {
246
266
  type: 'array',
247
267
  items: {
248
268
  type: 'number',
249
- minimum: 0.1
269
+ minimum: 0.1,
250
270
  },
251
- description: 'Array of serving amounts for each stock entry to create (e.g., [1, 2, 2] for 1 single serving + 2 double servings). Total will be used for ingredient consumption.'
252
- }
271
+ description: 'Array of serving amounts for each stock entry to create (e.g., [1, 2, 2] for 1 single serving + 2 double servings). Total will be used for ingredient consumption.',
272
+ },
253
273
  },
254
- required: [
255
- ...(allowNoMealPlan ? ['recipeId'] : ['mealPlanEntryId']),
256
- 'stockAmounts'
257
- ]
258
- }
274
+ required: [...(allowNoMealPlan ? ['recipeId'] : ['mealPlanEntryId']), 'stockAmounts'],
275
+ },
259
276
  };
260
- })()
277
+ })(),
261
278
  ];
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import { BaseToolHandler } from '../base.js';
6
6
  import { InventoryToolHandlers } from '../inventory/handlers.js';
7
+ import { ValidationError } from '../../utils/errors.js';
8
+ const RECIPES_COOKING_COMPLETE_TOOL = 'recipes_cooking_complete';
7
9
  export class RecipeToolHandlers extends BaseToolHandler {
8
10
  inventoryHandlers = new InventoryToolHandlers();
9
11
  /**
@@ -16,8 +18,8 @@ export class RecipeToolHandlers extends BaseToolHandler {
16
18
  const dayResult = await this.apiCall(`/objects/meal_plan`, 'GET', undefined, {
17
19
  queryParams: {
18
20
  'query[]': `day=${dayString}`,
19
- order: 'day'
20
- }
21
+ order: 'day',
22
+ },
21
23
  });
22
24
  if (Array.isArray(dayResult)) {
23
25
  allResults.push(...dayResult);
@@ -36,7 +38,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
36
38
  const fieldList = this.parseArrayParam(fields, 'fields');
37
39
  // Fetch recipes
38
40
  const recipes = await this.apiCall('/objects/recipes', 'GET', undefined, {
39
- queryParams: { 'query[]': 'type=normal' }
41
+ queryParams: { 'query[]': 'type=normal' },
40
42
  });
41
43
  if (!Array.isArray(recipes)) {
42
44
  return this.createSuccess([]);
@@ -81,7 +83,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
81
83
  const mealPlanData = {
82
84
  day,
83
85
  type: mealType || 'lunch',
84
- recipe_id: id
86
+ recipe_id: id,
85
87
  };
86
88
  const result = await this.apiCall('/objects/meal_plan', 'POST', mealPlanData);
87
89
  return this.createSuccess(result, 'Recipe added to meal plan successfully');
@@ -101,13 +103,13 @@ export class RecipeToolHandlers extends BaseToolHandler {
101
103
  // Cook the recipe
102
104
  const cookData = {
103
105
  recipe_id: id,
104
- servings: servingCount
106
+ servings: servingCount,
105
107
  };
106
108
  const result = await this.apiCall('/recipes/cook', 'POST', cookData);
107
109
  return this.createSuccess({
108
110
  recipe: recipe.name,
109
111
  servings: servingCount,
110
- result
112
+ result,
111
113
  }, `Recipe "${recipe.name}" cooked successfully`);
112
114
  });
113
115
  };
@@ -134,7 +136,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
134
136
  // Get all recipes and filter locally
135
137
  // Note: This could be optimized with server-side search if Grocy supports it
136
138
  const recipes = await this.apiCall('/objects/recipes', 'GET', undefined, {
137
- queryParams: { 'query[]': 'type=normal' }
139
+ queryParams: { 'query[]': 'type=normal' },
138
140
  });
139
141
  if (!Array.isArray(recipes)) {
140
142
  return this.createSuccess([]);
@@ -157,7 +159,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
157
159
  this.validateRequired({ date }, ['date']);
158
160
  const targetDate = new Date(date);
159
161
  if (isNaN(targetDate.getTime())) {
160
- throw new Error('Invalid date format. Use YYYY-MM-DD.');
162
+ throw new ValidationError('Invalid date format. Use YYYY-MM-DD.', 'getMealPlan');
161
163
  }
162
164
  const datesToQuery = [];
163
165
  if (weekly) {
@@ -184,20 +186,22 @@ export class RecipeToolHandlers extends BaseToolHandler {
184
186
  const result = await this.getMealPlanForDays(datesToQuery);
185
187
  if (result.length === 0) {
186
188
  return this.createSuccess({
187
- message: weekly ? 'No meals planned for the requested week' : 'No meals planned for the requested date',
188
- meal_plan_by_date: {}
189
+ message: weekly
190
+ ? 'No meals planned for the requested week'
191
+ : 'No meals planned for the requested date',
192
+ meal_plan_by_date: {},
189
193
  }, 'Meal plan retrieved successfully');
190
194
  }
191
- // Extract unique recipe IDs
192
- const recipeIds = [...new Set(result.map(entry => entry.recipe_id).filter(id => id))];
195
+ // Extract unique recipe IDs
196
+ const recipeIds = [...new Set(result.map((entry) => entry.recipe_id).filter((id) => id))];
193
197
  // Fetch recipe details and all sections in parallel
194
198
  const [recipeDetails, allSections] = await Promise.all([
195
- Promise.all(recipeIds.map(recipeId => this.apiCall(`/objects/recipes/${recipeId}`))),
196
- this.apiCall('/objects/meal_plan_sections')
199
+ Promise.all(recipeIds.map((recipeId) => this.apiCall(`/objects/recipes/${recipeId}`))),
200
+ this.apiCall('/objects/meal_plan_sections'),
197
201
  ]);
198
202
  // Simplify meal plan entries - don't merge recipe/section details
199
203
  const simplifiedMealPlanByDate = {};
200
- result.forEach(entry => {
204
+ result.forEach((entry) => {
201
205
  const entryDate = entry.day;
202
206
  if (!simplifiedMealPlanByDate[entryDate]) {
203
207
  simplifiedMealPlanByDate[entryDate] = [];
@@ -209,7 +213,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
209
213
  recipe_id: entry.recipe_id,
210
214
  recipe_servings: entry.recipe_servings,
211
215
  note: entry.note,
212
- done: entry.done
216
+ done: entry.done,
213
217
  });
214
218
  });
215
219
  return this.createSuccess({
@@ -219,11 +223,13 @@ export class RecipeToolHandlers extends BaseToolHandler {
219
223
  name: recipe.name,
220
224
  product_id: recipe.product_id,
221
225
  })),
222
- sections: Array.isArray(allSections) ? allSections.map((section) => ({
223
- id: section.id,
224
- name: section.name,
225
- time_info: section.time_info
226
- })) : []
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
+ : [],
227
233
  }, 'Meal plan retrieved successfully');
228
234
  });
229
235
  };
@@ -268,7 +274,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
268
274
  description: description || '',
269
275
  base_servings: baseServings || 1,
270
276
  type: 'normal',
271
- instructions: instructions || ''
277
+ instructions: instructions || '',
272
278
  };
273
279
  const result = await this.apiCall('/objects/recipes', 'POST', recipeData);
274
280
  return this.createSuccess(result, `Recipe "${name}" created successfully`);
@@ -296,7 +302,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
296
302
  const servingCount = this.parseNumberParam(servings, 'servings', false) || 1;
297
303
  const consumeData = {
298
304
  recipe_id: id,
299
- servings: servingCount
305
+ servings: servingCount,
300
306
  };
301
307
  const result = await this.apiCall('/recipes/consume', 'POST', consumeData);
302
308
  return this.createSuccess(result, `Recipe consumed (${servingCount} servings)`);
@@ -337,29 +343,29 @@ export class RecipeToolHandlers extends BaseToolHandler {
337
343
  // Get configuration from unified config
338
344
  const { config } = await import('../../config/index.js');
339
345
  const { toolSubConfigs } = config.parseToolConfiguration();
340
- const subConfigs = toolSubConfigs?.get('complete');
346
+ const subConfigs = toolSubConfigs?.get(RECIPES_COOKING_COMPLETE_TOOL);
341
347
  const allowMealPlanEntryAlreadyDone = subConfigs?.get('allow_meal_plan_entry_already_done') ?? false;
342
348
  const printLabels = subConfigs?.get('print_labels') ?? true;
343
349
  const allowNoMealPlan = subConfigs?.get('allow_no_meal_plan') ?? false;
344
350
  // Validate parameters based on configuration
345
351
  if (!allowNoMealPlan && !mealPlanEntryId) {
346
- throw new Error('mealPlanEntryId is required when allow_no_meal_plan is false.');
352
+ throw new ValidationError('mealPlanEntryId is required when allow_no_meal_plan is false.', 'recipes_cooking_complete');
347
353
  }
348
354
  if (allowNoMealPlan && !recipeId) {
349
- throw new Error('recipeId is required when allow_no_meal_plan is true.');
355
+ throw new ValidationError('recipeId is required when allow_no_meal_plan is true.', 'recipes_cooking_complete');
350
356
  }
351
357
  if (allowNoMealPlan && mealPlanEntryId) {
352
- throw new Error('mealPlanEntryId should not be provided when allow_no_meal_plan is true. Use recipeId instead.');
358
+ throw new ValidationError('mealPlanEntryId should not be provided when allow_no_meal_plan is true. Use recipeId instead.', 'recipes_cooking_complete');
353
359
  }
354
360
  this.validateRequired({ stockAmounts }, ['stockAmounts']);
355
361
  if (!Array.isArray(stockAmounts) || stockAmounts.length === 0) {
356
- throw new Error('stockAmounts must be a non-empty array of serving amounts.');
362
+ throw new ValidationError('stockAmounts must be a non-empty array of serving amounts.', 'recipes_cooking_complete');
357
363
  }
358
364
  // Validate all stock amounts are positive numbers
359
365
  for (let i = 0; i < stockAmounts.length; i++) {
360
366
  const amount = stockAmounts[i];
361
367
  if (typeof amount !== 'number' || amount <= 0) {
362
- throw new Error(`stockAmounts[${i}] must be a positive number, got: ${amount}`);
368
+ throw new ValidationError(`stockAmounts[${i}] must be a positive number, got: ${amount}`, 'recipes_cooking_complete');
363
369
  }
364
370
  }
365
371
  const completedSteps = [];
@@ -379,10 +385,10 @@ export class RecipeToolHandlers extends BaseToolHandler {
379
385
  // Meal plan mode - traditional workflow
380
386
  const mealPlanEntry = await this.apiCall(`/objects/meal_plan/${mealPlanEntryId}`);
381
387
  if (!mealPlanEntry) {
382
- throw new Error(`Meal plan entry ${mealPlanEntryId} not found.`);
388
+ throw new ValidationError(`Meal plan entry ${mealPlanEntryId} not found.`, 'recipes_cooking_complete');
383
389
  }
384
390
  if (mealPlanEntry.done == 1 && !allowMealPlanEntryAlreadyDone) {
385
- throw new Error(`Meal plan entry ${mealPlanEntryId} is already marked as done. Cannot mark as cooked again.`);
391
+ throw new ValidationError(`Meal plan entry ${mealPlanEntryId} is already marked as done. Cannot mark as cooked again.`, 'recipes_cooking_complete');
386
392
  }
387
393
  actualRecipeId = mealPlanEntry.recipe_id;
388
394
  totalServings = stockAmounts.reduce((sum, amount) => sum + amount, 0);
@@ -391,7 +397,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
391
397
  // Mark the meal plan entry as done and update recipe_servings
392
398
  await this.apiCall(`/objects/meal_plan/${mealPlanEntryId}`, 'PUT', {
393
399
  done: 1,
394
- recipe_servings: totalServings
400
+ recipe_servings: totalServings,
395
401
  });
396
402
  completedSteps.push('Meal plan entry marked as done');
397
403
  }
@@ -404,10 +410,10 @@ export class RecipeToolHandlers extends BaseToolHandler {
404
410
  else {
405
411
  // Query for the mealplan shadow recipe by name
406
412
  const shadowRecipes = await this.apiCall('/objects/recipes', 'GET', undefined, {
407
- queryParams: { 'query[]': `name=${mealplanShadow}` }
413
+ queryParams: { 'query[]': `name=${mealplanShadow}` },
408
414
  });
409
415
  if (!Array.isArray(shadowRecipes) || shadowRecipes.length === 0) {
410
- throw new Error(`Mealplan shadow recipe '${mealplanShadow}' not found. Cannot consume ingredients.`);
416
+ throw new ValidationError(`Mealplan shadow recipe '${mealplanShadow}' not found. Cannot consume ingredients.`, 'recipes_cooking_complete');
411
417
  }
412
418
  const shadowRecipeId = shadowRecipes[0].id;
413
419
  // Consume ingredients using the shadow recipe ID
@@ -415,15 +421,15 @@ export class RecipeToolHandlers extends BaseToolHandler {
415
421
  completedSteps.push('Recipe consumed via meal plan entry');
416
422
  }
417
423
  // Handle stock entry splitting and label printing
418
- let stockEntries = { splitEntries: [], labelsPrinted: 0 };
424
+ const stockEntries = { splitEntries: [], labelsPrinted: 0 };
419
425
  const recipe = await this.apiCall(`/objects/recipes/${actualRecipeId}`);
420
426
  if (recipe && recipe.product_id) {
421
427
  // Get product details and most recent stock entry
422
428
  const [product, entries] = await Promise.all([
423
429
  this.apiCall(`/objects/products/${recipe.product_id}`),
424
430
  this.apiCall(`/stock/products/${recipe.product_id}/entries`, 'GET', undefined, {
425
- queryParams: { order: 'row_created_timestamp:desc', limit: '1' }
426
- })
431
+ queryParams: { order: 'row_created_timestamp:desc', limit: '1' },
432
+ }),
427
433
  ]);
428
434
  // Get quantity unit info
429
435
  let quantityUnit = null;
@@ -464,7 +470,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
464
470
  return this.createSuccess({
465
471
  message: `Recipe ${actualRecipeId} cooked (${totalServings} servings consumed, ${stockEntries.splitEntries.length} stock entries created, ${stockEntries.labelsPrinted} labels printed)`,
466
472
  stockEntries,
467
- completedSteps
473
+ completedSteps,
468
474
  });
469
475
  });
470
476
  };
@@ -24,10 +24,10 @@ export const recipeModule = {
24
24
  recipes_cooking_complete: handlers.cookedSomething,
25
25
  // Shopping Integration
26
26
  recipes_shopping_add_all_products: handlers.addAllProductsToShopping,
27
- recipes_shopping_add_missing_products: handlers.addMissingProductsToShopping
27
+ recipes_shopping_add_missing_products: handlers.addMissingProductsToShopping,
28
28
  },
29
29
  validators: {
30
- recipes_cooking_complete: validateCompleteSubConfigs
31
- }
30
+ recipes_cooking_complete: validateCompleteSubConfigs,
31
+ },
32
32
  };
33
33
  export * from './definitions.js';
@@ -2,6 +2,7 @@
2
2
  * Recipe tools sub-configuration validation functions
3
3
  */
4
4
  import { ValidationHelpers } from '../validation-helpers.js';
5
+ import { ValidationError } from '../../utils/errors.js';
5
6
  /**
6
7
  * Validation function for complete tool sub-configurations
7
8
  */
@@ -15,9 +16,14 @@ export const validateCompleteSubConfigs = (subConfigs) => {
15
16
  ValidationHelpers.validateBoolean(printLabels, 'print_labels');
16
17
  // Business logic validation
17
18
  if (allowNoMealPlan && allowMealPlanEntryAlreadyDone) {
18
- throw new Error('allow_no_meal_plan and allow_meal_plan_entry_already_done cannot both be true - they are mutually exclusive modes');
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');
19
20
  }
20
21
  // Check for unknown options
21
- const knownOptions = new Set(['allow_meal_plan_entry_already_done', 'allow_no_meal_plan', 'print_labels', 'ack_token']);
22
+ const knownOptions = new Set([
23
+ 'allow_meal_plan_entry_already_done',
24
+ 'allow_no_meal_plan',
25
+ 'print_labels',
26
+ 'ack_token',
27
+ ]);
22
28
  ValidationHelpers.validateKnownOptions(subConfigs, knownOptions, 'complete');
23
29
  };