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.
- package/CHANGELOG.md +31 -0
- package/README.md +112 -32
- package/build/api/client.js +11 -13
- package/build/config/index.js +74 -20
- package/build/resources/CHANGELOG.md +31 -0
- package/build/resources/DOCS.md +8 -3
- package/build/resources/README.md +112 -32
- package/build/resources/api-reference.md +88 -88
- package/build/resources/config.md +30 -17
- package/build/resources/examples.md +120 -221
- package/build/resources/installation.md +9 -14
- package/build/resources/response-format.md +18 -13
- package/build/server/http-server.js +154 -42
- package/build/server/mcp-server.js +143 -96
- package/build/server/resources.js +37 -24
- package/build/server/tool-input-zod.js +86 -0
- package/build/tools/base.js +19 -15
- package/build/tools/household/definitions.js +47 -43
- package/build/tools/household/handlers.js +2 -2
- package/build/tools/household/index.js +2 -2
- package/build/tools/inventory/definitions.js +150 -113
- package/build/tools/inventory/handlers.js +55 -42
- package/build/tools/inventory/index.js +2 -2
- package/build/tools/module-loader.js +15 -12
- package/build/tools/recipes/definitions.js +103 -86
- package/build/tools/recipes/handlers.js +44 -38
- package/build/tools/recipes/index.js +3 -3
- package/build/tools/recipes/validations.js +8 -2
- package/build/tools/shopping/definitions.js +22 -20
- package/build/tools/shopping/handlers.js +1 -1
- package/build/tools/shopping/index.js +2 -2
- package/build/tools/system/definitions.js +25 -22
- package/build/tools/system/handlers.js +74 -12
- package/build/tools/system/index.js +2 -2
- package/build/tools/validation-helpers.js +10 -7
- package/build/types/index.js +12 -10
- package/build/utils/errors.js +10 -5
- package/build/utils/logger.js +16 -15
- package/build/version.js +2 -2
- 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]
|
|
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: [
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
252
|
+
...(allowNoMealPlan
|
|
253
|
+
? {
|
|
254
|
+
recipeId: {
|
|
255
|
+
type: 'number',
|
|
256
|
+
description: 'ID of the recipe to cook directly.',
|
|
257
|
+
},
|
|
238
258
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
188
|
-
|
|
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)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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([
|
|
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
|
};
|