mcp-grocy 2.0.0 → 2.0.1
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.
|
@@ -134,13 +134,18 @@ export async function createToolRegistry() {
|
|
|
134
134
|
const { toolModules } = await ModuleLoader.loadAllModules();
|
|
135
135
|
const definitions = [];
|
|
136
136
|
const handlers = {};
|
|
137
|
+
const validators = {};
|
|
137
138
|
for (const module of toolModules) {
|
|
138
139
|
definitions.push(...module.definitions);
|
|
139
140
|
Object.assign(handlers, module.handlers);
|
|
141
|
+
if (module.validators) {
|
|
142
|
+
Object.assign(validators, module.validators);
|
|
143
|
+
}
|
|
140
144
|
}
|
|
141
145
|
return {
|
|
142
146
|
getDefinitions: () => definitions,
|
|
143
147
|
getHandler: (name) => handlers[name],
|
|
148
|
+
getValidator: (name) => validators[name],
|
|
144
149
|
getToolNames: () => definitions.map(def => def.name)
|
|
145
150
|
};
|
|
146
151
|
}
|
|
@@ -6,6 +6,25 @@ import { BaseToolHandler } from '../base.js';
|
|
|
6
6
|
import { InventoryToolHandlers } from '../inventory/handlers.js';
|
|
7
7
|
export class RecipeToolHandlers extends BaseToolHandler {
|
|
8
8
|
inventoryHandlers = new InventoryToolHandlers();
|
|
9
|
+
/**
|
|
10
|
+
* Helper method to get meal plan for multiple days
|
|
11
|
+
*/
|
|
12
|
+
async getMealPlanForDays(dates) {
|
|
13
|
+
const allResults = [];
|
|
14
|
+
for (const date of dates) {
|
|
15
|
+
const dayString = date.toISOString().split('T')[0];
|
|
16
|
+
const dayResult = await this.apiCall(`/objects/meal_plan`, 'GET', undefined, {
|
|
17
|
+
queryParams: {
|
|
18
|
+
'query[]': `day=${dayString}`,
|
|
19
|
+
order: 'day'
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
if (Array.isArray(dayResult)) {
|
|
23
|
+
allResults.push(...dayResult);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return allResults;
|
|
27
|
+
}
|
|
9
28
|
/**
|
|
10
29
|
* Get recipes with specified fields
|
|
11
30
|
*/
|
|
@@ -134,35 +153,78 @@ export class RecipeToolHandlers extends BaseToolHandler {
|
|
|
134
153
|
getMealPlan = async (args) => {
|
|
135
154
|
return this.executeToolHandler(async () => {
|
|
136
155
|
const { date, weekly } = args || {};
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
params.set('dates[0]', startOfWeek.toISOString().split('T')[0]);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
// Get the day before for context
|
|
159
|
-
const dayBefore = new Date(targetDate);
|
|
160
|
-
dayBefore.setDate(targetDate.getDate() - 1);
|
|
161
|
-
params.set('dates[0]', dayBefore.toISOString().split('T')[0]);
|
|
156
|
+
// Date parameter is mandatory
|
|
157
|
+
this.validateRequired({ date }, ['date']);
|
|
158
|
+
const targetDate = new Date(date);
|
|
159
|
+
if (isNaN(targetDate.getTime())) {
|
|
160
|
+
throw new Error('Invalid date format. Use YYYY-MM-DD.');
|
|
161
|
+
}
|
|
162
|
+
const datesToQuery = [];
|
|
163
|
+
if (weekly) {
|
|
164
|
+
// For weekly view, get the start of the week (Monday)
|
|
165
|
+
const dayOfWeek = targetDate.getDay();
|
|
166
|
+
const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
|
|
167
|
+
const startDate = new Date(targetDate);
|
|
168
|
+
startDate.setDate(targetDate.getDate() + mondayOffset);
|
|
169
|
+
// Get all 7 days of the week with ±1 day buffer for timezone issues
|
|
170
|
+
for (let i = -1; i <= 7; i++) {
|
|
171
|
+
const day = new Date(startDate);
|
|
172
|
+
day.setDate(startDate.getDate() + i);
|
|
173
|
+
datesToQuery.push(day);
|
|
162
174
|
}
|
|
163
175
|
}
|
|
164
|
-
|
|
165
|
-
|
|
176
|
+
else {
|
|
177
|
+
// Get the specific date with ±1 day buffer for timezone issues
|
|
178
|
+
const dayBefore = new Date(targetDate);
|
|
179
|
+
dayBefore.setDate(targetDate.getDate() - 1);
|
|
180
|
+
const dayAfter = new Date(targetDate);
|
|
181
|
+
dayAfter.setDate(targetDate.getDate() + 1);
|
|
182
|
+
datesToQuery.push(dayBefore, targetDate, dayAfter);
|
|
183
|
+
}
|
|
184
|
+
const result = await this.getMealPlanForDays(datesToQuery);
|
|
185
|
+
if (result.length === 0) {
|
|
186
|
+
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
|
+
}, 'Meal plan retrieved successfully');
|
|
190
|
+
}
|
|
191
|
+
// Extract unique recipe IDs
|
|
192
|
+
const recipeIds = [...new Set(result.map(entry => entry.recipe_id).filter(id => id))];
|
|
193
|
+
// Fetch recipe details and all sections in parallel
|
|
194
|
+
const [recipeDetails, allSections] = await Promise.all([
|
|
195
|
+
Promise.all(recipeIds.map(recipeId => this.apiCall(`/objects/recipes/${recipeId}`))),
|
|
196
|
+
this.apiCall('/objects/meal_plan_sections')
|
|
197
|
+
]);
|
|
198
|
+
// Simplify meal plan entries - don't merge recipe/section details
|
|
199
|
+
const simplifiedMealPlanByDate = {};
|
|
200
|
+
result.forEach(entry => {
|
|
201
|
+
const entryDate = entry.day;
|
|
202
|
+
if (!simplifiedMealPlanByDate[entryDate]) {
|
|
203
|
+
simplifiedMealPlanByDate[entryDate] = [];
|
|
204
|
+
}
|
|
205
|
+
simplifiedMealPlanByDate[entryDate].push({
|
|
206
|
+
id: entry.id,
|
|
207
|
+
day: entry.day,
|
|
208
|
+
section_id: entry.section_id,
|
|
209
|
+
recipe_id: entry.recipe_id,
|
|
210
|
+
recipe_servings: entry.recipe_servings,
|
|
211
|
+
note: entry.note,
|
|
212
|
+
done: entry.done
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
return this.createSuccess({
|
|
216
|
+
meal_plan_by_date: simplifiedMealPlanByDate,
|
|
217
|
+
recipes: recipeDetails.map((recipe) => ({
|
|
218
|
+
id: recipe.id,
|
|
219
|
+
name: recipe.name,
|
|
220
|
+
product_id: recipe.product_id,
|
|
221
|
+
})),
|
|
222
|
+
sections: Array.isArray(allSections) ? allSections.map((section) => ({
|
|
223
|
+
id: section.id,
|
|
224
|
+
name: section.name,
|
|
225
|
+
time_info: section.time_info
|
|
226
|
+
})) : []
|
|
227
|
+
}, 'Meal plan retrieved successfully');
|
|
166
228
|
});
|
|
167
229
|
};
|
|
168
230
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { recipeToolDefinitions } from './definitions.js';
|
|
2
2
|
import { RecipeToolHandlers } from './handlers.js';
|
|
3
|
+
import { validateCompleteSubConfigs } from './validations.js';
|
|
3
4
|
// Use simplified handlers
|
|
4
5
|
const handlers = new RecipeToolHandlers();
|
|
5
6
|
export const recipeModule = {
|
|
@@ -24,6 +25,9 @@ export const recipeModule = {
|
|
|
24
25
|
// Shopping Integration
|
|
25
26
|
recipes_shopping_add_all_products: handlers.addAllProductsToShopping,
|
|
26
27
|
recipes_shopping_add_missing_products: handlers.addMissingProductsToShopping
|
|
28
|
+
},
|
|
29
|
+
validators: {
|
|
30
|
+
recipes_cooking_complete: validateCompleteSubConfigs
|
|
27
31
|
}
|
|
28
32
|
};
|
|
29
33
|
export * from './definitions.js';
|
|
@@ -1,77 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Common validation helpers for all tools
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Auto-discover and load validator functions for tools
|
|
6
|
-
*/
|
|
7
|
-
export class ValidatorRegistry {
|
|
8
|
-
static validatorCache = new Map();
|
|
9
|
-
/**
|
|
10
|
-
* Get validator for a tool by automatically discovering and loading it
|
|
11
|
-
* Convention: tools/[module]/validations.ts should export validate[ToolName]SubConfigs
|
|
12
|
-
*/
|
|
13
|
-
static async getValidator(toolName) {
|
|
14
|
-
if (this.validatorCache.has(toolName)) {
|
|
15
|
-
return this.validatorCache.get(toolName);
|
|
16
|
-
}
|
|
17
|
-
try {
|
|
18
|
-
// Convert tool_name to ToolName (camelCase with first letter uppercase)
|
|
19
|
-
const validatorFunctionName = this.getValidatorFunctionName(toolName);
|
|
20
|
-
// Try to dynamically import the validator from the appropriate module
|
|
21
|
-
const modulePath = this.getModulePath(toolName);
|
|
22
|
-
const validationsModule = await import(modulePath);
|
|
23
|
-
const validator = validationsModule[validatorFunctionName];
|
|
24
|
-
if (validator && typeof validator === 'function') {
|
|
25
|
-
this.validatorCache.set(toolName, validator);
|
|
26
|
-
return validator;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
30
|
-
// Validator doesn't exist or failed to load - that's fine, not all tools need validators
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Convert tool_name to expected validator function name
|
|
36
|
-
* Example: complete -> validateCompleteSubConfigs
|
|
37
|
-
*/
|
|
38
|
-
static getValidatorFunctionName(toolName) {
|
|
39
|
-
const camelCase = toolName.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
40
|
-
const pascalCase = camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
|
|
41
|
-
return `validate${pascalCase}SubConfigs`;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Determine which module a tool belongs to based on its name
|
|
45
|
-
*/
|
|
46
|
-
static getModulePath(toolName) {
|
|
47
|
-
// Map tool names to their modules
|
|
48
|
-
const moduleMap = {
|
|
49
|
-
// Recipe tools
|
|
50
|
-
'get_recipes': './recipes/validations.js',
|
|
51
|
-
'get_recipe_by_id': './recipes/validations.js',
|
|
52
|
-
'create_recipe': './recipes/validations.js',
|
|
53
|
-
'get_recipe_fulfillment': './recipes/validations.js',
|
|
54
|
-
'get_recipes_fulfillment': './recipes/validations.js',
|
|
55
|
-
'consume_recipe': './recipes/validations.js',
|
|
56
|
-
'add_recipe_products_to_shopping_list': './recipes/validations.js',
|
|
57
|
-
'add_missing_products_to_shopping_list': './recipes/validations.js',
|
|
58
|
-
'complete': './recipes/validations.js',
|
|
59
|
-
// Stock tools
|
|
60
|
-
'get_all_stock': './stock/validations.js',
|
|
61
|
-
'get_stock_volatile': './stock/validations.js',
|
|
62
|
-
'get_stock_by_location': './stock/validations.js',
|
|
63
|
-
'inventory_product': './stock/validations.js',
|
|
64
|
-
'purchase_product': './stock/validations.js',
|
|
65
|
-
'consume_product': './stock/validations.js',
|
|
66
|
-
'transfer_product': './stock/validations.js',
|
|
67
|
-
'open_product': './stock/validations.js',
|
|
68
|
-
'lookup_product': './stock/validations.js',
|
|
69
|
-
'print_stock_entry_label': './stock/validations.js',
|
|
70
|
-
// Add more modules as needed...
|
|
71
|
-
};
|
|
72
|
-
return moduleMap[toolName] || `./unknown/validations.js`;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
1
|
/**
|
|
76
2
|
* Common validation utility functions
|
|
77
3
|
*/
|
package/build/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-grocy",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Grocy integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -85,5 +85,8 @@
|
|
|
85
85
|
"homepage": "https://github.com/miguelangel-nubla/mcp-grocy#readme",
|
|
86
86
|
"engines": {
|
|
87
87
|
"node": ">=18.0.0"
|
|
88
|
+
},
|
|
89
|
+
"config": {
|
|
90
|
+
"supportedGrocyVersion": "4.5.0"
|
|
88
91
|
}
|
|
89
92
|
}
|