mcp-grocy 2.7.0 → 2.7.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.
- package/CHANGELOG.md +14 -0
- package/README.md +15 -1
- package/build/api/client.js +119 -0
- package/build/config/index.js +283 -0
- package/build/main.js +44 -0
- package/build/resources/CHANGELOG.md +15 -3
- package/build/resources/README.md +15 -1
- package/build/resources/api-reference.md +2 -1
- package/build/resources/response-format.md +1 -3
- package/build/server/http-server.js +398 -0
- package/build/server/mcp-server.js +230 -0
- package/build/server/resources.js +73 -0
- package/build/server/tool-input-zod.js +86 -0
- package/build/tools/base.js +157 -0
- package/build/tools/household/definitions.js +161 -0
- package/build/tools/household/handlers.js +109 -0
- package/build/tools/household/index.js +25 -0
- package/build/tools/index.js +2 -0
- package/build/tools/inventory/definitions.js +416 -0
- package/build/tools/inventory/handlers.js +443 -0
- package/build/tools/inventory/index.js +32 -0
- package/build/tools/module-loader.js +154 -0
- package/build/tools/recipes/definitions.js +278 -0
- package/build/tools/recipes/handlers.js +518 -0
- package/build/tools/recipes/index.js +33 -0
- package/build/tools/recipes/validations.js +29 -0
- package/build/tools/shopping/definitions.js +147 -0
- package/build/tools/shopping/handlers.js +198 -0
- package/build/tools/shopping/index.js +17 -0
- package/build/tools/system/definitions.js +89 -0
- package/build/tools/system/handlers.js +156 -0
- package/build/tools/system/index.js +16 -0
- package/build/tools/types.js +1 -0
- package/build/tools/validation-helpers.js +39 -0
- package/build/types/index.js +64 -0
- package/build/utils/errors.js +143 -0
- package/build/utils/logger.js +142 -0
- package/build/version.js +1 -1
- package/package.json +27 -25
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import { BaseToolHandler } from '../base.js';
|
|
2
|
+
import { ValidationError } from '../../utils/errors.js';
|
|
3
|
+
import Fuse from 'fuse.js';
|
|
4
|
+
export class InventoryToolHandlers extends BaseToolHandler {
|
|
5
|
+
// ==================== PRODUCT MANAGEMENT ====================
|
|
6
|
+
getProducts = async (args) => {
|
|
7
|
+
return this.executeToolHandler(async () => {
|
|
8
|
+
const { fields } = args || {};
|
|
9
|
+
// Validate required parameters
|
|
10
|
+
this.validateRequired({ fields }, ['fields']);
|
|
11
|
+
const fieldList = this.parseArrayParam(fields, 'fields');
|
|
12
|
+
// Fetch products
|
|
13
|
+
const products = await this.apiCall('/objects/products');
|
|
14
|
+
if (!Array.isArray(products)) {
|
|
15
|
+
return this.createSuccess([]);
|
|
16
|
+
}
|
|
17
|
+
// Filter to only include requested fields
|
|
18
|
+
const filteredProducts = this.filterFields(products, fieldList);
|
|
19
|
+
return this.createSuccess(filteredProducts);
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
getProductGroups = async () => {
|
|
23
|
+
return this.executeToolHandler(async () => {
|
|
24
|
+
const data = await this.apiCall('/objects/product_groups');
|
|
25
|
+
return this.createSuccess(data);
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
getPriceHistory = async (args) => {
|
|
29
|
+
return this.executeToolHandler(async () => {
|
|
30
|
+
const { productId } = args || {};
|
|
31
|
+
this.validateRequired({ productId }, ['productId']);
|
|
32
|
+
const data = await this.apiCall(`/stock/products/${productId}/price-history`);
|
|
33
|
+
return this.createSuccess(data);
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
// ==================== STOCK QUERIES ====================
|
|
37
|
+
getAllStock = async () => {
|
|
38
|
+
return this.executeToolHandler(async () => {
|
|
39
|
+
const data = await this.apiCall('/stock');
|
|
40
|
+
return this.createSuccess(data);
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
getStockByProduct = async (args) => {
|
|
44
|
+
return this.executeToolHandler(async () => {
|
|
45
|
+
const { productId } = args || {};
|
|
46
|
+
this.validateRequired({ productId }, ['productId']);
|
|
47
|
+
const stockEntries = await this.apiCall(`/stock/products/${productId}/entries`);
|
|
48
|
+
// Define essential fields for stock entries
|
|
49
|
+
const entryFields = [
|
|
50
|
+
'id',
|
|
51
|
+
'amount',
|
|
52
|
+
'best_before_date',
|
|
53
|
+
'purchased_date',
|
|
54
|
+
'stock_id',
|
|
55
|
+
'note',
|
|
56
|
+
'location_id',
|
|
57
|
+
];
|
|
58
|
+
// Filter entries to only include essential fields
|
|
59
|
+
const filteredEntries = Array.isArray(stockEntries)
|
|
60
|
+
? this.filterFields(stockEntries, entryFields)
|
|
61
|
+
: [];
|
|
62
|
+
return this.createSuccess(filteredEntries);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
getStockVolatile = async (args) => {
|
|
66
|
+
return this.executeToolHandler(async () => {
|
|
67
|
+
const queryParams = args?.includeDetails ? { include_details: 'true' } : {};
|
|
68
|
+
const data = await this.apiCall('/stock/volatile', 'GET', undefined, { queryParams });
|
|
69
|
+
return this.createSuccess(data);
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
getStockByLocation = async (args) => {
|
|
73
|
+
return this.executeToolHandler(async () => {
|
|
74
|
+
const { locationId } = args || {};
|
|
75
|
+
this.validateRequired({ locationId }, ['locationId']);
|
|
76
|
+
const data = await this.apiCall(`stock`, 'GET', undefined, {
|
|
77
|
+
queryParams: { 'query[]': `location_id=${locationId}` },
|
|
78
|
+
});
|
|
79
|
+
return this.createSuccess(data);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
// ==================== STOCK TRANSACTIONS ====================
|
|
83
|
+
purchaseProduct = async (args) => {
|
|
84
|
+
return this.executeToolHandler(async () => {
|
|
85
|
+
const { productId, amount, bestBeforeDate, price, locationId, note } = args || {};
|
|
86
|
+
this.validateRequired({ productId, amount }, ['productId', 'amount']);
|
|
87
|
+
const body = { amount };
|
|
88
|
+
if (bestBeforeDate)
|
|
89
|
+
body.best_before_date = bestBeforeDate;
|
|
90
|
+
if (price !== undefined)
|
|
91
|
+
body.price = price;
|
|
92
|
+
if (locationId)
|
|
93
|
+
body.location_id = locationId;
|
|
94
|
+
if (note)
|
|
95
|
+
body.note = note;
|
|
96
|
+
const result = await this.apiCall(`/stock/products/${productId}/add`, 'POST', body);
|
|
97
|
+
return this.createSuccess(result, 'Product purchased successfully');
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
consumeProduct = async (args) => {
|
|
101
|
+
return this.executeToolHandler(async () => {
|
|
102
|
+
const { productId, amount, spoiled = false, locationId, note } = args || {};
|
|
103
|
+
this.validateRequired({ productId, amount }, ['productId', 'amount']);
|
|
104
|
+
const body = {
|
|
105
|
+
amount,
|
|
106
|
+
transaction_type: spoiled ? 'inventory-correction' : 'consume',
|
|
107
|
+
};
|
|
108
|
+
if (locationId)
|
|
109
|
+
body.location_id = locationId;
|
|
110
|
+
if (note)
|
|
111
|
+
body.note = note;
|
|
112
|
+
if (spoiled)
|
|
113
|
+
body.spoiled = true;
|
|
114
|
+
const result = await this.apiCall(`/stock/products/${productId}/consume`, 'POST', body);
|
|
115
|
+
return this.createSuccess(result, 'Product consumed successfully');
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
transferProduct = async (args) => {
|
|
119
|
+
return this.executeToolHandler(async () => {
|
|
120
|
+
const { productId, amount, fromLocationId, toLocationId, note } = args || {};
|
|
121
|
+
this.validateRequired({ productId, amount, fromLocationId, toLocationId }, [
|
|
122
|
+
'productId',
|
|
123
|
+
'amount',
|
|
124
|
+
'fromLocationId',
|
|
125
|
+
'toLocationId',
|
|
126
|
+
]);
|
|
127
|
+
const body = {
|
|
128
|
+
amount,
|
|
129
|
+
location_id_from: fromLocationId,
|
|
130
|
+
location_id_to: toLocationId,
|
|
131
|
+
};
|
|
132
|
+
if (note)
|
|
133
|
+
body.note = note;
|
|
134
|
+
const result = await this.apiCall(`/stock/products/${productId}/transfer`, 'POST', body);
|
|
135
|
+
return this.createSuccess(result, 'Product transferred successfully');
|
|
136
|
+
});
|
|
137
|
+
};
|
|
138
|
+
inventoryProduct = async (args) => {
|
|
139
|
+
return this.executeToolHandler(async () => {
|
|
140
|
+
const { productId, newAmount, bestBeforeDate, locationId, note } = args || {};
|
|
141
|
+
this.validateRequired({ productId, newAmount }, ['productId', 'newAmount']);
|
|
142
|
+
const body = { new_amount: newAmount };
|
|
143
|
+
if (bestBeforeDate)
|
|
144
|
+
body.best_before_date = bestBeforeDate;
|
|
145
|
+
if (locationId)
|
|
146
|
+
body.location_id = locationId;
|
|
147
|
+
if (note)
|
|
148
|
+
body.note = note;
|
|
149
|
+
const result = await this.apiCall(`/stock/products/${productId}/inventory`, 'POST', body);
|
|
150
|
+
return this.createSuccess(result, 'Product inventory updated successfully');
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
openProduct = async (args) => {
|
|
154
|
+
return this.executeToolHandler(async () => {
|
|
155
|
+
const { productId, amount = 1, note } = args || {};
|
|
156
|
+
this.validateRequired({ productId }, ['productId']);
|
|
157
|
+
const body = { amount };
|
|
158
|
+
if (note)
|
|
159
|
+
body.note = note;
|
|
160
|
+
const result = await this.apiCall(`/stock/products/${productId}/open`, 'POST', body);
|
|
161
|
+
return this.createSuccess(result, 'Product opened successfully');
|
|
162
|
+
});
|
|
163
|
+
};
|
|
164
|
+
lookupProduct = async (args) => {
|
|
165
|
+
return this.executeToolHandler(async () => {
|
|
166
|
+
const { productName } = args || {};
|
|
167
|
+
this.validateRequired({ productName }, ['productName']);
|
|
168
|
+
const [productsResponse, locationsResponse, quantityUnitsResponse] = await Promise.all([
|
|
169
|
+
this.apiCall('/objects/products'),
|
|
170
|
+
this.apiCall('/objects/locations'),
|
|
171
|
+
this.apiCall('/objects/quantity_units'),
|
|
172
|
+
]);
|
|
173
|
+
const products = Array.isArray(productsResponse) ? productsResponse : [];
|
|
174
|
+
const locations = Array.isArray(locationsResponse) ? locationsResponse : [];
|
|
175
|
+
const quantityUnits = Array.isArray(quantityUnitsResponse) ? quantityUnitsResponse : [];
|
|
176
|
+
const fuseOptions = {
|
|
177
|
+
keys: [
|
|
178
|
+
{ name: 'name', weight: 1.0 },
|
|
179
|
+
{ name: 'description', weight: 0.3 },
|
|
180
|
+
],
|
|
181
|
+
threshold: 0.6,
|
|
182
|
+
distance: 100,
|
|
183
|
+
minMatchCharLength: 1,
|
|
184
|
+
ignoreLocation: true,
|
|
185
|
+
includeScore: true,
|
|
186
|
+
includeMatches: true,
|
|
187
|
+
useExtendedSearch: false,
|
|
188
|
+
isCaseSensitive: false,
|
|
189
|
+
shouldSort: true,
|
|
190
|
+
findAllMatches: false,
|
|
191
|
+
};
|
|
192
|
+
const fuse = new Fuse(products, fuseOptions);
|
|
193
|
+
const fuseResults = fuse.search(productName);
|
|
194
|
+
let productMatches = fuseResults.map((result) => ({
|
|
195
|
+
...result.item,
|
|
196
|
+
matchScore: Math.round((1 - result.score) * 100),
|
|
197
|
+
fuseScore: result.score,
|
|
198
|
+
matches: result.matches,
|
|
199
|
+
}));
|
|
200
|
+
if (productMatches.length === 0) {
|
|
201
|
+
const permissiveFuse = new Fuse(products, {
|
|
202
|
+
...fuseOptions,
|
|
203
|
+
threshold: 0.8,
|
|
204
|
+
distance: 200,
|
|
205
|
+
});
|
|
206
|
+
const permissiveResults = permissiveFuse.search(productName);
|
|
207
|
+
productMatches = permissiveResults.map((result) => ({
|
|
208
|
+
...result.item,
|
|
209
|
+
matchScore: Math.round((1 - result.score) * 100),
|
|
210
|
+
fuseScore: result.score,
|
|
211
|
+
matches: result.matches,
|
|
212
|
+
isPermissiveMatch: true,
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
productMatches = productMatches.slice(0, 5);
|
|
216
|
+
if (productMatches.length === 0) {
|
|
217
|
+
return this.createError(`No products found matching "${productName}"`, {
|
|
218
|
+
suggestion: 'Try a different product name or check the spelling',
|
|
219
|
+
availableProducts: products.slice(0, 10).map((p) => p.name),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const enrichedMatches = await Promise.all(productMatches.map(async (product) => {
|
|
223
|
+
let productEntries;
|
|
224
|
+
try {
|
|
225
|
+
productEntries = await this.apiCall(`/stock/products/${product.id}/entries`);
|
|
226
|
+
productEntries = Array.isArray(productEntries) ? productEntries : [];
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
productEntries = [];
|
|
230
|
+
}
|
|
231
|
+
const stockEntries = productEntries.map((stockItem) => {
|
|
232
|
+
return {
|
|
233
|
+
amount: stockItem.amount,
|
|
234
|
+
bestBeforeDate: stockItem.best_before_date,
|
|
235
|
+
stockId: stockItem.id,
|
|
236
|
+
locationId: parseInt(stockItem.location_id),
|
|
237
|
+
};
|
|
238
|
+
});
|
|
239
|
+
stockEntries.sort((a, b) => {
|
|
240
|
+
if (!a.bestBeforeDate && !b.bestBeforeDate)
|
|
241
|
+
return 0;
|
|
242
|
+
if (!a.bestBeforeDate)
|
|
243
|
+
return 1;
|
|
244
|
+
if (!b.bestBeforeDate)
|
|
245
|
+
return -1;
|
|
246
|
+
return new Date(a.bestBeforeDate).getTime() - new Date(b.bestBeforeDate).getTime();
|
|
247
|
+
});
|
|
248
|
+
const unit = quantityUnits.find((u) => u.id == product.qu_id_stock);
|
|
249
|
+
const unitInfo = unit ? { id: unit.id, name: unit.name } : { id: null, name: 'pieces' };
|
|
250
|
+
const hasMultipleLocations = stockEntries.length > 1 &&
|
|
251
|
+
new Set(stockEntries.map((entry) => entry.locationId)).size > 1;
|
|
252
|
+
const locationInstructions = hasMultipleLocations
|
|
253
|
+
? 'IMPORTANT: This product has stock in multiple locations. Make sure the user requested a specific location or confirm the locationId before performing any operations.'
|
|
254
|
+
: undefined;
|
|
255
|
+
return {
|
|
256
|
+
productId: product.id,
|
|
257
|
+
productName: product.name,
|
|
258
|
+
stockEntries: stockEntries,
|
|
259
|
+
totalStockAmount: productEntries.reduce((sum, s) => sum + parseFloat(s.amount || 0), 0),
|
|
260
|
+
unit: unitInfo,
|
|
261
|
+
locationInstructions,
|
|
262
|
+
};
|
|
263
|
+
}));
|
|
264
|
+
return this.createSuccess({
|
|
265
|
+
message: `Found ${enrichedMatches.length} product matches for "${productName}" (ordered from most likely to least likely match)`,
|
|
266
|
+
productMatches: enrichedMatches,
|
|
267
|
+
allAvailableLocations: locations.map((l) => ({ id: l.id, name: l.name })),
|
|
268
|
+
instructions: 'Review the matches above. Use the exact productId and locationId from this data for any product operations (inventory_stock_entry_consume, inventory_stock_entry_transfer, inventory_transactions_purchase, inventory_transactions_adjust, etc.).',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
printProductLabel = async (args) => {
|
|
273
|
+
return this.executeToolHandler(async () => {
|
|
274
|
+
const { productId } = args || {};
|
|
275
|
+
this.validateRequired({ productId }, ['productId']);
|
|
276
|
+
const result = await this.apiCall(`/stock/products/${productId}/printlabel`);
|
|
277
|
+
return this.createSuccess(result, 'Product label printed successfully');
|
|
278
|
+
});
|
|
279
|
+
};
|
|
280
|
+
printStockEntryLabel = async (args) => {
|
|
281
|
+
return this.executeToolHandler(async () => {
|
|
282
|
+
const { stockId, productId } = args || {};
|
|
283
|
+
this.validateRequired({ stockId, productId }, ['stockId', 'productId']);
|
|
284
|
+
const stockEntryResponse = await this.apiCall(`/stock/entry/${stockId}`);
|
|
285
|
+
if (!stockEntryResponse || !stockEntryResponse.product_id) {
|
|
286
|
+
throw new ValidationError(`Could not resolve product ID from stock entry ${stockId}`, 'printStockEntryLabel');
|
|
287
|
+
}
|
|
288
|
+
if (stockEntryResponse.product_id !== productId) {
|
|
289
|
+
throw new ValidationError(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.product_id}, but ${productId} was provided`, 'printStockEntryLabel');
|
|
290
|
+
}
|
|
291
|
+
const result = await this.apiCall(`/stock/entry/${stockId}/printlabel`);
|
|
292
|
+
return this.createSuccess(result, 'Stock entry label printed successfully');
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
// ==================== GRANULAR STOCK ENTRY OPERATIONS ====================
|
|
296
|
+
async splitStockEntry(originalEntry, stockAmounts, getUnitForm) {
|
|
297
|
+
const splitEntries = [];
|
|
298
|
+
if (stockAmounts.length === 1) {
|
|
299
|
+
const amount = stockAmounts[0];
|
|
300
|
+
if (typeof amount !== 'number' || amount <= 0) {
|
|
301
|
+
throw new ValidationError(`Invalid amount: ${amount}`, 'splitStockEntry');
|
|
302
|
+
}
|
|
303
|
+
const note = `${originalEntry.note || ''} - ${originalEntry.id} - 1`;
|
|
304
|
+
await this.apiCall(`/stock/entry/${originalEntry.id}`, 'PUT', {
|
|
305
|
+
amount: amount,
|
|
306
|
+
open: false,
|
|
307
|
+
note: note,
|
|
308
|
+
best_before_date: originalEntry.best_before_date,
|
|
309
|
+
purchased_date: originalEntry.purchased_date,
|
|
310
|
+
location_id: originalEntry.location_id,
|
|
311
|
+
});
|
|
312
|
+
splitEntries.push({
|
|
313
|
+
stockId: originalEntry.id,
|
|
314
|
+
amount: amount,
|
|
315
|
+
type: 'updated',
|
|
316
|
+
unit: getUnitForm(amount),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
for (let i = 0; i < stockAmounts.length; i++) {
|
|
321
|
+
const amount = stockAmounts[i];
|
|
322
|
+
if (typeof amount !== 'number' || amount <= 0) {
|
|
323
|
+
throw new ValidationError(`Invalid amount at index ${i}: ${amount}`, 'splitStockEntry');
|
|
324
|
+
}
|
|
325
|
+
const note = `${originalEntry.note || ''} - ${originalEntry.id} - ${i + 1}`;
|
|
326
|
+
if (i === 0) {
|
|
327
|
+
await this.apiCall(`/stock/entry/${originalEntry.id}`, 'PUT', {
|
|
328
|
+
amount: amount,
|
|
329
|
+
open: false,
|
|
330
|
+
note: note,
|
|
331
|
+
best_before_date: originalEntry.best_before_date,
|
|
332
|
+
purchased_date: originalEntry.purchased_date,
|
|
333
|
+
location_id: originalEntry.location_id,
|
|
334
|
+
});
|
|
335
|
+
splitEntries.push({
|
|
336
|
+
stockId: originalEntry.id,
|
|
337
|
+
amount,
|
|
338
|
+
type: 'updated',
|
|
339
|
+
unit: getUnitForm(amount),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const createResponse = await this.apiCall(`/stock/products/${originalEntry.product_id}/add`, 'POST', {
|
|
344
|
+
amount,
|
|
345
|
+
best_before_date: originalEntry.best_before_date,
|
|
346
|
+
purchased_date: originalEntry.purchased_date,
|
|
347
|
+
transaction_type: 'purchase',
|
|
348
|
+
location_id: originalEntry.location_id,
|
|
349
|
+
note: note,
|
|
350
|
+
});
|
|
351
|
+
const stockId = createResponse[0].stock_id || createResponse[0].id;
|
|
352
|
+
const stockResponse = await this.apiCall('/objects/stock');
|
|
353
|
+
const stockEntries = Array.isArray(stockResponse) ? stockResponse : [];
|
|
354
|
+
const actualStockEntry = stockEntries.find((entry) => entry.product_id === originalEntry.product_id && entry.stock_id === stockId);
|
|
355
|
+
if (!actualStockEntry) {
|
|
356
|
+
throw new ValidationError(`Could not find created stock entry with product_id ${originalEntry.product_id} and stock_id ${stockId}`, 'splitStockEntry');
|
|
357
|
+
}
|
|
358
|
+
splitEntries.push({
|
|
359
|
+
stockId: actualStockEntry.id,
|
|
360
|
+
amount,
|
|
361
|
+
type: 'created',
|
|
362
|
+
unit: getUnitForm(amount),
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return splitEntries;
|
|
368
|
+
}
|
|
369
|
+
consumeStockEntry = async (args) => {
|
|
370
|
+
return this.executeToolHandler(async () => {
|
|
371
|
+
const { stockId, productId, amount, spoiled = false, note } = args || {};
|
|
372
|
+
this.validateRequired({ stockId, productId, amount }, ['stockId', 'productId', 'amount']);
|
|
373
|
+
const stockEntryResponse = await this.apiCall(`/stock/entry/${stockId}`);
|
|
374
|
+
if (!stockEntryResponse || !stockEntryResponse.product_id) {
|
|
375
|
+
throw new ValidationError(`Could not resolve product ID from stock entry ${stockId}`, 'consumeStockEntry');
|
|
376
|
+
}
|
|
377
|
+
if (stockEntryResponse.product_id !== productId) {
|
|
378
|
+
throw new ValidationError(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.product_id}, but ${productId} was provided`, 'consumeStockEntry');
|
|
379
|
+
}
|
|
380
|
+
const body = {
|
|
381
|
+
amount,
|
|
382
|
+
spoiled,
|
|
383
|
+
stock_entry_id: stockEntryResponse.stock_id,
|
|
384
|
+
location_id: stockEntryResponse.location_id,
|
|
385
|
+
};
|
|
386
|
+
if (note)
|
|
387
|
+
body.note = note;
|
|
388
|
+
const result = await this.apiCall(`/stock/products/${stockEntryResponse.product_id}/consume`, 'POST', body);
|
|
389
|
+
return this.createSuccess(result, 'Stock entry consumed successfully');
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
transferStockEntry = async (args) => {
|
|
393
|
+
return this.executeToolHandler(async () => {
|
|
394
|
+
const { stockId, productId, amount, locationIdTo, note } = args || {};
|
|
395
|
+
this.validateRequired({ stockId, productId, amount, locationIdTo }, [
|
|
396
|
+
'stockId',
|
|
397
|
+
'productId',
|
|
398
|
+
'amount',
|
|
399
|
+
'locationIdTo',
|
|
400
|
+
]);
|
|
401
|
+
const stockEntryResponse = await this.apiCall(`/stock/entry/${stockId}`);
|
|
402
|
+
if (!stockEntryResponse || !stockEntryResponse.product_id) {
|
|
403
|
+
throw new ValidationError(`Could not resolve product ID from stock entry ${stockId}`, 'transferStockEntry');
|
|
404
|
+
}
|
|
405
|
+
if (stockEntryResponse.product_id !== productId) {
|
|
406
|
+
throw new ValidationError(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.product_id}, but ${productId} was provided`, 'transferStockEntry');
|
|
407
|
+
}
|
|
408
|
+
const body = {
|
|
409
|
+
amount,
|
|
410
|
+
location_id_from: stockEntryResponse.location_id,
|
|
411
|
+
location_id_to: locationIdTo,
|
|
412
|
+
transaction_type: 'transfer',
|
|
413
|
+
stock_entry_id: stockEntryResponse.stock_id,
|
|
414
|
+
};
|
|
415
|
+
if (note)
|
|
416
|
+
body.note = note;
|
|
417
|
+
const result = await this.apiCall(`/stock/products/${stockEntryResponse.product_id}/transfer`, 'POST', body);
|
|
418
|
+
return this.createSuccess(result, 'Stock entry transferred successfully');
|
|
419
|
+
});
|
|
420
|
+
};
|
|
421
|
+
openStockEntry = async (args) => {
|
|
422
|
+
return this.executeToolHandler(async () => {
|
|
423
|
+
const { stockId, productId, amount, note } = args || {};
|
|
424
|
+
this.validateRequired({ stockId, productId, amount }, ['stockId', 'productId', 'amount']);
|
|
425
|
+
const stockEntryResponse = await this.apiCall(`/stock/entry/${stockId}`);
|
|
426
|
+
if (!stockEntryResponse || !stockEntryResponse.product_id) {
|
|
427
|
+
throw new ValidationError(`Could not resolve product ID from stock entry ${stockId}`, 'openStockEntry');
|
|
428
|
+
}
|
|
429
|
+
if (stockEntryResponse.product_id !== productId) {
|
|
430
|
+
throw new ValidationError(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.product_id}, but ${productId} was provided`, 'openStockEntry');
|
|
431
|
+
}
|
|
432
|
+
const body = {
|
|
433
|
+
amount,
|
|
434
|
+
stock_entry_id: stockEntryResponse.stock_id,
|
|
435
|
+
location_id: stockEntryResponse.location_id,
|
|
436
|
+
};
|
|
437
|
+
if (note)
|
|
438
|
+
body.note = note;
|
|
439
|
+
const result = await this.apiCall(`/stock/products/${stockEntryResponse.product_id}/open`, 'POST', body);
|
|
440
|
+
return this.createSuccess(result, 'Stock entry opened successfully');
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { inventoryToolDefinitions } from './definitions.js';
|
|
2
|
+
import { InventoryToolHandlers } from './handlers.js';
|
|
3
|
+
const handlers = new InventoryToolHandlers();
|
|
4
|
+
export const inventoryModule = {
|
|
5
|
+
definitions: inventoryToolDefinitions,
|
|
6
|
+
handlers: {
|
|
7
|
+
// Product Management
|
|
8
|
+
inventory_products_get: handlers.getProducts,
|
|
9
|
+
inventory_products_get_groups: handlers.getProductGroups,
|
|
10
|
+
inventory_products_get_price_history: handlers.getPriceHistory,
|
|
11
|
+
// Stock Queries
|
|
12
|
+
inventory_stock_get_all: handlers.getAllStock,
|
|
13
|
+
inventory_stock_get_by_product: handlers.getStockByProduct,
|
|
14
|
+
inventory_stock_get_volatile: handlers.getStockVolatile,
|
|
15
|
+
inventory_stock_get_by_location: handlers.getStockByLocation,
|
|
16
|
+
// Stock Transactions
|
|
17
|
+
inventory_transactions_purchase: handlers.purchaseProduct,
|
|
18
|
+
inventory_transactions_consume: handlers.consumeProduct,
|
|
19
|
+
inventory_transactions_transfer: handlers.transferProduct,
|
|
20
|
+
inventory_transactions_adjust: handlers.inventoryProduct,
|
|
21
|
+
inventory_transactions_open: handlers.openProduct,
|
|
22
|
+
// Product Lookup and Label Printing
|
|
23
|
+
inventory_products_lookup: handlers.lookupProduct,
|
|
24
|
+
inventory_products_print_label: handlers.printProductLabel,
|
|
25
|
+
inventory_stock_entry_print_label: handlers.printStockEntryLabel,
|
|
26
|
+
// Granular Stock Entry Operations
|
|
27
|
+
inventory_stock_entry_consume: handlers.consumeStockEntry,
|
|
28
|
+
inventory_stock_entry_transfer: handlers.transferStockEntry,
|
|
29
|
+
inventory_stock_entry_open: handlers.openStockEntry,
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export * from './definitions.js';
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified dynamic module loader
|
|
3
|
+
* Improved performance with better caching and lazy loading
|
|
4
|
+
*/
|
|
5
|
+
import { readdirSync } from 'fs';
|
|
6
|
+
import { dirname } from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { logger } from '../utils/logger.js';
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
/**
|
|
11
|
+
* Simplified module loader with performance optimizations
|
|
12
|
+
*/
|
|
13
|
+
export class ModuleLoader {
|
|
14
|
+
static moduleCache = new Map();
|
|
15
|
+
static discoveredFolders = null;
|
|
16
|
+
/**
|
|
17
|
+
* Discover tool folders (cached)
|
|
18
|
+
*/
|
|
19
|
+
static discoverFolders() {
|
|
20
|
+
if (this.discoveredFolders !== null) {
|
|
21
|
+
return this.discoveredFolders;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const toolsDir = __dirname;
|
|
25
|
+
this.discoveredFolders = readdirSync(toolsDir, { withFileTypes: true })
|
|
26
|
+
.filter((dirent) => dirent.isDirectory())
|
|
27
|
+
.filter((dirent) => !dirent.name.startsWith('.') && dirent.name !== 'node_modules')
|
|
28
|
+
.map((dirent) => dirent.name);
|
|
29
|
+
logger.module(`Discovered ${this.discoveredFolders.length} tool folders`);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
logger.error('Failed to discover tool folders', 'MODULE', { error });
|
|
33
|
+
this.discoveredFolders = [];
|
|
34
|
+
}
|
|
35
|
+
return this.discoveredFolders;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Load all tool modules with lazy loading
|
|
39
|
+
*/
|
|
40
|
+
static async loadAllModules() {
|
|
41
|
+
const folders = this.discoverFolders();
|
|
42
|
+
const toolModules = [];
|
|
43
|
+
// Load modules in parallel for better performance
|
|
44
|
+
const loadPromises = folders.map((folder) => this.loadModule(folder));
|
|
45
|
+
const results = await Promise.allSettled(loadPromises);
|
|
46
|
+
results.forEach((result, index) => {
|
|
47
|
+
const folder = folders[index];
|
|
48
|
+
if (result.status === 'fulfilled' && result.value?.toolModule) {
|
|
49
|
+
toolModules.push(result.value.toolModule);
|
|
50
|
+
logger.module(`Loaded module: ${folder}`);
|
|
51
|
+
}
|
|
52
|
+
else if (result.status === 'rejected') {
|
|
53
|
+
logger.debug(`Failed to load module ${folder}`, 'MODULE', {
|
|
54
|
+
error: result.reason,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
logger.tools(`Loaded ${toolModules.length} tool modules`);
|
|
59
|
+
return { toolModules };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Load a specific module with caching
|
|
63
|
+
*/
|
|
64
|
+
static async loadModule(folderName) {
|
|
65
|
+
// Check cache first
|
|
66
|
+
const cached = this.moduleCache.get(folderName);
|
|
67
|
+
if (cached) {
|
|
68
|
+
return cached.loaded ? cached : null;
|
|
69
|
+
}
|
|
70
|
+
const moduleInfo = { loaded: false };
|
|
71
|
+
try {
|
|
72
|
+
// Use .js extension for production builds
|
|
73
|
+
const extension = '.js';
|
|
74
|
+
const indexPath = `./${folderName}/index${extension}`;
|
|
75
|
+
const moduleIndex = await import(indexPath);
|
|
76
|
+
// Find tool module export
|
|
77
|
+
for (const [, exportValue] of Object.entries(moduleIndex)) {
|
|
78
|
+
if (this.isToolModule(exportValue)) {
|
|
79
|
+
moduleInfo.toolModule = exportValue;
|
|
80
|
+
moduleInfo.loaded = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!moduleInfo.loaded) {
|
|
85
|
+
logger.debug(`No tool module found in ${folderName}`, 'MODULE');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
90
|
+
moduleInfo.error = errorMessage;
|
|
91
|
+
logger.debug(`Failed to load module ${folderName}: ${errorMessage}`, 'MODULE');
|
|
92
|
+
}
|
|
93
|
+
this.moduleCache.set(folderName, moduleInfo);
|
|
94
|
+
return moduleInfo.loaded ? moduleInfo : null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Check if an export looks like a ToolModule
|
|
98
|
+
*/
|
|
99
|
+
static isToolModule(obj) {
|
|
100
|
+
if (obj === null || typeof obj !== 'object') {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
const m = obj;
|
|
104
|
+
return (Array.isArray(m.definitions) &&
|
|
105
|
+
m.definitions.length > 0 &&
|
|
106
|
+
typeof m.handlers === 'object' &&
|
|
107
|
+
m.handlers !== null);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get module from cache
|
|
111
|
+
*/
|
|
112
|
+
static getCachedModule(moduleName) {
|
|
113
|
+
const cached = this.moduleCache.get(moduleName);
|
|
114
|
+
return cached?.toolModule;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get cache statistics
|
|
118
|
+
*/
|
|
119
|
+
static getCacheStats() {
|
|
120
|
+
let loaded = 0;
|
|
121
|
+
let errors = 0;
|
|
122
|
+
for (const module of this.moduleCache.values()) {
|
|
123
|
+
if (module.loaded)
|
|
124
|
+
loaded++;
|
|
125
|
+
if (module.error)
|
|
126
|
+
errors++;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
total: this.moduleCache.size,
|
|
130
|
+
loaded,
|
|
131
|
+
errors,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Factory function
|
|
136
|
+
export async function createToolRegistry() {
|
|
137
|
+
const { toolModules } = await ModuleLoader.loadAllModules();
|
|
138
|
+
const definitions = [];
|
|
139
|
+
const handlers = {};
|
|
140
|
+
const validators = {};
|
|
141
|
+
for (const module of toolModules) {
|
|
142
|
+
definitions.push(...module.definitions);
|
|
143
|
+
Object.assign(handlers, module.handlers);
|
|
144
|
+
if (module.validators) {
|
|
145
|
+
Object.assign(validators, module.validators);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
getDefinitions: () => definitions,
|
|
150
|
+
getHandler: (name) => handlers[name],
|
|
151
|
+
getValidator: (name) => validators[name],
|
|
152
|
+
getToolNames: () => definitions.map((def) => def.name),
|
|
153
|
+
};
|
|
154
|
+
}
|