mcp-grocy 2.0.0 → 2.0.2

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.
@@ -177,9 +177,14 @@ export class ConfigManager {
177
177
  parseToolConfiguration() {
178
178
  const enabledTools = new Set();
179
179
  const toolSubConfigs = new Map();
180
+ const toolAckTokens = new Map();
180
181
  for (const [toolName, toolConfig] of Object.entries(this.config.yaml.tools)) {
181
182
  if (toolConfig.enabled) {
182
183
  enabledTools.add(toolName);
184
+ // Store ack_token separately if configured
185
+ if (toolConfig.ack_token && typeof toolConfig.ack_token === 'string') {
186
+ toolAckTokens.set(toolName, toolConfig.ack_token);
187
+ }
183
188
  // Extract sub-configs (everything except standard fields)
184
189
  const subConfigs = new Map();
185
190
  for (const [key, value] of Object.entries(toolConfig)) {
@@ -192,7 +197,7 @@ export class ConfigManager {
192
197
  }
193
198
  }
194
199
  }
195
- return { enabledTools, toolSubConfigs };
200
+ return { enabledTools, toolSubConfigs, toolAckTokens };
196
201
  }
197
202
  }
198
203
  // Export singleton instance
@@ -17,6 +17,7 @@ export class GrocyMcpServer {
17
17
  server;
18
18
  enabledTools = new Set();
19
19
  toolSubConfigs = new Map();
20
+ toolAckTokens = new Map();
20
21
  resourceHandler;
21
22
  toolRegistry;
22
23
  constructor(server, toolRegistry, resourceHandler) {
@@ -49,8 +50,9 @@ export class GrocyMcpServer {
49
50
  return new GrocyMcpServer(server, toolRegistry, resourceHandler);
50
51
  }
51
52
  parseToolConfiguration() {
52
- const { enabledTools, toolSubConfigs } = config.parseToolConfiguration();
53
+ const { enabledTools, toolSubConfigs, toolAckTokens } = config.parseToolConfiguration();
53
54
  this.toolSubConfigs = toolSubConfigs;
55
+ this.toolAckTokens = toolAckTokens;
54
56
  // Validate tool names
55
57
  const validToolNames = new Set(this.toolRegistry.getToolNames());
56
58
  if (enabledTools.size > 0) {
@@ -99,11 +101,11 @@ export class GrocyMcpServer {
99
101
  try {
100
102
  const subConfigs = this.toolSubConfigs.get(toolName);
101
103
  const result = await handler(args, subConfigs);
102
- // Automatically add ack_token to successful responses
103
- if (!result.isError && subConfigs) {
104
- const ackToken = subConfigs.get('ack_token');
105
- if (ackToken && typeof ackToken === 'string') {
106
- result.content.push({
104
+ // Automatically add ack_token to successful responses (at the beginning)
105
+ if (!result.isError) {
106
+ const ackToken = this.toolAckTokens.get(toolName);
107
+ if (ackToken) {
108
+ result.content.unshift({
107
109
  type: 'text',
108
110
  text: `Acknowledgment token: ${ackToken}`
109
111
  });
@@ -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
- // Build query parameters for meal plan with context
138
- const params = new URLSearchParams({
139
- force_today: '1',
140
- order: 'day',
141
- limit: weekly ? '14' : '3',
142
- offset: weekly ? '-7' : '-1'
143
- });
144
- if (date) {
145
- const targetDate = new Date(date);
146
- if (isNaN(targetDate.getTime())) {
147
- throw new Error('Invalid date format. Use YYYY-MM-DD.');
148
- }
149
- if (weekly) {
150
- // For weekly view, get the start of the week (Monday)
151
- const dayOfWeek = targetDate.getDay();
152
- const mondayOffset = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
153
- const startOfWeek = new Date(targetDate);
154
- startOfWeek.setDate(targetDate.getDate() + mondayOffset);
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
- const result = await this.apiCall(`/objects/meal_plan`, 'GET', undefined, { queryParams: Object.fromEntries(params) });
165
- return this.createSuccess(result, 'Meal plan retrieved successfully');
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
@@ -1,4 +1,4 @@
1
1
  // Auto-generated file - DO NOT MODIFY
2
- export const VERSION = '2.0.0';
2
+ export const VERSION = '2.0.2';
3
3
  export const PACKAGE_NAME = 'mcp-grocy';
4
4
  export const SERVER_NAME = 'grocy-api';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-grocy",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
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
  }