module-loader-service.ts•4.92 kB
/**
* Module Loader Service
*
* This service handles dynamic loading of MCP components from directories.
* It provides file-based automatic loading without hardcoded associations.
*/
import { readdir, stat } from "fs/promises";
import { join, extname } from "path";
import { pathToFileURL } from "url";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type {
MCPModule,
IModuleLoaderService
} from "../types/index.js";
export class ModuleLoaderService implements IModuleLoaderService {
private loadedModules: Map<string, MCPModule> = new Map();
/**
* Load all modules from a specified directory
* @param directoryPath The path to the directory containing modules
* @returns Array of loaded MCP modules
*/
async loadModulesFromDirectory(directoryPath: string): Promise<MCPModule[]> {
console.log(`Loading modules from directory: ${directoryPath}`);
try {
const files = await this.getModuleFiles(directoryPath);
const modules: MCPModule[] = [];
for (const file of files) {
try {
const module = await this.loadModule(file);
if (module) {
modules.push(module);
this.loadedModules.set(file, module);
console.log(`✓ Loaded module: ${module.metadata?.name || file}`);
}
} catch (error) {
console.error(`✗ Failed to load module ${file}:`, error);
}
}
console.log(`Successfully loaded ${modules.length} modules from ${directoryPath}`);
return modules;
} catch (error) {
console.error(`Failed to load modules from directory ${directoryPath}:`, error);
return [];
}
}
/**
* Register a loaded module with the MCP server
* @param module The module to register
* @param server The MCP server instance
*/
async registerModule(module: MCPModule, server: McpServer): Promise<void> {
try {
await module.register(server);
console.log(`✓ Registered module: ${module.metadata?.name || 'Unknown'}`);
} catch (error) {
console.error(`✗ Failed to register module ${module.metadata?.name || 'Unknown'}:`, error);
throw error;
}
}
/**
* Get all TypeScript/JavaScript files from a directory
* @param directoryPath The directory to scan
* @returns Array of file paths
*/
private async getModuleFiles(directoryPath: string): Promise<string[]> {
const files: string[] = [];
try {
const entries = await readdir(directoryPath);
for (const entry of entries) {
const fullPath = join(directoryPath, entry);
const stats = await stat(fullPath);
if (stats.isFile()) {
const ext = extname(entry).toLowerCase();
// Load TypeScript and JavaScript files, but skip index files and declaration files
if ((ext === '.ts' || ext === '.js') &&
!entry.startsWith('index.') &&
!entry.endsWith('.d.ts')) {
files.push(fullPath);
}
}
}
} catch (error) {
console.error(`Error reading directory ${directoryPath}:`, error);
}
return files;
}
/**
* Dynamically import and validate a module
* @param filePath The path to the module file
* @returns The loaded MCP module or null if invalid
*/
private async loadModule(filePath: string): Promise<MCPModule | null> {
try {
// Convert file path to file URL for dynamic import
const fileUrl = pathToFileURL(filePath).href;
const imported = await import(fileUrl);
// Check if the module exports a valid MCP module structure
if (this.isValidMCPModule(imported)) {
return imported as MCPModule;
}
// Check if it exports a default MCP module
if (imported.default && this.isValidMCPModule(imported.default)) {
return imported.default as MCPModule;
}
console.warn(`Module ${filePath} does not export a valid MCP module structure`);
return null;
} catch (error) {
console.error(`Error importing module ${filePath}:`, error);
return null;
}
}
/**
* Validate if an imported object is a valid MCP module
* @param obj The object to validate
* @returns True if valid MCP module
*/
private isValidMCPModule(obj: any): boolean {
return obj &&
typeof obj === 'object' &&
typeof obj.register === 'function';
}
/**
* Get information about all loaded modules
* @returns Array of module metadata
*/
getLoadedModulesInfo(): Array<{ name: string; metadata?: any }> {
return Array.from(this.loadedModules.entries()).map(([path, module]) => ({
name: module.metadata?.name || path,
metadata: module.metadata
}));
}
/**
* Clear all loaded modules
*/
clearLoadedModules(): void {
this.loadedModules.clear();
}
}