UNPKG

openapi-directory-mcp

Version:

Model Context Protocol server for accessing enhanced triple-source OpenAPI directory (APIs.guru + additional APIs + custom imports)

215 lines 7.87 kB
import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { ToolRegistry } from "./registry.js"; // ES module compatibility - avoid conflicts in test environments const getToolsDir = () => { try { // Use dynamic evaluation to avoid Jest parsing issues with import.meta const importMeta = eval("import.meta"); if (typeof importMeta !== "undefined" && importMeta.url) { const __filename = fileURLToPath(importMeta.url); const toolsDir = path.dirname(__filename); return toolsDir; } } catch (error) { // Fallback for test environments } // Try multiple paths to find the tools directory const projectRoot = findProjectRoot(); const possiblePaths = [ // Current working directory variations path.join(process.cwd(), "dist", "tools"), path.join(process.cwd(), "src", "tools"), // Search up the directory tree for package.json projectRoot && path.join(projectRoot, "dist", "tools"), projectRoot && path.join(projectRoot, "src", "tools"), ].filter(Boolean); // Find the first existing path that contains tool directories for (const testPath of possiblePaths) { try { if (fs.existsSync(testPath) && fs.statSync(testPath).isDirectory()) { // Check if it contains expected tool category directories const entries = fs.readdirSync(testPath, { withFileTypes: true }); const hasToolDirs = entries.some((entry) => entry.isDirectory() && [ "provider", "api", "search", "openapi", "endpoint", "cache", ].includes(entry.name)); if (hasToolDirs) { return testPath; } } } catch { // Continue to next path } } // Last resort fallback const fallback = path.join(process.cwd(), "dist", "tools"); return fallback; }; // Helper function to find project root by looking for package.json const findProjectRoot = () => { let currentDir = process.cwd(); // Limit search to prevent infinite loops for (let i = 0; i < 10; i++) { const packageJsonPath = path.join(currentDir, "package.json"); if (fs.existsSync(packageJsonPath)) { try { const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); // Verify it's our project by checking the name if (packageJson.name === "openapi-directory-mcp") { return currentDir; } } catch { // Continue searching } } const parentDir = path.dirname(currentDir); if (parentDir === currentDir) { // Reached root directory break; } currentDir = parentDir; } return null; }; export class ToolLoader { constructor() { this.registry = new ToolRegistry(); this.loaded = false; } async loadAllTools() { if (!this.loaded) { await this.scanAndLoadCategories(); this.loaded = true; } return this.registry.getAllTools(); } async scanAndLoadCategories() { const toolsDir = getToolsDir(); try { const entries = fs.readdirSync(toolsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && this.isValidCategoryName(entry.name)) { await this.loadCategory(entry.name); } } } catch (error) { console.error("Error scanning tools directory:", error); throw new Error(`Failed to scan tools directory: ${error}`); } } async loadCategory(categoryName) { const categoryPath = path.join(getToolsDir(), categoryName); try { const files = fs .readdirSync(categoryPath) .filter((file) => file.endsWith(".js") && !file.endsWith(".d.ts")) .filter((file) => file !== "index.js"); // Skip index files for (const file of files) { await this.loadToolFile(categoryName, categoryPath, file); } } catch (error) { console.error(`Error loading category ${categoryName}:`, error); // Don't throw - continue loading other categories } } async loadToolFile(categoryName, categoryPath, filename) { const filePath = path.join(categoryPath, filename); const relativePath = path.relative(getToolsDir(), filePath); try { // Use absolute file path or file:// URL for dynamic import // This ensures the import works regardless of the current working directory const absolutePath = path.resolve(filePath); const fileUrl = `file://${absolutePath.replace(/\\/g, "/")}`; const module = await import(fileUrl); let tool; // Try different export patterns if (module.default && this.isValidTool(module.default)) { tool = module.default; } else if (module.tool && this.isValidTool(module.tool)) { tool = module.tool; } else { // Look for any named export that's a valid tool for (const value of Object.values(module)) { if (this.isValidTool(value)) { tool = value; break; } } } if (tool) { this.registry.register(categoryName, tool); // Tool loaded successfully } else { // No valid tool found - skip file } } catch (error) { console.error(`Error loading tool file ${relativePath}:`, error); // Don't throw - continue loading other files } } isValidTool(obj) { return (obj && typeof obj === "object" && typeof obj.name === "string" && obj.name.length > 0 && typeof obj.description === "string" && obj.description.length > 0 && typeof obj.execute === "function" && obj.inputSchema !== undefined); } isValidCategoryName(name) { // Skip hidden directories, node_modules, etc. return (!name.startsWith(".") && !name.startsWith("_") && name !== "node_modules" && name !== "dist" && name !== "build"); } getRegistry() { return this.registry; } async getCategories() { if (!this.loaded) { await this.scanAndLoadCategories(); this.loaded = true; } return this.registry.getCategories().map((categoryName) => ({ name: categoryName, tools: this.registry.getCategory(categoryName), })); } async getToolsByCategory(categoryName) { if (!this.loaded) { await this.scanAndLoadCategories(); this.loaded = true; } return this.registry.getCategory(categoryName); } async getTool(name) { if (!this.loaded) { await this.scanAndLoadCategories(); this.loaded = true; } return this.registry.getTool(name); } } // Create a singleton instance for easy access export const toolLoader = new ToolLoader(); export const toolRegistry = toolLoader.getRegistry(); //# sourceMappingURL=loader.js.map