mcp-grocy 1.9.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.
@@ -0,0 +1,234 @@
1
+ export const stockToolDefinitions = [
2
+ {
3
+ name: 'get_all_stock',
4
+ description: 'Get all stock entries from every location in your Grocy instance. This returns the complete stock database with detailed information including stock entry IDs.',
5
+ inputSchema: {
6
+ type: 'object',
7
+ properties: {},
8
+ required: []
9
+ }
10
+ },
11
+ {
12
+ name: 'get_stock_volatile',
13
+ description: 'Get volatile stock information (due products, overdue products, expired products, missing products).',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ includeDetails: {
18
+ type: 'boolean',
19
+ description: 'Whether to include additional details about each stock item'
20
+ }
21
+ },
22
+ required: []
23
+ }
24
+ },
25
+ {
26
+ name: 'get_stock_by_location',
27
+ description: 'Get stock entries from a specific location in your Grocy instance.',
28
+ inputSchema: {
29
+ type: 'object',
30
+ properties: {
31
+ locationId: {
32
+ type: 'number',
33
+ description: 'ID of the location to get stock for.'
34
+ }
35
+ },
36
+ required: ['locationId']
37
+ }
38
+ },
39
+ {
40
+ name: 'inventory_product',
41
+ description: 'Track a product inventory (set current stock amount). Use get_products to find the product ID and get_locations to find location IDs.',
42
+ inputSchema: {
43
+ type: 'object',
44
+ properties: {
45
+ productId: {
46
+ type: 'number',
47
+ description: 'ID of the product to inventory. Use get_products tool to find the correct product ID by name.'
48
+ },
49
+ newAmount: {
50
+ type: 'number',
51
+ description: 'The new total amount in stock in the product\'s stock unit (e.g., 5 pieces, 2.5 kg, 1000 ml). Ensure you know the product\'s stock unit before specifying amount.'
52
+ },
53
+ bestBeforeDate: {
54
+ type: 'string',
55
+ description: 'Best before date in YYYY-MM-DD format (default: today + 1 year)'
56
+ },
57
+ locationId: {
58
+ type: 'number',
59
+ description: 'ID of the storage location. Use get_locations tool to find available location IDs and names.'
60
+ },
61
+ note: {
62
+ type: 'string',
63
+ description: 'Optional note'
64
+ }
65
+ },
66
+ required: ['productId', 'newAmount', 'locationId']
67
+ }
68
+ },
69
+ {
70
+ name: 'purchase_product',
71
+ description: 'Track a product purchase in your Grocy instance. Use get_products to find product IDs, get_shopping_locations for store IDs, and get_locations for storage location IDs.',
72
+ inputSchema: {
73
+ type: 'object',
74
+ properties: {
75
+ productId: {
76
+ type: 'number',
77
+ description: 'ID of the product to purchase. Use get_products tool to find the correct product ID by name.'
78
+ },
79
+ amount: {
80
+ type: 'number',
81
+ description: 'Amount to purchase in the product\'s stock unit (e.g., 1 piece, 2.5 kg, 500 ml). Ensure you know the product\'s stock unit before specifying amount.'
82
+ },
83
+ bestBeforeDate: {
84
+ type: 'string',
85
+ description: 'Best before date in YYYY-MM-DD format (default: today + 1 year)'
86
+ },
87
+ price: {
88
+ type: 'number',
89
+ description: 'Price of the purchase (optional)'
90
+ },
91
+ storeId: {
92
+ type: 'number',
93
+ description: 'ID of the store where purchased (optional). Use get_shopping_locations tool to find available store IDs and names.'
94
+ },
95
+ locationId: {
96
+ type: 'number',
97
+ description: 'ID of the storage location. Use get_locations tool to find available location IDs and names.'
98
+ },
99
+ note: {
100
+ type: 'string',
101
+ description: 'Optional note'
102
+ }
103
+ },
104
+ required: ['productId', 'amount', 'locationId']
105
+ }
106
+ },
107
+ {
108
+ name: 'consume_product',
109
+ description: 'Track consumption of a specific stock entry in your Grocy instance.',
110
+ inputSchema: {
111
+ type: 'object',
112
+ properties: {
113
+ stockId: {
114
+ type: 'number',
115
+ description: 'ID of the specific stock entry to consume.'
116
+ },
117
+ productId: {
118
+ type: 'number',
119
+ // ProductId is required for verification - if user knows the stockId, they must know the productId.
120
+ // This ensures the call is made to the correct stock entry and prevents accidental operations.
121
+ description: 'ID of the product being consumed.'
122
+ },
123
+ amount: {
124
+ type: 'number',
125
+ description: 'Amount to consume in the product\'s stock unit (e.g., 1 piece, 0.5 kg, 250 ml). Ensure you know the product\'s stock unit before specifying amount.'
126
+ },
127
+ spoiled: {
128
+ type: 'boolean',
129
+ description: 'Whether the product is spoiled (default: false)',
130
+ default: false
131
+ },
132
+ note: {
133
+ type: 'string',
134
+ description: 'Optional note'
135
+ }
136
+ },
137
+ required: ['stockId', 'productId', 'amount']
138
+ }
139
+ },
140
+ {
141
+ name: 'transfer_product',
142
+ description: 'Transfer a specific stock entry to another location in your Grocy instance.',
143
+ inputSchema: {
144
+ type: 'object',
145
+ properties: {
146
+ stockId: {
147
+ type: 'number',
148
+ description: 'ID of the specific stock entry to transfer.'
149
+ },
150
+ productId: {
151
+ type: 'number',
152
+ // ProductId is required for verification - if user knows the stockId, they must know the productId.
153
+ // This ensures the call is made to the correct stock entry and prevents accidental operations.
154
+ description: 'ID of the product being transferred.'
155
+ },
156
+ amount: {
157
+ type: 'number',
158
+ description: 'Amount to transfer in the product\'s stock unit (e.g., 1 piece, 0.5 kg, 250 ml). Ensure you know the product\'s stock unit before specifying amount.'
159
+ },
160
+ locationIdTo: {
161
+ type: 'number',
162
+ description: 'ID of the destination location.'
163
+ },
164
+ note: {
165
+ type: 'string',
166
+ description: 'Optional note for this transfer'
167
+ }
168
+ },
169
+ required: ['stockId', 'productId', 'amount', 'locationIdTo']
170
+ }
171
+ },
172
+ {
173
+ name: 'open_product',
174
+ description: 'Mark a specific stock entry as opened in your Grocy instance.',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {
178
+ stockId: {
179
+ type: 'number',
180
+ description: 'ID of the specific stock entry to mark as opened.'
181
+ },
182
+ productId: {
183
+ type: 'number',
184
+ // ProductId is required for verification - if user knows the stockId, they must know the productId.
185
+ // This ensures the call is made to the correct stock entry and prevents accidental operations.
186
+ description: 'ID of the product being opened.'
187
+ },
188
+ amount: {
189
+ type: 'number',
190
+ description: 'Amount to mark as opened in the product\'s stock unit (e.g., 1 piece, 0.5 kg, 200 ml). Ensure you know the product\'s stock unit before specifying amount.'
191
+ },
192
+ note: {
193
+ type: 'string',
194
+ description: 'Optional note'
195
+ }
196
+ },
197
+ required: ['stockId', 'productId', 'amount']
198
+ }
199
+ },
200
+ {
201
+ name: 'lookup_product',
202
+ description: 'Lookup product information with advanced fuzzy matching. Returns all relevant data including exact IDs, available locations, and stock entries.',
203
+ inputSchema: {
204
+ type: 'object',
205
+ properties: {
206
+ productName: {
207
+ type: 'string',
208
+ description: 'Name of the product to lookup.'
209
+ }
210
+ },
211
+ required: ['productName']
212
+ }
213
+ },
214
+ {
215
+ name: 'print_stock_entry_label',
216
+ description: 'Print a label for a specific stock entry.',
217
+ inputSchema: {
218
+ type: 'object',
219
+ properties: {
220
+ stockId: {
221
+ type: 'number',
222
+ description: 'ID of the stock entry to print label for.'
223
+ },
224
+ productId: {
225
+ type: 'number',
226
+ // ProductId is required for verification - if user knows the stockId, they must know the productId.
227
+ // This ensures the call is made to the correct stock entry and prevents accidental operations.
228
+ description: 'ID of the product for the label.'
229
+ }
230
+ },
231
+ required: ['stockId', 'productId']
232
+ }
233
+ }
234
+ ];
@@ -0,0 +1,391 @@
1
+ import { BaseToolHandler } from '../base.js';
2
+ import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
3
+ import apiClient from '../../api/client.js';
4
+ import Fuse from 'fuse.js';
5
+ export class StockToolHandlers extends BaseToolHandler {
6
+ async splitStockEntry(originalEntry, stockAmounts, getUnitForm) {
7
+ const splitEntries = [];
8
+ if (stockAmounts.length === 1) {
9
+ const note = `${originalEntry.note} - ${originalEntry.id} - 1`;
10
+ await apiClient.put(`/stock/entry/${originalEntry.id}`, {
11
+ amount: stockAmounts[0],
12
+ open: false,
13
+ note: note,
14
+ best_before_date: originalEntry.best_before_date,
15
+ purchased_date: originalEntry.purchased_date,
16
+ location_id: originalEntry.location_id
17
+ });
18
+ splitEntries.push({
19
+ stockId: originalEntry.id,
20
+ amount: stockAmounts[0],
21
+ type: 'updated',
22
+ unit: getUnitForm(stockAmounts[0])
23
+ });
24
+ }
25
+ else {
26
+ for (let i = 0; i < stockAmounts.length; i++) {
27
+ const amount = stockAmounts[i];
28
+ const note = `${originalEntry.note} - ${originalEntry.id} - ${i + 1}`;
29
+ if (i === 0) {
30
+ await apiClient.put(`/stock/entry/${originalEntry.id}`, {
31
+ amount: amount,
32
+ open: false,
33
+ note: note,
34
+ best_before_date: originalEntry.best_before_date,
35
+ purchased_date: originalEntry.purchased_date,
36
+ location_id: originalEntry.location_id
37
+ });
38
+ splitEntries.push({
39
+ stockId: originalEntry.id,
40
+ amount,
41
+ type: 'updated',
42
+ unit: getUnitForm(amount)
43
+ });
44
+ }
45
+ else {
46
+ const createResponse = await apiClient.post(`/stock/products/${originalEntry.product_id}/add`, {
47
+ amount,
48
+ best_before_date: originalEntry.best_before_date,
49
+ purchased_date: originalEntry.purchased_date,
50
+ transaction_type: 'purchase',
51
+ location_id: originalEntry.location_id,
52
+ note: note
53
+ });
54
+ const stockId = createResponse.data[0].stock_id || createResponse.data[0].id;
55
+ const stockResponse = await apiClient.get('/objects/stock');
56
+ const stockEntries = Array.isArray(stockResponse.data) ? stockResponse.data : [];
57
+ const actualStockEntry = stockEntries.find((entry) => entry.product_id === originalEntry.product_id &&
58
+ entry.stock_id === stockId);
59
+ if (!actualStockEntry) {
60
+ throw new Error(`Could not find created stock entry with product_id ${originalEntry.product_id} and stock_id ${stockId}`);
61
+ }
62
+ splitEntries.push({
63
+ stockId: actualStockEntry.id,
64
+ amount,
65
+ type: 'created',
66
+ unit: getUnitForm(amount)
67
+ });
68
+ }
69
+ }
70
+ }
71
+ return splitEntries;
72
+ }
73
+ getAllStock = async (args) => {
74
+ return this.handleApiCall('/stock', 'Get current stock');
75
+ };
76
+ getStockVolatile = async (args) => {
77
+ const queryParams = args?.includeDetails ? { include_details: 'true' } : {};
78
+ return this.handleApiCall('/stock/volatile', 'Get volatile stock information', { queryParams });
79
+ };
80
+ getStockByLocation = async (args) => {
81
+ const { locationId } = args || {};
82
+ if (!locationId) {
83
+ throw new McpError(ErrorCode.InvalidParams, 'locationId is required');
84
+ }
85
+ return this.handleApiCall(`stock`, 'Get stock by location', {
86
+ queryParams: { location_id: locationId.toString() }
87
+ });
88
+ };
89
+ inventoryProduct = async (args) => {
90
+ const { productId, newAmount, bestBeforeDate, locationId, note } = args || {};
91
+ if (!productId || newAmount === undefined) {
92
+ throw new McpError(ErrorCode.InvalidParams, 'productId and newAmount are required');
93
+ }
94
+ const defaultBBD = new Date();
95
+ defaultBBD.setFullYear(defaultBBD.getFullYear() + 1);
96
+ const formattedBBD = bestBeforeDate || defaultBBD.toISOString().split('T')[0];
97
+ const body = {
98
+ new_amount: newAmount,
99
+ best_before_date: formattedBBD,
100
+ transaction_type: 'inventory-correction'
101
+ };
102
+ if (locationId)
103
+ body.location_id = locationId;
104
+ if (note)
105
+ body.note = note;
106
+ return this.handleApiCall(`/stock/products/${productId}/inventory`, 'Inventory product', {
107
+ method: 'POST',
108
+ body
109
+ });
110
+ };
111
+ purchaseProduct = async (args) => {
112
+ const { productId, amount = 1, bestBeforeDate, price, storeId, locationId, note } = args || {};
113
+ if (!productId) {
114
+ throw new McpError(ErrorCode.InvalidParams, 'productId is required');
115
+ }
116
+ const defaultBBD = new Date();
117
+ defaultBBD.setFullYear(defaultBBD.getFullYear() + 1);
118
+ const formattedBBD = bestBeforeDate || defaultBBD.toISOString().split('T')[0];
119
+ const body = {
120
+ amount,
121
+ transaction_type: 'purchase',
122
+ best_before_date: formattedBBD
123
+ };
124
+ if (price !== undefined)
125
+ body.price = price;
126
+ if (storeId)
127
+ body.shopping_location_id = storeId;
128
+ if (locationId)
129
+ body.location_id = locationId;
130
+ if (note)
131
+ body.note = note;
132
+ return this.handleApiCall(`/stock/products/${productId}/add`, 'Purchase product', {
133
+ method: 'POST',
134
+ body
135
+ });
136
+ };
137
+ consumeProduct = async (args) => {
138
+ const { stockId, productId, amount, spoiled = false, note } = args || {};
139
+ if (!stockId) {
140
+ throw new McpError(ErrorCode.InvalidParams, 'stockId is required. Use get_stock_by_product or get_stock_by_location to find specific stockId values.');
141
+ }
142
+ if (!productId) {
143
+ throw new McpError(ErrorCode.InvalidParams, 'productId is required.');
144
+ }
145
+ if (amount === undefined) {
146
+ throw new McpError(ErrorCode.InvalidParams, 'amount is required');
147
+ }
148
+ try {
149
+ const stockEntryResponse = await apiClient.get(`/stock/entry/${stockId}`);
150
+ if (!stockEntryResponse.data || !stockEntryResponse.data.product_id) {
151
+ throw new Error(`Could not resolve product ID from stock entry ${stockId}`);
152
+ }
153
+ if (stockEntryResponse.data.product_id !== productId) {
154
+ throw new Error(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.data.product_id}, but ${productId} was provided`);
155
+ }
156
+ const body = {
157
+ amount,
158
+ spoiled,
159
+ stock_entry_id: stockEntryResponse.data.stock_id,
160
+ location_id: stockEntryResponse.data.location_id
161
+ };
162
+ if (note)
163
+ body.note = note;
164
+ return this.handleApiCall(`/stock/products/${stockEntryResponse.data.product_id}/consume`, 'Consume product', {
165
+ method: 'POST',
166
+ body
167
+ });
168
+ }
169
+ catch (error) {
170
+ return this.createErrorResult(`Failed to consume product: ${error.message}`);
171
+ }
172
+ };
173
+ transferProduct = async (args) => {
174
+ const { stockId, productId, amount, locationIdTo, note } = args || {};
175
+ if (!stockId) {
176
+ throw new McpError(ErrorCode.InvalidParams, 'stockId is required. Use get_stock_by_product or get_stock_by_location to find specific stockId values.');
177
+ }
178
+ if (!productId) {
179
+ throw new McpError(ErrorCode.InvalidParams, 'productId is required.');
180
+ }
181
+ if (amount === undefined) {
182
+ throw new McpError(ErrorCode.InvalidParams, 'amount is required');
183
+ }
184
+ try {
185
+ const stockEntryResponse = await apiClient.get(`/stock/entry/${stockId}`);
186
+ if (!stockEntryResponse.data || !stockEntryResponse.data.product_id) {
187
+ throw new Error(`Could not resolve product ID from stock entry ${stockId}`);
188
+ }
189
+ if (stockEntryResponse.data.product_id !== productId) {
190
+ throw new Error(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.data.product_id}, but ${productId} was provided`);
191
+ }
192
+ const body = {
193
+ amount,
194
+ location_id_from: stockEntryResponse.data.location_id,
195
+ location_id_to: locationIdTo,
196
+ transaction_type: 'transfer',
197
+ stock_entry_id: stockEntryResponse.data.stock_id
198
+ };
199
+ if (note)
200
+ body.note = note;
201
+ return this.handleApiCall(`/stock/products/${stockEntryResponse.data.product_id}/transfer`, 'Transfer product', {
202
+ method: 'POST',
203
+ body
204
+ });
205
+ }
206
+ catch (error) {
207
+ return this.createErrorResult(`Failed to transfer product: ${error.message}`);
208
+ }
209
+ };
210
+ openProduct = async (args) => {
211
+ const { stockId, productId, amount, note } = args;
212
+ if (!stockId) {
213
+ throw new McpError(ErrorCode.InvalidParams, 'stockId is required. Use get_stock_by_product tool to find specific stockId values.');
214
+ }
215
+ if (!productId) {
216
+ throw new McpError(ErrorCode.InvalidParams, 'productId is required.');
217
+ }
218
+ if (amount === undefined) {
219
+ throw new McpError(ErrorCode.InvalidParams, 'amount is required');
220
+ }
221
+ try {
222
+ const stockEntryResponse = await apiClient.get(`/stock/entry/${stockId}`);
223
+ if (!stockEntryResponse.data || !stockEntryResponse.data.product_id) {
224
+ throw new Error(`Could not resolve product ID from stock entry ${stockId}`);
225
+ }
226
+ if (stockEntryResponse.data.product_id !== productId) {
227
+ throw new Error(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.data.product_id}, but ${productId} was provided`);
228
+ }
229
+ const body = {
230
+ amount,
231
+ stock_entry_id: stockEntryResponse.data.stock_id,
232
+ location_id: stockEntryResponse.data.location_id
233
+ };
234
+ if (note)
235
+ body.note = note;
236
+ return this.handleApiCall(`/stock/products/${stockEntryResponse.data.product_id}/open`, 'Open product', {
237
+ method: 'POST',
238
+ body
239
+ });
240
+ }
241
+ catch (error) {
242
+ console.error('Error opening product:', error);
243
+ return this.createErrorResult(`Failed to open product: ${error.message}`, {
244
+ help: "Use get_stock_by_product tool to find valid stockId values for a specific product.",
245
+ example: "Try using get_stock_by_product with a product ID to find valid stock entries"
246
+ });
247
+ }
248
+ };
249
+ lookupProduct = async (args) => {
250
+ const { productName } = args || {};
251
+ if (!productName) {
252
+ throw new McpError(ErrorCode.InvalidParams, 'productName is required');
253
+ }
254
+ try {
255
+ const [productsResponse, locationsResponse, quantityUnitsResponse] = await Promise.all([
256
+ apiClient.get('/objects/products'),
257
+ apiClient.get('/objects/locations'),
258
+ apiClient.get('/objects/quantity_units')
259
+ ]);
260
+ const products = Array.isArray(productsResponse.data) ? productsResponse.data : [];
261
+ const locations = Array.isArray(locationsResponse.data) ? locationsResponse.data : [];
262
+ const quantityUnits = Array.isArray(quantityUnitsResponse.data) ? quantityUnitsResponse.data : [];
263
+ const fuseOptions = {
264
+ keys: [
265
+ { name: 'name', weight: 1.0 },
266
+ { name: 'description', weight: 0.3 }
267
+ ],
268
+ threshold: 0.6,
269
+ distance: 100,
270
+ minMatchCharLength: 1,
271
+ ignoreLocation: true,
272
+ includeScore: true,
273
+ includeMatches: true,
274
+ useExtendedSearch: false,
275
+ isCaseSensitive: false,
276
+ shouldSort: true,
277
+ findAllMatches: false
278
+ };
279
+ const fuse = new Fuse(products, fuseOptions);
280
+ const fuseResults = fuse.search(productName);
281
+ let productMatches = fuseResults.map((result) => ({
282
+ ...result.item,
283
+ matchScore: Math.round((1 - result.score) * 100),
284
+ fuseScore: result.score,
285
+ matches: result.matches
286
+ }));
287
+ if (productMatches.length === 0) {
288
+ const permissiveFuse = new Fuse(products, {
289
+ ...fuseOptions,
290
+ threshold: 0.8,
291
+ distance: 200
292
+ });
293
+ const permissiveResults = permissiveFuse.search(productName);
294
+ productMatches = permissiveResults.map((result) => ({
295
+ ...result.item,
296
+ matchScore: Math.round((1 - result.score) * 100),
297
+ fuseScore: result.score,
298
+ matches: result.matches,
299
+ isPermissiveMatch: true
300
+ }));
301
+ }
302
+ productMatches = productMatches.slice(0, 5);
303
+ if (productMatches.length === 0) {
304
+ return this.createErrorResult(`No products found matching "${productName}"`, {
305
+ suggestion: 'Try a different product name or check the spelling',
306
+ availableProducts: products.slice(0, 10).map((p) => p.name)
307
+ });
308
+ }
309
+ const enrichedMatches = await Promise.all(productMatches.map(async (product) => {
310
+ let productEntries = [];
311
+ try {
312
+ const entriesResponse = await apiClient.get(`/stock/products/${product.id}/entries`);
313
+ productEntries = Array.isArray(entriesResponse.data) ? entriesResponse.data : [];
314
+ }
315
+ catch {
316
+ productEntries = [];
317
+ }
318
+ const stockEntries = productEntries.map((stockItem) => {
319
+ return {
320
+ amount: stockItem.amount,
321
+ bestBeforeDate: stockItem.best_before_date,
322
+ stockId: stockItem.id,
323
+ locationId: parseInt(stockItem.location_id)
324
+ };
325
+ });
326
+ stockEntries.sort((a, b) => {
327
+ if (!a.bestBeforeDate && !b.bestBeforeDate)
328
+ return 0;
329
+ if (!a.bestBeforeDate)
330
+ return 1;
331
+ if (!b.bestBeforeDate)
332
+ return -1;
333
+ return new Date(a.bestBeforeDate).getTime() - new Date(b.bestBeforeDate).getTime();
334
+ });
335
+ const unit = quantityUnits.find((u) => u.id == product.qu_id_stock);
336
+ const unitInfo = unit
337
+ ? { id: unit.id, name: unit.name }
338
+ : { id: null, name: 'pieces' };
339
+ const hasMultipleLocations = stockEntries.length > 1 &&
340
+ new Set(stockEntries.map((entry) => entry.locationId)).size > 1;
341
+ const locationInstructions = hasMultipleLocations
342
+ ? 'IMPORTANT: This product has stock in multiple locations. Make sure the user requested a specific location or confirm the locationId before performing any operations.'
343
+ : undefined;
344
+ return {
345
+ productId: product.id,
346
+ productName: product.name,
347
+ stockEntries: stockEntries,
348
+ totalStockAmount: productEntries.reduce((sum, s) => sum + parseFloat(s.amount || 0), 0),
349
+ unit: unitInfo,
350
+ locationInstructions
351
+ };
352
+ }));
353
+ return this.createSuccessResult({
354
+ message: `Found ${enrichedMatches.length} product matches for "${productName}" (ordered from most likely to least likely match)`,
355
+ productMatches: enrichedMatches,
356
+ allAvailableLocations: locations.map((l) => ({ id: l.id, name: l.name })),
357
+ instructions: 'Review the matches above. Use the exact productId and locationId from this data for any product operations (consume_product, purchase_product, inventory_product, transfer_product, etc.).'
358
+ });
359
+ }
360
+ catch (error) {
361
+ return this.createErrorResult(`Failed to lookup product: ${error.message}`);
362
+ }
363
+ };
364
+ printStockEntryLabel = async (args) => {
365
+ const { stockId, productId } = args || {};
366
+ if (!stockId) {
367
+ throw new McpError(ErrorCode.InvalidParams, 'stockId is required. Use get_stock_by_product tool to find specific stockId values.');
368
+ }
369
+ if (!productId) {
370
+ throw new McpError(ErrorCode.InvalidParams, 'productId is required.');
371
+ }
372
+ try {
373
+ const stockEntryResponse = await apiClient.get(`/stock/entry/${stockId}`);
374
+ if (!stockEntryResponse.data || !stockEntryResponse.data.product_id) {
375
+ throw new Error(`Could not resolve product ID from stock entry ${stockId}`);
376
+ }
377
+ if (stockEntryResponse.data.product_id !== productId) {
378
+ throw new Error(`Product ID mismatch: stock entry ${stockId} belongs to product ${stockEntryResponse.data.product_id}, but ${productId} was provided`);
379
+ }
380
+ return this.handleApiCall(`/stock/entry/${stockId}/printlabel`, 'Print stock entry label');
381
+ }
382
+ catch (error) {
383
+ console.error('Error printing stock entry label:', error);
384
+ return this.createErrorResult(`Failed to print stock entry label: ${error.message}`, {
385
+ help: "Use get_stock_by_product tool to find valid stockId values for a specific product.",
386
+ example: "Try using get_stock_by_product with a product ID to find valid stock entries"
387
+ });
388
+ }
389
+ };
390
+ }
391
+ export const stockHandlers = new StockToolHandlers();
@@ -0,0 +1,19 @@
1
+ import { stockToolDefinitions } from './definitions.js';
2
+ import { stockHandlers } from './handlers.js';
3
+ export const stockModule = {
4
+ definitions: stockToolDefinitions,
5
+ handlers: {
6
+ get_all_stock: stockHandlers.getAllStock,
7
+ get_stock_volatile: stockHandlers.getStockVolatile,
8
+ get_stock_by_location: stockHandlers.getStockByLocation,
9
+ inventory_product: stockHandlers.inventoryProduct,
10
+ purchase_product: stockHandlers.purchaseProduct,
11
+ consume_product: stockHandlers.consumeProduct,
12
+ transfer_product: stockHandlers.transferProduct,
13
+ open_product: stockHandlers.openProduct,
14
+ lookup_product: stockHandlers.lookupProduct,
15
+ print_stock_entry_label: stockHandlers.printStockEntryLabel
16
+ }
17
+ };
18
+ export * from './definitions.js';
19
+ export * from './handlers.js';