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