#!/usr/bin/env node /** * Comprehensive API Audit: api-docs.json vs MCP Tools * Achieves 100% API coverage with accurate parameter mapping */ import fs from "fs"; import path from "path"; class ComprehensiveAPIAuditor { constructor() { this.apiEndpoints = []; this.currentTools = []; this.missingEndpoints = []; this.parameterMismatches = []; this.newTools = []; this.auditResults = {}; } /** * Phase 1: Load and parse api-docs.json (OpenAPI 3.0) */ loadApiDocs() { try { console.log("šŸ“– Loading api-docs.json..."); const apiDocsPath = path.join(process.cwd(), "../api-docs.json"); const apiDocsContent = fs.readFileSync(apiDocsPath, "utf8"); const apiDocs = JSON.parse(apiDocsContent); console.log( `šŸ“Š API Docs Info: ${apiDocs.info.title} v${apiDocs.info.version}` ); // Parse OpenAPI paths for (const [pathUrl, pathData] of Object.entries(apiDocs.paths || {})) { for (const [method, methodData] of Object.entries(pathData)) { if (typeof methodData === "object" && methodData.summary) { const endpoint = { path: pathUrl, method: method.toUpperCase(), operationId: methodData.operationId || "", summary: methodData.summary || "", description: methodData.description || "", tags: methodData.tags || [], parameters: this.extractParameters(methodData), security: methodData.security || [], authRequired: this.hasAuthRequired(methodData.security), }; this.apiEndpoints.push(endpoint); } } } console.log( `āœ… Loaded ${this.apiEndpoints.length} endpoints from api-docs.json` ); return true; } catch (error) { console.error("āŒ Error loading api-docs.json:", error.message); return false; } } /** * Extract parameters from OpenAPI method data */ extractParameters(methodData) { const parameters = []; // Path parameters if (methodData.parameters) { methodData.parameters.forEach((param) => { parameters.push({ name: param.name, type: param.schema?.type || "string", required: param.required || false, description: param.description || "", location: param.in || "query", }); }); } // Request body parameters if ( methodData.requestBody?.content?.["application/json"]?.schema?.properties ) { const schema = methodData.requestBody.content["application/json"].schema; const required = schema.required || []; Object.entries(schema.properties).forEach(([propName, propData]) => { parameters.push({ name: propName, type: propData.type || "string", required: required.includes(propName), description: propData.description || "", location: "body", }); }); } return parameters; } /** * Check if endpoint requires authentication */ hasAuthRequired(security) { return ( security && security.length > 0 && security.some((sec) => Object.keys(sec).length > 0) ); } /** * Load current MCP tools from endpoints.js */ loadCurrentTools() { try { console.log("šŸ”§ Loading current MCP tools..."); const endpointsPath = path.join(process.cwd(), "src/config/endpoints.js"); const endpointsContent = fs.readFileSync(endpointsPath, "utf8"); const authTypes = [ "PUBLIC", "PROVIDER", "PATIENT", "PARTNER", "AFFILIATE", "NETWORK", ]; authTypes.forEach((authType) => { const regex = new RegExp( `export const ${authType}_ENDPOINTS\\s*=\\s*\\[([\\s\\S]*?)\\];`, "g" ); const match = regex.exec(endpointsContent); if (match) { const sectionContent = match[1]; const endpointRegex = /\{[\s\S]*?\}/g; let endpointMatch; while ( (endpointMatch = endpointRegex.exec(sectionContent)) !== null ) { const endpointStr = endpointMatch[0]; const tool = this.parseEndpointString( endpointStr, authType.toLowerCase() ); if (tool) { this.currentTools.push(tool); } } } }); console.log(`āœ… Loaded ${this.currentTools.length} current MCP tools`); return true; } catch (error) { console.error("āŒ Error loading current tools:", error.message); return false; } } /** * Parse endpoint string to extract tool information */ parseEndpointString(endpointStr, authType) { const pathMatch = endpointStr.match(/path:\s*["']([^"']+)["']/); const methodMatch = endpointStr.match(/method:\s*["']([^"']+)["']/); const descMatch = endpointStr.match(/description:\s*["']([^"']*?)["']/); if (!pathMatch || !methodMatch) return null; return { authType, path: pathMatch[1], method: methodMatch[1].toUpperCase(), description: descMatch ? descMatch[1] : "", parameters: this.extractParametersFromString(endpointStr), toolName: this.generateToolName(authType, pathMatch[1], methodMatch[1]), }; } /** * Extract parameters from endpoint string */ extractParametersFromString(endpointStr) { const parameters = []; const paramMatch = endpointStr.match( /parameters:\s*\{([\s\S]*?)\}(?:\s*,\s*\}|\s*\})/ ); if (paramMatch) { const paramContent = paramMatch[1]; const paramRegex = /(\w+):\s*\{([^}]*)\}/g; let match; while ((match = paramRegex.exec(paramContent)) !== null) { const paramName = match[1]; const paramDef = match[2]; const typeMatch = paramDef.match(/type:\s*["']([^"']+)["']/); const requiredMatch = paramDef.match(/required:\s*(true|false)/); const descMatch = paramDef.match(/description:\s*["']([^"']*?)["']/); parameters.push({ name: paramName, type: typeMatch ? typeMatch[1] : "string", required: requiredMatch ? requiredMatch[1] === "true" : false, description: descMatch ? descMatch[1] : "", }); } } return parameters; } /** * Generate tool name following MCP conventions */ generateToolName(authType, path, method) { let cleanPath = path.replace(/^\//, "").replace(/\{[^}]+\}/g, ""); const pathParts = cleanPath.split("/").filter((part) => part.length > 0); if (pathParts[0] === "api") { pathParts.shift(); } let toolName = pathParts .join("") .replace(/[^a-zA-Z0-9]/g, "") .toLowerCase(); if (!toolName) { toolName = method.toLowerCase(); } return `${authType}_${method.toLowerCase()}_${toolName}`; } /** * Phase 1: Individual Endpoint Verification */ performEndpointVerification() { console.log("\nšŸ” Phase 1: Individual Endpoint Verification"); this.apiEndpoints.forEach((apiEndpoint) => { const matchingTool = this.currentTools.find( (tool) => tool.path === apiEndpoint.path && tool.method === apiEndpoint.method ); if (!matchingTool) { // Missing endpoint this.missingEndpoints.push({ ...apiEndpoint, authType: this.determineAuthType(apiEndpoint), }); } else { // Check parameter mismatches const mismatch = this.compareParameters(apiEndpoint, matchingTool); if (mismatch.hasDifferences) { this.parameterMismatches.push({ endpoint: apiEndpoint, tool: matchingTool, ...mismatch, }); } } }); console.log(`šŸ“Š Missing endpoints: ${this.missingEndpoints.length}`); console.log(`šŸ“Š Parameter mismatches: ${this.parameterMismatches.length}`); } /** * Determine authentication type based on endpoint characteristics */ determineAuthType(endpoint) { const path = endpoint.path.toLowerCase(); const hasAuth = endpoint.authRequired; // Public endpoints (no auth or login/register) if ( !hasAuth || path.includes("/login") || path.includes("/register") || path.includes("/forgot-password") || path.includes("/reset-password") || path.includes("/verify-email") || path.includes("/webhook") || path.includes("/room-joined") ) { return "public"; } // Provider endpoints (clinical/EMR data) if ( path.includes("/emr/") || path.includes("/patient") || path.includes("/appointment") || path.includes("/prescription") || path.includes("/medical-record") || path.includes("/provider") || path.includes("/practitioner") || path.includes("/doctor") ) { return "provider"; } // Patient endpoints if (path.includes("/patient/") && !path.includes("/emr/")) { return "patient"; } // Partner endpoints if (path.includes("/partner/")) { return "partner"; } // Affiliate endpoints if (path.includes("/affiliate/")) { return "affiliate"; } // Network endpoints if (path.includes("/network/")) { return "network"; } // Default to provider for authenticated clinical endpoints return "provider"; } /** * Compare parameters between API endpoint and MCP tool */ compareParameters(apiEndpoint, tool) { const missing = []; const extra = []; const different = []; // Check for missing parameters in tool apiEndpoint.parameters.forEach((apiParam) => { const toolParam = tool.parameters.find((tp) => tp.name === apiParam.name); if (!toolParam) { missing.push(apiParam); } else { // Check for differences if ( apiParam.type !== toolParam.type || apiParam.required !== toolParam.required || (apiParam.description && apiParam.description !== toolParam.description) ) { different.push({ name: apiParam.name, api: apiParam, tool: toolParam, }); } } }); // Check for extra parameters in tool tool.parameters.forEach((toolParam) => { const apiParam = apiEndpoint.parameters.find( (ap) => ap.name === toolParam.name ); if (!apiParam) { extra.push(toolParam); } }); return { hasDifferences: missing.length > 0 || extra.length > 0 || different.length > 0, missing, extra, different, }; } /** * Phase 2: Generate new tools for missing endpoints */ generateNewTools() { console.log("\nšŸ”§ Phase 2: Tool Generation and Updates"); this.missingEndpoints.forEach((endpoint) => { const toolName = this.generateToolName( endpoint.authType, endpoint.path, endpoint.method ); const newTool = { toolName, authType: endpoint.authType, path: endpoint.path, method: endpoint.method, controller: this.generateController(endpoint), category: this.determineCategory(endpoint), description: this.cleanString(endpoint.summary || endpoint.description), parameters: this.convertParametersToMCPFormat(endpoint.parameters), }; this.newTools.push(newTool); }); console.log(`āœ… Generated ${this.newTools.length} new tools`); } /** * Generate controller name from endpoint */ generateController(endpoint) { if (endpoint.operationId) { return `ApiController@${endpoint.operationId}`; } const pathParts = endpoint.path .split("/") .filter((part) => part && !part.startsWith("{")); if (pathParts.length > 1) { const controller = pathParts[pathParts.length - 1]; return `${ controller.charAt(0).toUpperCase() + controller.slice(1) }Controller@${endpoint.method.toLowerCase()}`; } return "ApiController@handleRequest"; } /** * Determine category based on endpoint characteristics */ determineCategory(endpoint) { const path = endpoint.path.toLowerCase(); const tags = endpoint.tags.map((tag) => tag.toLowerCase()); if ( tags.includes("meetings") || path.includes("meeting") || path.includes("call") ) { return "ENDPOINT_CATEGORIES.MEETINGS"; } if (path.includes("appointment")) { return "ENDPOINT_CATEGORIES.APPOINTMENT_SCHEDULING"; } if (path.includes("patient")) { return "ENDPOINT_CATEGORIES.PATIENT_MANAGEMENT"; } if (path.includes("prescription") || path.includes("medication")) { return "ENDPOINT_CATEGORIES.PRESCRIPTION_MANAGEMENT"; } if (path.includes("document")) { return "ENDPOINT_CATEGORIES.DOCUMENT_MANAGEMENT"; } if (path.includes("form")) { return "ENDPOINT_CATEGORIES.FORMS_QUESTIONNAIRES"; } if (path.includes("lab")) { return "ENDPOINT_CATEGORIES.MEDICAL_RECORDS"; } if (path.includes("user") || path.includes("admin")) { return "ENDPOINT_CATEGORIES.USER_MANAGEMENT"; } return "ENDPOINT_CATEGORIES.PROVIDER_MANAGEMENT"; } /** * Clean string for JavaScript */ cleanString(str) { if (!str) return ""; return str .replace(/"/g, '\\"') .replace(/\n/g, " ") .replace(/\r/g, "") .trim(); } /** * Convert API parameters to MCP format */ convertParametersToMCPFormat(apiParams) { const mcpParams = {}; apiParams.forEach((param) => { const cleanName = param.name.replace(/[^a-zA-Z0-9_]/g, "_"); mcpParams[cleanName] = { type: param.type || "string", required: param.required || false, description: this.cleanString(param.description) || "Parameter", }; }); return mcpParams; } /** * Run comprehensive audit */ async runAudit() { console.log("šŸš€ COMPREHENSIVE API AUDIT STARTING\n"); // Load data if (!this.loadApiDocs()) return false; if (!this.loadCurrentTools()) return false; // Perform verification this.performEndpointVerification(); // Generate new tools this.generateNewTools(); // Save results this.auditResults = { summary: { totalApiEndpoints: this.apiEndpoints.length, totalCurrentTools: this.currentTools.length, missingEndpoints: this.missingEndpoints.length, parameterMismatches: this.parameterMismatches.length, newToolsGenerated: this.newTools.length, }, missingEndpoints: this.missingEndpoints, parameterMismatches: this.parameterMismatches, newTools: this.newTools, timestamp: new Date().toISOString(), }; fs.writeFileSync( "comprehensive-audit-results.json", JSON.stringify(this.auditResults, null, 2) ); console.log("\nšŸ“‹ AUDIT RESULTS SUMMARY:"); console.log( `šŸ“Š Total API endpoints: ${this.auditResults.summary.totalApiEndpoints}` ); console.log( `šŸ“Š Current MCP tools: ${this.auditResults.summary.totalCurrentTools}` ); console.log( `āŒ Missing endpoints: ${this.auditResults.summary.missingEndpoints}` ); console.log( `āš ļø Parameter mismatches: ${this.auditResults.summary.parameterMismatches}` ); console.log( `šŸ†• New tools generated: ${this.auditResults.summary.newToolsGenerated}` ); console.log("\nšŸ’¾ Results saved to comprehensive-audit-results.json"); return true; } } // Run the audit const auditor = new ComprehensiveAPIAuditor(); auditor.runAudit().then((success) => { if (success) { console.log("\nāœ… Comprehensive audit completed successfully!"); } else { console.log("\nāŒ Audit failed"); } });