145 lines
4.7 KiB
JavaScript
145 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Audit all MCP servers for:
|
|
* 1. Missing argument descriptions
|
|
* 2. Missing _meta labels
|
|
* 3. Missing app resource URIs
|
|
* 4. Improper tool naming
|
|
*/
|
|
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import { fileURLToPath } from 'url';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = path.dirname(__filename);
|
|
|
|
const serversDir = path.join(__dirname, 'mcp-servers');
|
|
const results = {
|
|
servers: [],
|
|
totalServers: 0,
|
|
totalTools: 0,
|
|
issues: {
|
|
missingArgDescriptions: 0,
|
|
missingLabels: 0,
|
|
missingAppURI: 0,
|
|
badNaming: 0,
|
|
}
|
|
};
|
|
|
|
// Get all server directories
|
|
const serverDirs = fs.readdirSync(serversDir).filter(dir => {
|
|
const fullPath = path.join(serversDir, dir);
|
|
return fs.statSync(fullPath).isDirectory() && fs.existsSync(path.join(fullPath, 'src/index.ts'));
|
|
});
|
|
|
|
console.log(`\n🔍 Auditing ${serverDirs.length} MCP servers...\n`);
|
|
|
|
for (const serverName of serverDirs) {
|
|
const indexPath = path.join(serversDir, serverName, 'src/index.ts');
|
|
const content = fs.readFileSync(indexPath, 'utf-8');
|
|
|
|
const serverResult = {
|
|
name: serverName,
|
|
tools: [],
|
|
issues: [],
|
|
};
|
|
|
|
// Extract tool definitions using regex (simple approach)
|
|
const toolMatches = content.matchAll(/{\s*name:\s*["']([^"']+)["']\s*,\s*description:\s*["']([^"']+)["']/gs);
|
|
|
|
for (const match of toolMatches) {
|
|
const toolName = match[1];
|
|
const toolDesc = match[2];
|
|
|
|
const tool = {
|
|
name: toolName,
|
|
description: toolDesc,
|
|
issues: [],
|
|
};
|
|
|
|
// Check tool naming convention
|
|
if (!toolName.match(/^[a-z]+_[a-z_]+$/)) {
|
|
tool.issues.push('Bad naming (not snake_case verb_noun)');
|
|
serverResult.issues.push(`Tool "${toolName}": Bad naming`);
|
|
results.issues.badNaming++;
|
|
}
|
|
|
|
// Find the tool's inputSchema section
|
|
const toolStartIndex = content.indexOf(`name: "${toolName}"`);
|
|
const nextToolIndex = content.indexOf('\n {', toolStartIndex + 1);
|
|
const toolSection = content.substring(
|
|
toolStartIndex,
|
|
nextToolIndex === -1 ? content.length : nextToolIndex
|
|
);
|
|
|
|
// Check for missing parameter descriptions
|
|
const paramMatches = toolSection.matchAll(/(\w+):\s*{\s*type:\s*["'](\w+)["']\s*(?:,\s*description:\s*["']([^"']+)["'])?/g);
|
|
let hasMissingDesc = false;
|
|
|
|
for (const paramMatch of paramMatches) {
|
|
const paramName = paramMatch[1];
|
|
const paramType = paramMatch[2];
|
|
const paramDesc = paramMatch[3];
|
|
|
|
// Skip if it's not actually a parameter (could be part of schema structure)
|
|
if (paramName === 'type' || paramName === 'properties' || paramName === 'required') continue;
|
|
|
|
if (!paramDesc && paramType !== 'object') {
|
|
tool.issues.push(`Parameter "${paramName}" missing description`);
|
|
hasMissingDesc = true;
|
|
}
|
|
}
|
|
|
|
if (hasMissingDesc) {
|
|
serverResult.issues.push(`Tool "${toolName}": Missing argument descriptions`);
|
|
results.issues.missingArgDescriptions++;
|
|
}
|
|
|
|
// Check for _meta labels
|
|
if (!toolSection.includes('_meta')) {
|
|
tool.issues.push('Missing _meta labels');
|
|
serverResult.issues.push(`Tool "${toolName}": Missing _meta labels`);
|
|
results.issues.missingLabels++;
|
|
}
|
|
|
|
// Check for app tools missing resourceUri
|
|
if (toolName.startsWith('view_') || toolName.startsWith('show_')) {
|
|
if (!toolSection.includes('resourceUri')) {
|
|
tool.issues.push('App tool missing resourceUri');
|
|
serverResult.issues.push(`Tool "${toolName}": App tool missing resourceUri`);
|
|
results.issues.missingAppURI++;
|
|
}
|
|
}
|
|
|
|
serverResult.tools.push(tool);
|
|
}
|
|
|
|
results.servers.push(serverResult);
|
|
results.totalServers++;
|
|
results.totalTools += serverResult.tools.length;
|
|
|
|
// Print server summary
|
|
if (serverResult.issues.length > 0) {
|
|
console.log(`❌ ${serverName} (${serverResult.tools.length} tools, ${serverResult.issues.length} issues)`);
|
|
serverResult.issues.forEach(issue => console.log(` - ${issue}`));
|
|
} else {
|
|
console.log(`✅ ${serverName} (${serverResult.tools.length} tools)`);
|
|
}
|
|
}
|
|
|
|
// Summary
|
|
console.log(`\n📊 Summary:`);
|
|
console.log(` Total servers: ${results.totalServers}`);
|
|
console.log(` Total tools: ${results.totalTools}`);
|
|
console.log(`\n⚠️ Issues found:`);
|
|
console.log(` Missing argument descriptions: ${results.issues.missingArgDescriptions}`);
|
|
console.log(` Missing _meta labels: ${results.issues.missingLabels}`);
|
|
console.log(` App tools missing resourceUri: ${results.issues.missingAppURI}`);
|
|
console.log(` Bad tool naming: ${results.issues.badNaming}`);
|
|
|
|
// Write detailed report
|
|
const reportPath = path.join(__dirname, 'audit-report.json');
|
|
fs.writeFileSync(reportPath, JSON.stringify(results, null, 2));
|
|
console.log(`\n📄 Detailed report written to: audit-report.json\n`);
|