fix add tool
This commit is contained in:
@@ -1,358 +1,575 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Comprehensive API Audit Script
|
||||
* Cross-references api-docs.json against endpoints.js to identify missing endpoints
|
||||
* and parameter discrepancies for complete MCP server coverage
|
||||
* 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";
|
||||
|
||||
/**
|
||||
* Load and parse API documentation
|
||||
*/
|
||||
function loadApiDocumentation() {
|
||||
try {
|
||||
const apiDocsPath = path.join(
|
||||
process.cwd(),
|
||||
"complete-api-parameters.json"
|
||||
);
|
||||
const apiDocsContent = fs.readFileSync(apiDocsPath, "utf8");
|
||||
return JSON.parse(apiDocsContent);
|
||||
} catch (error) {
|
||||
console.error("❌ Error loading API documentation:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse current endpoints configuration
|
||||
*/
|
||||
function loadCurrentEndpoints() {
|
||||
try {
|
||||
const endpointsPath = path.join(process.cwd(), "src/config/endpoints.js");
|
||||
const endpointsContent = fs.readFileSync(endpointsPath, "utf8");
|
||||
|
||||
// Extract endpoint arrays using regex
|
||||
const publicMatch = endpointsContent.match(
|
||||
/export const PUBLIC_ENDPOINTS = \[([\s\S]*?)\];/
|
||||
);
|
||||
const providerMatch = endpointsContent.match(
|
||||
/export const PROVIDER_ENDPOINTS = \[([\s\S]*?)\];/
|
||||
);
|
||||
const patientMatch = endpointsContent.match(
|
||||
/export const PATIENT_ENDPOINTS = \[([\s\S]*?)\];/
|
||||
);
|
||||
const partnerMatch = endpointsContent.match(
|
||||
/export const PARTNER_ENDPOINTS = \[([\s\S]*?)\];/
|
||||
);
|
||||
const affiliateMatch = endpointsContent.match(
|
||||
/export const AFFILIATE_ENDPOINTS = \[([\s\S]*?)\];/
|
||||
);
|
||||
const networkMatch = endpointsContent.match(
|
||||
/export const NETWORK_ENDPOINTS = \[([\s\S]*?)\];/
|
||||
);
|
||||
|
||||
const endpoints = {
|
||||
PUBLIC: [],
|
||||
PROVIDER: [],
|
||||
PATIENT: [],
|
||||
PARTNER: [],
|
||||
AFFILIATE: [],
|
||||
NETWORK: [],
|
||||
};
|
||||
|
||||
// Parse endpoints from each array
|
||||
if (publicMatch) {
|
||||
endpoints.PUBLIC = extractEndpointsFromText(publicMatch[1]);
|
||||
}
|
||||
if (providerMatch) {
|
||||
endpoints.PROVIDER = extractEndpointsFromText(providerMatch[1]);
|
||||
}
|
||||
if (patientMatch) {
|
||||
endpoints.PATIENT = extractEndpointsFromText(patientMatch[1]);
|
||||
}
|
||||
if (partnerMatch) {
|
||||
endpoints.PARTNER = extractEndpointsFromText(partnerMatch[1]);
|
||||
}
|
||||
if (affiliateMatch) {
|
||||
endpoints.AFFILIATE = extractEndpointsFromText(affiliateMatch[1]);
|
||||
}
|
||||
if (networkMatch) {
|
||||
endpoints.NETWORK = extractEndpointsFromText(networkMatch[1]);
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
} catch (error) {
|
||||
console.error("❌ Error loading current endpoints:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract endpoint objects from text using regex
|
||||
*/
|
||||
function extractEndpointsFromText(text) {
|
||||
const endpoints = [];
|
||||
const endpointRegex =
|
||||
/\{[\s\S]*?path:\s*["']([^"']+)["'][\s\S]*?method:\s*["']([^"']+)["'][\s\S]*?\}/g;
|
||||
|
||||
let match;
|
||||
while ((match = endpointRegex.exec(text)) !== null) {
|
||||
endpoints.push({
|
||||
path: match[1],
|
||||
method: match[2].toUpperCase(),
|
||||
});
|
||||
class ComprehensiveAPIAuditor {
|
||||
constructor() {
|
||||
this.apiEndpoints = [];
|
||||
this.currentTools = [];
|
||||
this.missingEndpoints = [];
|
||||
this.parameterMismatches = [];
|
||||
this.newTools = [];
|
||||
this.auditResults = {};
|
||||
}
|
||||
|
||||
return endpoints;
|
||||
}
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* Categorize API endpoints by authentication type
|
||||
*/
|
||||
function categorizeApiEndpoints(apiEndpoints) {
|
||||
const categorized = {
|
||||
PUBLIC: [],
|
||||
PROVIDER: [],
|
||||
PATIENT: [],
|
||||
PARTNER: [],
|
||||
AFFILIATE: [],
|
||||
NETWORK: [],
|
||||
};
|
||||
|
||||
apiEndpoints.forEach((endpoint) => {
|
||||
const path = endpoint.path;
|
||||
const requiresAuth = endpoint.requiresAuth;
|
||||
|
||||
// Categorization logic based on path patterns and authentication
|
||||
if (
|
||||
!requiresAuth ||
|
||||
path.includes("/login") ||
|
||||
path.includes("/register") ||
|
||||
path.includes("/password")
|
||||
) {
|
||||
categorized.PUBLIC.push(endpoint);
|
||||
} else if (
|
||||
path.includes("/emr/") ||
|
||||
path.includes("/api/emr") ||
|
||||
endpoint.tags?.some((tag) =>
|
||||
["Provider", "Medical", "Clinical"].includes(tag)
|
||||
)
|
||||
) {
|
||||
categorized.PROVIDER.push(endpoint);
|
||||
} else if (
|
||||
path.includes("/patient/") ||
|
||||
path.includes("/frontend/") ||
|
||||
endpoint.tags?.some((tag) => ["Patient", "Patient Portal"].includes(tag))
|
||||
) {
|
||||
categorized.PATIENT.push(endpoint);
|
||||
} else if (
|
||||
path.includes("/partner/") ||
|
||||
endpoint.tags?.some((tag) => ["Partner"].includes(tag))
|
||||
) {
|
||||
categorized.PARTNER.push(endpoint);
|
||||
} else if (
|
||||
path.includes("/affiliate/") ||
|
||||
endpoint.tags?.some((tag) => ["Affiliate"].includes(tag))
|
||||
) {
|
||||
categorized.AFFILIATE.push(endpoint);
|
||||
} else if (
|
||||
path.includes("/network/") ||
|
||||
endpoint.tags?.some((tag) => ["Network"].includes(tag))
|
||||
) {
|
||||
categorized.NETWORK.push(endpoint);
|
||||
} else {
|
||||
// Default to PROVIDER for authenticated endpoints
|
||||
categorized.PROVIDER.push(endpoint);
|
||||
}
|
||||
});
|
||||
|
||||
return categorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find missing endpoints by comparing API docs with current implementation
|
||||
*/
|
||||
function findMissingEndpoints(apiEndpoints, currentEndpoints) {
|
||||
const missing = {
|
||||
PUBLIC: [],
|
||||
PROVIDER: [],
|
||||
PATIENT: [],
|
||||
PARTNER: [],
|
||||
AFFILIATE: [],
|
||||
NETWORK: [],
|
||||
};
|
||||
|
||||
Object.keys(apiEndpoints).forEach((authType) => {
|
||||
const apiList = apiEndpoints[authType];
|
||||
const currentList = currentEndpoints[authType] || [];
|
||||
|
||||
apiList.forEach((apiEndpoint) => {
|
||||
const exists = currentList.some(
|
||||
(current) =>
|
||||
current.path === apiEndpoint.path &&
|
||||
current.method === apiEndpoint.method.toUpperCase()
|
||||
console.log(
|
||||
`📊 API Docs Info: ${apiDocs.info.title} v${apiDocs.info.version}`
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
missing[authType].push(apiEndpoint);
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return missing;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate audit report
|
||||
*/
|
||||
function generateAuditReport(apiEndpoints, currentEndpoints, missingEndpoints) {
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
totalApiEndpoints: 0,
|
||||
totalCurrentEndpoints: 0,
|
||||
totalMissingEndpoints: 0,
|
||||
byAuthType: {},
|
||||
},
|
||||
missingEndpoints,
|
||||
recommendations: [],
|
||||
};
|
||||
/**
|
||||
* Extract parameters from OpenAPI method data
|
||||
*/
|
||||
extractParameters(methodData) {
|
||||
const parameters = [];
|
||||
|
||||
// Calculate totals
|
||||
Object.keys(apiEndpoints).forEach((authType) => {
|
||||
const apiCount = apiEndpoints[authType].length;
|
||||
const currentCount = currentEndpoints[authType]?.length || 0;
|
||||
const missingCount = missingEndpoints[authType].length;
|
||||
|
||||
report.summary.totalApiEndpoints += apiCount;
|
||||
report.summary.totalCurrentEndpoints += currentCount;
|
||||
report.summary.totalMissingEndpoints += missingCount;
|
||||
|
||||
report.summary.byAuthType[authType] = {
|
||||
apiEndpoints: apiCount,
|
||||
currentEndpoints: currentCount,
|
||||
missingEndpoints: missingCount,
|
||||
coverage:
|
||||
currentCount > 0
|
||||
? ((currentCount / apiCount) * 100).toFixed(1) + "%"
|
||||
: "0%",
|
||||
};
|
||||
});
|
||||
|
||||
// Generate recommendations
|
||||
Object.keys(missingEndpoints).forEach((authType) => {
|
||||
if (missingEndpoints[authType].length > 0) {
|
||||
report.recommendations.push({
|
||||
authType,
|
||||
action: `Implement ${missingEndpoints[authType].length} missing ${authType} endpoints`,
|
||||
priority:
|
||||
authType === "PROVIDER"
|
||||
? "HIGH"
|
||||
: authType === "PUBLIC"
|
||||
? "MEDIUM"
|
||||
: "LOW",
|
||||
// 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",
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
// Request body parameters
|
||||
if (
|
||||
methodData.requestBody?.content?.["application/json"]?.schema?.properties
|
||||
) {
|
||||
const schema = methodData.requestBody.content["application/json"].schema;
|
||||
const required = schema.required || [];
|
||||
|
||||
/**
|
||||
* Main audit function
|
||||
*/
|
||||
function performAudit() {
|
||||
console.log("🔍 Starting comprehensive API audit...\n");
|
||||
Object.entries(schema.properties).forEach(([propName, propData]) => {
|
||||
parameters.push({
|
||||
name: propName,
|
||||
type: propData.type || "string",
|
||||
required: required.includes(propName),
|
||||
description: propData.description || "",
|
||||
location: "body",
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Load data
|
||||
console.log("📋 Loading API documentation...");
|
||||
const apiDocs = loadApiDocumentation();
|
||||
console.log(`✅ Loaded ${apiDocs.length} API endpoints\n`);
|
||||
return parameters;
|
||||
}
|
||||
|
||||
console.log("📋 Loading current endpoint configuration...");
|
||||
const currentEndpoints = loadCurrentEndpoints();
|
||||
const currentTotal = Object.values(currentEndpoints).reduce(
|
||||
(sum, arr) => sum + arr.length,
|
||||
0
|
||||
);
|
||||
console.log(`✅ Loaded ${currentTotal} current endpoints\n`);
|
||||
|
||||
// Categorize API endpoints
|
||||
console.log("🏷️ Categorizing API endpoints by authentication type...");
|
||||
const categorizedApiEndpoints = categorizeApiEndpoints(apiDocs);
|
||||
console.log("✅ Categorization complete\n");
|
||||
|
||||
// Find missing endpoints
|
||||
console.log("🔍 Identifying missing endpoints...");
|
||||
const missingEndpoints = findMissingEndpoints(
|
||||
categorizedApiEndpoints,
|
||||
currentEndpoints
|
||||
);
|
||||
console.log("✅ Analysis complete\n");
|
||||
|
||||
// Generate report
|
||||
console.log("📊 Generating audit report...");
|
||||
const report = generateAuditReport(
|
||||
categorizedApiEndpoints,
|
||||
currentEndpoints,
|
||||
missingEndpoints
|
||||
);
|
||||
|
||||
// Save report
|
||||
const reportPath = path.join(
|
||||
process.cwd(),
|
||||
"comprehensive-api-audit-report.json"
|
||||
);
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||||
console.log(`✅ Report saved to: ${reportPath}\n`);
|
||||
|
||||
// Display summary
|
||||
console.log("📈 AUDIT SUMMARY:");
|
||||
console.log(`Total API endpoints: ${report.summary.totalApiEndpoints}`);
|
||||
console.log(
|
||||
`Current implementation: ${report.summary.totalCurrentEndpoints}`
|
||||
);
|
||||
console.log(`Missing endpoints: ${report.summary.totalMissingEndpoints}`);
|
||||
console.log(
|
||||
`Overall coverage: ${(
|
||||
(report.summary.totalCurrentEndpoints /
|
||||
report.summary.totalApiEndpoints) *
|
||||
100
|
||||
).toFixed(1)}%\n`
|
||||
);
|
||||
|
||||
// Display by auth type
|
||||
console.log("📊 COVERAGE BY AUTHENTICATION TYPE:");
|
||||
Object.keys(report.summary.byAuthType).forEach((authType) => {
|
||||
const stats = report.summary.byAuthType[authType];
|
||||
console.log(
|
||||
`${authType}: ${stats.currentEndpoints}/${stats.apiEndpoints} (${stats.coverage}) - Missing: ${stats.missingEndpoints}`
|
||||
/**
|
||||
* Check if endpoint requires authentication
|
||||
*/
|
||||
hasAuthRequired(security) {
|
||||
return (
|
||||
security &&
|
||||
security.length > 0 &&
|
||||
security.some((sec) => Object.keys(sec).length > 0)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
console.log("\n🎯 RECOMMENDATIONS:");
|
||||
report.recommendations.forEach((rec) => {
|
||||
console.log(
|
||||
`${
|
||||
rec.priority === "HIGH" ? "🔴" : rec.priority === "MEDIUM" ? "🟡" : "🟢"
|
||||
} ${rec.action} (${rec.priority} priority)`
|
||||
/**
|
||||
* 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*\})/
|
||||
);
|
||||
});
|
||||
|
||||
return report;
|
||||
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 audit if called directly
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
performAudit();
|
||||
}
|
||||
|
||||
// Also run if this is the main module (for Node.js compatibility)
|
||||
if (process.argv[1] && process.argv[1].endsWith("comprehensive-api-audit.js")) {
|
||||
performAudit();
|
||||
}
|
||||
|
||||
export { performAudit };
|
||||
// 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");
|
||||
}
|
||||
});
|
||||
|
Reference in New Issue
Block a user