validate-tools-sync.js•5.21 kB
#!/usr/bin/env node
/**
* Validates that the tools listed in mcpb-bundle/manifest.json match
* the tools actually provided by the running MCP server
*
* This uses JSON-RPC to query the server directly, avoiding fragile regex parsing.
*/
import { readFile } from 'fs/promises';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { spawn } from 'child_process';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const rootDir = join(__dirname, '..');
// ANSI color codes for pretty output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m'
};
async function extractToolsFromManifest() {
const manifestPath = join(rootDir, 'mcpb-bundle', 'manifest.json');
const content = await readFile(manifestPath, 'utf-8');
const manifest = JSON.parse(content);
return manifest.tools.map(tool => tool.name).sort();
}
async function extractToolsFromServer() {
return new Promise((resolve, reject) => {
// Start the MCP server
const serverPath = join(rootDir, 'dist', 'index.js');
const server = spawn('node', [serverPath], {
stdio: ['pipe', 'pipe', 'pipe']
});
let output = '';
let errorOutput = '';
const messages = [];
server.stdout.on('data', (data) => {
output += data.toString();
// Try to parse each line as JSON-RPC message
const lines = output.split('\n');
output = lines.pop() || ''; // Keep incomplete line
for (const line of lines) {
if (line.trim()) {
try {
messages.push(JSON.parse(line));
} catch (e) {
// Not JSON, might be debug output
}
}
}
});
server.stderr.on('data', (data) => {
errorOutput += data.toString();
});
// Step 1: Send initialize request
const initRequest = {
jsonrpc: '2.0',
id: 1,
method: 'initialize',
params: {
protocolVersion: '2024-11-05',
capabilities: {},
clientInfo: {
name: 'validate-tools-sync',
version: '1.0.0'
}
}
};
server.stdin.write(JSON.stringify(initRequest) + '\n');
// Wait for initialize response, then send tools/list
setTimeout(() => {
// Step 2: Send tools/list request
const toolsRequest = {
jsonrpc: '2.0',
id: 2,
method: 'tools/list',
params: {}
};
server.stdin.write(JSON.stringify(toolsRequest) + '\n');
// Wait for tools/list response
setTimeout(() => {
server.kill();
// Find the tools/list response
const toolsResponse = messages.find(msg => msg.id === 2 && msg.result);
if (!toolsResponse) {
reject(new Error('No tools/list response received'));
return;
}
const tools = toolsResponse.result.tools.map(tool => tool.name).sort();
resolve(tools);
}, 1000);
}, 500);
server.on('error', (error) => {
reject(new Error(`Failed to start server: ${error.message}`));
});
});
}
async function main() {
console.log(`${colors.cyan}🔍 Validating tool synchronization...${colors.reset}\n`);
try {
const manifestTools = await extractToolsFromManifest();
const serverTools = await extractToolsFromServer();
console.log(`${colors.blue}📋 Manifest tools (${manifestTools.length}):${colors.reset}`);
manifestTools.forEach(tool => console.log(` - ${tool}`));
console.log(`\n${colors.blue}⚙️ Server tools (${serverTools.length}):${colors.reset}`);
serverTools.forEach(tool => console.log(` - ${tool}`));
// Find differences
const missingInManifest = serverTools.filter(t => !manifestTools.includes(t));
const missingInServer = manifestTools.filter(t => !serverTools.includes(t));
console.log('\n' + '='.repeat(60));
if (missingInManifest.length === 0 && missingInServer.length === 0) {
console.log(`${colors.green}✅ SUCCESS: All tools are in sync!${colors.reset}`);
console.log(`${colors.green} Both manifest.json and server.ts have ${manifestTools.length} tools.${colors.reset}`);
process.exit(0);
} else {
console.log(`${colors.red}❌ MISMATCH DETECTED!${colors.reset}\n`);
if (missingInManifest.length > 0) {
console.log(`${colors.yellow}⚠️ Tools in server.ts but NOT in manifest.json:${colors.reset}`);
missingInManifest.forEach(tool => console.log(` ${colors.red}✗${colors.reset} ${tool}`));
console.log();
}
if (missingInServer.length > 0) {
console.log(`${colors.yellow}⚠️ Tools in manifest.json but NOT in server.ts:${colors.reset}`);
missingInServer.forEach(tool => console.log(` ${colors.red}✗${colors.reset} ${tool}`));
console.log();
}
console.log(`${colors.red}Please update the files to match!${colors.reset}`);
process.exit(1);
}
} catch (error) {
console.error(`${colors.red}❌ Error:${colors.reset}`, error.message);
process.exit(1);
}
}
main();