mcp-grocy 2.7.2 → 2.8.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 +15 -2
- package/README.md +1 -1
- package/build/resources/CHANGELOG.md +15 -2
- package/build/resources/README.md +1 -1
- package/build/resources/examples.md +13 -1
- package/build/server/tool-input-zod.js +37 -5
- package/build/tools/base.js +37 -4
- package/build/tools/base.test.js +64 -0
- package/build/tools/recipes/definitions.js +28 -6
- package/build/tools/recipes/handlers.js +18 -3
- package/build/tools/recipes/index.js +2 -1
- package/build/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
# [2.8.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.2...v2.8.0) (2026-09-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* accept numbers and booleans encoded as strings in tool arguments ([5dfbbc7](https://github.com/miguelangel-nubla/mcp-grocy/commit/5dfbbc799fdfde36255f6fcc3665e0113f36cf14))
|
|
7
|
+
* advertise tool parameter descriptions in tools/list ([0418bf5](https://github.com/miguelangel-nubla/mcp-grocy/commit/0418bf5be83428f19e52c2952e2f3e5bdd078d73))
|
|
8
|
+
* **tools:** normalize created_object_id and id to numbers in responses ([5f613d6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5f613d6b6c87f860ac3a79e6d8a473a91d19d3a7))
|
|
2
9
|
|
|
3
10
|
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add recipes_mealplan_add_note tool for meal plan notes with sections ([e80aa65](https://github.com/miguelangel-nubla/mcp-grocy/commit/e80aa651e9653bf765d361a4ebc3d285d325a23e)), closes [#2](https://github.com/miguelangel-nubla/mcp-grocy/issues/2)
|
|
14
|
+
|
|
15
|
+
## [2.7.2](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.1...v2.7.2) (2026-09-10)
|
|
16
|
+
|
|
4
17
|
### Bug Fixes
|
|
5
18
|
|
|
6
|
-
|
|
19
|
+
- add prepack script, update release configs, and fix docker workflows ([c464c05](https://github.com/miguelangel-nubla/mcp-grocy/commit/c464c054ab4f3f420647196ded7454c60f3702fc))
|
|
7
20
|
|
|
8
21
|
## [2.7.1](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.0...v2.7.1) (2026-09-10)
|
|
9
22
|
|
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ Transform your LLM into an intelligent household management assistant with focus
|
|
|
54
54
|
### 🛒 **Smart Shopping & Planning**
|
|
55
55
|
|
|
56
56
|
- Maintain shopping lists with intelligent quantity management
|
|
57
|
-
- Plan meals with recipe scheduling and fulfillment checking
|
|
57
|
+
- Plan meals with recipe scheduling, free-text notes per meal plan section, and fulfillment checking
|
|
58
58
|
- Automatically add missing ingredients to shopping lists
|
|
59
59
|
- Track shopping locations and optimize store visits
|
|
60
60
|
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
|
|
1
|
+
# [2.8.0](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.2...v2.8.0) (2026-09-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* accept numbers and booleans encoded as strings in tool arguments ([5dfbbc7](https://github.com/miguelangel-nubla/mcp-grocy/commit/5dfbbc799fdfde36255f6fcc3665e0113f36cf14))
|
|
7
|
+
* advertise tool parameter descriptions in tools/list ([0418bf5](https://github.com/miguelangel-nubla/mcp-grocy/commit/0418bf5be83428f19e52c2952e2f3e5bdd078d73))
|
|
8
|
+
* **tools:** normalize created_object_id and id to numbers in responses ([5f613d6](https://github.com/miguelangel-nubla/mcp-grocy/commit/5f613d6b6c87f860ac3a79e6d8a473a91d19d3a7))
|
|
2
9
|
|
|
3
10
|
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add recipes_mealplan_add_note tool for meal plan notes with sections ([e80aa65](https://github.com/miguelangel-nubla/mcp-grocy/commit/e80aa651e9653bf765d361a4ebc3d285d325a23e)), closes [#2](https://github.com/miguelangel-nubla/mcp-grocy/issues/2)
|
|
14
|
+
|
|
15
|
+
## [2.7.2](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.1...v2.7.2) (2026-09-10)
|
|
16
|
+
|
|
4
17
|
### Bug Fixes
|
|
5
18
|
|
|
6
|
-
|
|
19
|
+
- add prepack script, update release configs, and fix docker workflows ([c464c05](https://github.com/miguelangel-nubla/mcp-grocy/commit/c464c054ab4f3f420647196ded7454c60f3702fc))
|
|
7
20
|
|
|
8
21
|
## [2.7.1](https://github.com/miguelangel-nubla/mcp-grocy/compare/v2.7.0...v2.7.1) (2026-09-10)
|
|
9
22
|
|
|
@@ -54,7 +54,7 @@ Transform your LLM into an intelligent household management assistant with focus
|
|
|
54
54
|
### 🛒 **Smart Shopping & Planning**
|
|
55
55
|
|
|
56
56
|
- Maintain shopping lists with intelligent quantity management
|
|
57
|
-
- Plan meals with recipe scheduling and fulfillment checking
|
|
57
|
+
- Plan meals with recipe scheduling, free-text notes per meal plan section, and fulfillment checking
|
|
58
58
|
- Automatically add missing ingredients to shopping lists
|
|
59
59
|
- Track shopping locations and optimize store visits
|
|
60
60
|
|
|
@@ -122,6 +122,18 @@ use_mcp_tool('mcp-grocy', 'recipes_mealplan_add_recipe', {
|
|
|
122
122
|
|
|
123
123
|
Use **`recipes_mealplan_get_sections`** for valid `sectionId` values.
|
|
124
124
|
|
|
125
|
+
### Add a note to the meal plan (no recipe)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
use_mcp_tool('mcp-grocy', 'recipes_mealplan_add_note', {
|
|
129
|
+
day: '2024-07-01',
|
|
130
|
+
note: 'Leftovers from Sunday',
|
|
131
|
+
sectionId: 3,
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Same `sectionId` values as above (`-1` = Grocy's built-in "no section").
|
|
136
|
+
|
|
125
137
|
### Remove a meal plan entry
|
|
126
138
|
|
|
127
139
|
```typescript
|
|
@@ -130,7 +142,7 @@ use_mcp_tool('mcp-grocy', 'recipes_mealplan_delete_entry', {
|
|
|
130
142
|
});
|
|
131
143
|
```
|
|
132
144
|
|
|
133
|
-
Use **`recipes_mealplan_get`** to find `mealPlanEntryId`.
|
|
145
|
+
Use **`recipes_mealplan_get`** to find `mealPlanEntryId`. Works for recipe and note entries.
|
|
134
146
|
|
|
135
147
|
### Cook / consume recipe ingredients
|
|
136
148
|
|
|
@@ -6,6 +6,36 @@ import { z } from 'zod';
|
|
|
6
6
|
function isStringTupleEnum(values) {
|
|
7
7
|
return values.length > 0 && values.every((v) => typeof v === 'string');
|
|
8
8
|
}
|
|
9
|
+
/**
|
|
10
|
+
* Some MCP clients serialize every tool argument as a string ("12", "true"). Accept those
|
|
11
|
+
* encodings for number/boolean properties: z.preprocess converts them before validation, and the
|
|
12
|
+
* JSON Schema advertised by tools/list still shows the wrapped type because Zod 4's toJSONSchema
|
|
13
|
+
* (io: 'input', which the SDK uses) resolves a preprocess pipe to its output schema — pinned by
|
|
14
|
+
* tests/tool-input-zod.test.ts. Only JSON number syntax and "true"/"false" are converted;
|
|
15
|
+
* anything else is passed through unchanged so the usual type error is still raised.
|
|
16
|
+
*/
|
|
17
|
+
const JSON_NUMBER = /^-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?$/;
|
|
18
|
+
function numericStringToNumber(value) {
|
|
19
|
+
if (typeof value !== 'string')
|
|
20
|
+
return value;
|
|
21
|
+
const trimmed = value.trim();
|
|
22
|
+
if (!JSON_NUMBER.test(trimmed))
|
|
23
|
+
return value;
|
|
24
|
+
const parsed = Number(trimmed);
|
|
25
|
+
return Number.isFinite(parsed) ? parsed : value;
|
|
26
|
+
}
|
|
27
|
+
function booleanStringToBoolean(value) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return value;
|
|
30
|
+
const normalized = value.trim().toLowerCase();
|
|
31
|
+
if (normalized === 'true')
|
|
32
|
+
return true;
|
|
33
|
+
if (normalized === 'false')
|
|
34
|
+
return false;
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
const lenientNumber = () => z.preprocess(numericStringToNumber, z.number());
|
|
38
|
+
const lenientBoolean = () => z.preprocess(booleanStringToBoolean, z.boolean());
|
|
9
39
|
function propertyToZod(prop, required) {
|
|
10
40
|
const t = prop.type;
|
|
11
41
|
let inner;
|
|
@@ -21,10 +51,10 @@ function propertyToZod(prop, required) {
|
|
|
21
51
|
}
|
|
22
52
|
case 'number':
|
|
23
53
|
case 'integer':
|
|
24
|
-
inner =
|
|
54
|
+
inner = lenientNumber();
|
|
25
55
|
break;
|
|
26
56
|
case 'boolean':
|
|
27
|
-
inner =
|
|
57
|
+
inner = lenientBoolean();
|
|
28
58
|
break;
|
|
29
59
|
case 'array': {
|
|
30
60
|
const items = prop.items;
|
|
@@ -35,10 +65,10 @@ function propertyToZod(prop, required) {
|
|
|
35
65
|
inner = z.array(z.string());
|
|
36
66
|
}
|
|
37
67
|
else if (items?.type === 'number' || items?.type === 'integer') {
|
|
38
|
-
inner = z.array(
|
|
68
|
+
inner = z.array(lenientNumber());
|
|
39
69
|
}
|
|
40
70
|
else if (items?.type === 'boolean') {
|
|
41
|
-
inner = z.array(
|
|
71
|
+
inner = z.array(lenientBoolean());
|
|
42
72
|
}
|
|
43
73
|
else {
|
|
44
74
|
inner = z.array(z.unknown());
|
|
@@ -62,7 +92,9 @@ function propertyToZod(prop, required) {
|
|
|
62
92
|
default:
|
|
63
93
|
inner = z.unknown();
|
|
64
94
|
}
|
|
65
|
-
|
|
95
|
+
// Keep the per-property description in the advertised JSON Schema (LLM clients read it).
|
|
96
|
+
const described = prop.description ? inner.describe(prop.description) : inner;
|
|
97
|
+
return required ? described : described.optional();
|
|
66
98
|
}
|
|
67
99
|
function objectPropsToZod(properties, requiredList) {
|
|
68
100
|
const required = new Set(requiredList ?? []);
|
package/build/tools/base.js
CHANGED
|
@@ -6,10 +6,43 @@ import { config } from '../config/index.js';
|
|
|
6
6
|
import { ErrorHandler, ValidationError } from '../utils/errors.js';
|
|
7
7
|
import { logger } from '../utils/logger.js';
|
|
8
8
|
export class BaseToolHandler {
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes response data so IDs (like created_object_id or entity id) are numeric.
|
|
11
|
+
*/
|
|
12
|
+
normalizeResponseData(data) {
|
|
13
|
+
if (!data || typeof data !== 'object') {
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
if (Array.isArray(data)) {
|
|
17
|
+
return data.map((item) => this.normalizeResponseData(item));
|
|
18
|
+
}
|
|
19
|
+
if (Object.prototype.toString.call(data) !== '[object Object]') {
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
const normalized = {};
|
|
23
|
+
for (const [key, value] of Object.entries(data)) {
|
|
24
|
+
if (key === 'created_object_id' && value !== null && value !== undefined) {
|
|
25
|
+
const num = Number(value);
|
|
26
|
+
normalized[key] = Number.isInteger(num) && num > 0 ? num : value;
|
|
27
|
+
}
|
|
28
|
+
else if (key === 'id' && typeof value === 'string') {
|
|
29
|
+
const num = Number(value);
|
|
30
|
+
normalized[key] = Number.isInteger(num) && num > 0 ? num : value;
|
|
31
|
+
}
|
|
32
|
+
else if (value && typeof value === 'object') {
|
|
33
|
+
normalized[key] = this.normalizeResponseData(value);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
normalized[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
9
41
|
/**
|
|
10
42
|
* Create a standardized success result
|
|
11
43
|
*/
|
|
12
44
|
createSuccess(data, message) {
|
|
45
|
+
const normalizedData = this.normalizeResponseData(data);
|
|
13
46
|
let serializeStructured;
|
|
14
47
|
try {
|
|
15
48
|
serializeStructured = config?.server?.serialize_structured_to_content ?? false;
|
|
@@ -17,10 +50,10 @@ export class BaseToolHandler {
|
|
|
17
50
|
catch {
|
|
18
51
|
serializeStructured = false;
|
|
19
52
|
}
|
|
20
|
-
const textContent = serializeStructured &&
|
|
53
|
+
const textContent = serializeStructured && normalizedData !== undefined && normalizedData !== null
|
|
21
54
|
? message
|
|
22
|
-
? `${message}\n${this.safeStringify(
|
|
23
|
-
: this.safeStringify(
|
|
55
|
+
? `${message}\n${this.safeStringify(normalizedData)}`
|
|
56
|
+
: this.safeStringify(normalizedData)
|
|
24
57
|
: message || 'Operation completed successfully';
|
|
25
58
|
return {
|
|
26
59
|
content: [
|
|
@@ -30,7 +63,7 @@ export class BaseToolHandler {
|
|
|
30
63
|
},
|
|
31
64
|
],
|
|
32
65
|
structuredContent: {
|
|
33
|
-
data:
|
|
66
|
+
data: normalizedData ?? null,
|
|
34
67
|
},
|
|
35
68
|
};
|
|
36
69
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { BaseToolHandler } from './base.js';
|
|
3
|
+
class TestHandler extends BaseToolHandler {
|
|
4
|
+
testNormalize(data) {
|
|
5
|
+
return this.normalizeResponseData(data);
|
|
6
|
+
}
|
|
7
|
+
testSuccess(data, message) {
|
|
8
|
+
return this.createSuccess(data, message);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
describe('BaseToolHandler normalization', () => {
|
|
12
|
+
const handler = new TestHandler();
|
|
13
|
+
it('normalizes created_object_id string to number', () => {
|
|
14
|
+
const input = { created_object_id: '1330' };
|
|
15
|
+
const output = handler.testNormalize(input);
|
|
16
|
+
expect(output).toEqual({ created_object_id: 1330 });
|
|
17
|
+
});
|
|
18
|
+
it('leaves created_object_id untouched if already a number', () => {
|
|
19
|
+
const input = { created_object_id: 1330 };
|
|
20
|
+
const output = handler.testNormalize(input);
|
|
21
|
+
expect(output).toEqual({ created_object_id: 1330 });
|
|
22
|
+
});
|
|
23
|
+
it('normalizes string id to number', () => {
|
|
24
|
+
const input = { id: '46', name: 'Arroz redondo' };
|
|
25
|
+
const output = handler.testNormalize(input);
|
|
26
|
+
expect(output).toEqual({ id: 46, name: 'Arroz redondo' });
|
|
27
|
+
});
|
|
28
|
+
it('normalizes nested objects and arrays', () => {
|
|
29
|
+
const input = {
|
|
30
|
+
list: { id: '1', name: 'Groceries' },
|
|
31
|
+
items: [
|
|
32
|
+
{ id: '10', product: { id: '46', name: 'Rice' } },
|
|
33
|
+
{ id: '11', product: { id: '55', name: 'Beans' } },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
const output = handler.testNormalize(input);
|
|
37
|
+
expect(output).toEqual({
|
|
38
|
+
list: { id: 1, name: 'Groceries' },
|
|
39
|
+
items: [
|
|
40
|
+
{ id: 10, product: { id: 46, name: 'Rice' } },
|
|
41
|
+
{ id: 11, product: { id: 55, name: 'Beans' } },
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
it('does not touch barcodes, dates, or non-id fields', () => {
|
|
46
|
+
const input = {
|
|
47
|
+
id: '1',
|
|
48
|
+
barcode: '012345678905',
|
|
49
|
+
created_at: '2026-09-11 12:00:00',
|
|
50
|
+
note: '46',
|
|
51
|
+
};
|
|
52
|
+
const output = handler.testNormalize(input);
|
|
53
|
+
expect(output).toEqual({
|
|
54
|
+
id: 1,
|
|
55
|
+
barcode: '012345678905',
|
|
56
|
+
created_at: '2026-09-11 12:00:00',
|
|
57
|
+
note: '46',
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
it('createSuccess automatically applies normalization to structuredContent and textContent', () => {
|
|
61
|
+
const res = handler.testSuccess({ created_object_id: '1330' }, 'Created');
|
|
62
|
+
expect(res.structuredContent?.data).toEqual({ created_object_id: 1330 });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -121,7 +121,7 @@ export const recipeToolDefinitions = [
|
|
|
121
121
|
// ==================== MEAL PLANNING ====================
|
|
122
122
|
{
|
|
123
123
|
name: 'recipes_mealplan_get',
|
|
124
|
-
description: '[RECIPES/MEALPLAN] Get your meal plan data from Grocy instance with corresponding recipe details. Returns planned
|
|
124
|
+
description: '[RECIPES/MEALPLAN] Get your meal plan data from Grocy instance with corresponding recipe details. Returns planned entries for the requested date plus surrounding days for context. Each entry has a type: "recipe" (recipe_id + recipe_servings; names in recipes) or "note" (free text in note, no recipe); other values (e.g., "product") are not handled by these tools. section_id maps to the returned sections. Use this to find out what is planned for a specific date (e.g., "what\'s for dinner tomorrow", "recipes for today", "meal plan for next week"). The returned id field (meal plan entry ID) can be used with recipes_mealplan_delete_entry.',
|
|
125
125
|
annotations: { readOnlyHint: true },
|
|
126
126
|
inputSchema: {
|
|
127
127
|
type: 'object',
|
|
@@ -140,7 +140,7 @@ export const recipeToolDefinitions = [
|
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
name: 'recipes_mealplan_get_sections',
|
|
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; the row with id -1 and name null is Grocy\'s built-in "no section".',
|
|
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 or recipes_mealplan_add_note to pick sectionId; the row with id -1 and name null is Grocy\'s built-in "no section".',
|
|
144
144
|
annotations: { readOnlyHint: true },
|
|
145
145
|
inputSchema: {
|
|
146
146
|
type: 'object',
|
|
@@ -150,7 +150,7 @@ export const recipeToolDefinitions = [
|
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
name: 'recipes_mealplan_add_recipe',
|
|
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).',
|
|
153
|
+
description: '[RECIPES/MEALPLAN] **Write:** put a recipe on the calendar (date + section + servings). For free text without a recipe use recipes_mealplan_add_note. Not for listing sections (recipes_mealplan_get_sections) or viewing the plan (recipes_mealplan_get).',
|
|
154
154
|
inputSchema: {
|
|
155
155
|
type: 'object',
|
|
156
156
|
properties: {
|
|
@@ -174,15 +174,37 @@ export const recipeToolDefinitions = [
|
|
|
174
174
|
required: ['recipeId', 'day', 'servings', 'sectionId'],
|
|
175
175
|
},
|
|
176
176
|
},
|
|
177
|
+
{
|
|
178
|
+
name: 'recipes_mealplan_add_note',
|
|
179
|
+
description: '[RECIPES/MEALPLAN] **Write:** add a free-text note (e.g., "Leftovers", "Eating out") to a meal plan day and section—no recipe involved, nothing is consumed from stock. To plan an actual recipe use recipes_mealplan_add_recipe.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
day: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'Day to add the note to in YYYY-MM-DD format (e.g., "2024-12-25").',
|
|
186
|
+
},
|
|
187
|
+
note: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Text shown on the meal plan for that day (e.g., "Leftovers from Sunday", "Dinner at grandma\'s"). Must not be empty.',
|
|
190
|
+
},
|
|
191
|
+
sectionId: {
|
|
192
|
+
type: 'number',
|
|
193
|
+
description: 'ID of the meal plan section (e.g., breakfast, lunch, dinner) to file the note under. Use recipes_mealplan_get_sections tool to discover available sections and their IDs. Use -1 for Grocy\'s built-in "no section"; if the user names no meal or section, use -1 rather than guessing.',
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
required: ['day', 'note', 'sectionId'],
|
|
197
|
+
},
|
|
198
|
+
},
|
|
177
199
|
{
|
|
178
200
|
name: 'recipes_mealplan_delete_entry',
|
|
179
|
-
description: '[RECIPES/MEALPLAN] Delete
|
|
201
|
+
description: '[RECIPES/MEALPLAN] Delete one meal plan entry of any type (recipe, note, product) by its mealPlanEntryId. Use recipes_mealplan_get to find the mealPlanEntryId of the entry you want to remove.',
|
|
180
202
|
inputSchema: {
|
|
181
203
|
type: 'object',
|
|
182
204
|
properties: {
|
|
183
205
|
mealPlanEntryId: {
|
|
184
206
|
type: 'number',
|
|
185
|
-
description: 'ID of the specific meal plan entry to delete. Use recipes_mealplan_get to find the correct entry ID.',
|
|
207
|
+
description: 'ID of the specific meal plan entry to delete (works for any entry type). Use recipes_mealplan_get to find the correct entry ID.',
|
|
186
208
|
},
|
|
187
209
|
},
|
|
188
210
|
required: ['mealPlanEntryId'],
|
|
@@ -259,7 +281,7 @@ export const recipeToolDefinitions = [
|
|
|
259
281
|
: {
|
|
260
282
|
mealPlanEntryId: {
|
|
261
283
|
type: 'number',
|
|
262
|
-
description: `ID of the meal plan entry. Must be a recipe entry; note and product entries cannot be completed with this tool.${allowAlreadyDone ? '' : ' Note: This will fail if the meal plan entry is already marked as done (done=1).'}`,
|
|
284
|
+
description: `ID of the meal plan entry. Must be a recipe entry (type 'recipe' in recipes_mealplan_get); note and product entries cannot be completed with this tool.${allowAlreadyDone ? '' : ' Note: This will fail if the meal plan entry is already marked as done (done=1).'}`,
|
|
263
285
|
},
|
|
264
286
|
}),
|
|
265
287
|
stockAmounts: {
|
|
@@ -121,6 +121,20 @@ export class RecipeToolHandlers extends BaseToolHandler {
|
|
|
121
121
|
return this.createSuccess(result, 'Recipe added to meal plan successfully');
|
|
122
122
|
});
|
|
123
123
|
};
|
|
124
|
+
/**
|
|
125
|
+
* Add free-text note to meal plan
|
|
126
|
+
*/
|
|
127
|
+
addNoteToMealPlan = async (args) => {
|
|
128
|
+
return this.executeToolHandler(async () => {
|
|
129
|
+
const { day, note, sectionId } = args || {};
|
|
130
|
+
this.validateRequired({ day, note, sectionId }, ['day', 'note', 'sectionId']);
|
|
131
|
+
if (typeof note !== 'string' || note.trim() === '') {
|
|
132
|
+
throw new ValidationError('note must be non-empty text', 'recipes_mealplan_add_note');
|
|
133
|
+
}
|
|
134
|
+
const result = await this.createMealPlanEntry(day, sectionId, { type: 'note', note }, 'recipes_mealplan_add_note');
|
|
135
|
+
return this.createSuccess(result, 'Note added to meal plan successfully');
|
|
136
|
+
});
|
|
137
|
+
};
|
|
124
138
|
/**
|
|
125
139
|
* Cook recipe - consume ingredients from stock
|
|
126
140
|
*/
|
|
@@ -241,6 +255,7 @@ export class RecipeToolHandlers extends BaseToolHandler {
|
|
|
241
255
|
simplifiedMealPlanByDate[entryDate].push({
|
|
242
256
|
id: entry.id,
|
|
243
257
|
day: entry.day,
|
|
258
|
+
type: entry.type,
|
|
244
259
|
section_id: entry.section_id,
|
|
245
260
|
recipe_id: entry.recipe_id,
|
|
246
261
|
recipe_servings: entry.recipe_servings,
|
|
@@ -275,14 +290,14 @@ export class RecipeToolHandlers extends BaseToolHandler {
|
|
|
275
290
|
});
|
|
276
291
|
};
|
|
277
292
|
/**
|
|
278
|
-
* Delete
|
|
293
|
+
* Delete a meal plan entry (any type)
|
|
279
294
|
*/
|
|
280
|
-
|
|
295
|
+
deleteMealPlanEntry = async (args) => {
|
|
281
296
|
return this.executeToolHandler(async () => {
|
|
282
297
|
const { mealPlanEntryId } = args || {};
|
|
283
298
|
this.validateRequired({ mealPlanEntryId }, ['mealPlanEntryId']);
|
|
284
299
|
const result = await this.apiCall(`/objects/meal_plan/${mealPlanEntryId}`, 'DELETE');
|
|
285
|
-
return this.createSuccess(result, '
|
|
300
|
+
return this.createSuccess(result, 'Meal plan entry deleted successfully');
|
|
286
301
|
});
|
|
287
302
|
};
|
|
288
303
|
// ==================== RECIPE CREATION ====================
|
|
@@ -18,7 +18,8 @@ export const recipeModule = {
|
|
|
18
18
|
recipes_mealplan_get: handlers.getMealPlan,
|
|
19
19
|
recipes_mealplan_get_sections: handlers.getMealPlanSections,
|
|
20
20
|
recipes_mealplan_add_recipe: handlers.addRecipeToMealPlan,
|
|
21
|
-
|
|
21
|
+
recipes_mealplan_add_note: handlers.addNoteToMealPlan,
|
|
22
|
+
recipes_mealplan_delete_entry: handlers.deleteMealPlanEntry,
|
|
22
23
|
// Recipe Cooking
|
|
23
24
|
recipes_cooking_consume: handlers.consumeRecipe,
|
|
24
25
|
recipes_cooking_complete: handlers.cookedSomething,
|
package/build/version.js
CHANGED