mcp-grocy
Version:
Model Context Protocol (MCP) server for Grocy integration
152 lines (151 loc) • 5.27 kB
JavaScript
/**
* Simplified dynamic module loader
* Improved performance with better caching and lazy loading
*/
import { readdirSync } from 'fs';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
import { logger } from '../utils/logger.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
/**
* Simplified module loader with performance optimizations
*/
export class ModuleLoader {
static moduleCache = new Map();
static discoveredFolders = null;
/**
* Discover tool folders (cached)
*/
static discoverFolders() {
if (this.discoveredFolders !== null) {
return this.discoveredFolders;
}
try {
const toolsDir = __dirname;
this.discoveredFolders = readdirSync(toolsDir, { withFileTypes: true })
.filter(dirent => dirent.isDirectory())
.filter(dirent => !dirent.name.startsWith('.') && dirent.name !== 'node_modules')
.map(dirent => dirent.name);
logger.module(`Discovered ${this.discoveredFolders.length} tool folders`);
}
catch (error) {
logger.error('Failed to discover tool folders', 'MODULE', { error });
this.discoveredFolders = [];
}
return this.discoveredFolders;
}
/**
* Load all tool modules with lazy loading
*/
static async loadAllModules() {
const folders = this.discoverFolders();
const toolModules = [];
// Load modules in parallel for better performance
const loadPromises = folders.map(folder => this.loadModule(folder));
const results = await Promise.allSettled(loadPromises);
results.forEach((result, index) => {
const folder = folders[index];
if (result.status === 'fulfilled' && result.value?.toolModule) {
toolModules.push(result.value.toolModule);
logger.module(`Loaded module: ${folder}`);
}
else if (result.status === 'rejected') {
logger.debug(`Failed to load module ${folder}`, 'MODULE', {
error: result.reason
});
}
});
logger.tools(`Loaded ${toolModules.length} tool modules`);
return { toolModules };
}
/**
* Load a specific module with caching
*/
static async loadModule(folderName) {
// Check cache first
const cached = this.moduleCache.get(folderName);
if (cached) {
return cached.loaded ? cached : null;
}
const moduleInfo = { loaded: false };
try {
// Use .js extension for production builds
const extension = '.js';
const indexPath = `./${folderName}/index${extension}`;
const moduleIndex = await import(indexPath);
// Find tool module export
for (const [, exportValue] of Object.entries(moduleIndex)) {
if (this.isToolModule(exportValue)) {
moduleInfo.toolModule = exportValue;
moduleInfo.loaded = true;
break;
}
}
if (!moduleInfo.loaded) {
logger.debug(`No tool module found in ${folderName}`, 'MODULE');
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
moduleInfo.error = errorMessage;
logger.debug(`Failed to load module ${folderName}: ${errorMessage}`, 'MODULE');
}
this.moduleCache.set(folderName, moduleInfo);
return moduleInfo.loaded ? moduleInfo : null;
}
/**
* Check if an export looks like a ToolModule
*/
static isToolModule(obj) {
return obj &&
typeof obj === 'object' &&
Array.isArray(obj.definitions) &&
typeof obj.handlers === 'object' &&
obj.definitions.length > 0;
}
/**
* Get module from cache
*/
static getCachedModule(moduleName) {
const cached = this.moduleCache.get(moduleName);
return cached?.toolModule;
}
/**
* Get cache statistics
*/
static getCacheStats() {
let loaded = 0;
let errors = 0;
for (const module of this.moduleCache.values()) {
if (module.loaded)
loaded++;
if (module.error)
errors++;
}
return {
total: this.moduleCache.size,
loaded,
errors
};
}
}
// Factory function
export async function createToolRegistry() {
const { toolModules } = await ModuleLoader.loadAllModules();
const definitions = [];
const handlers = {};
const validators = {};
for (const module of toolModules) {
definitions.push(...module.definitions);
Object.assign(handlers, module.handlers);
if (module.validators) {
Object.assign(validators, module.validators);
}
}
return {
getDefinitions: () => definitions,
getHandler: (name) => handlers[name],
getValidator: (name) => validators[name],
getToolNames: () => definitions.map(def => def.name)
};
}