#!/usr/bin/env node
import { spawn } from 'child_process';
import { TEST_CONFIG } from './config.js';
const COLORS = {
reset: '\x1b[0m',
green: '\x1b[32m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m'
};
class MCPTester {
results = [];
serverPath;
constructor(serverPath) {
this.serverPath = serverPath;
}
async runCommand(args) {
return new Promise((resolve) => {
const startTime = Date.now();
const proc = spawn('npx', ['@modelcontextprotocol/inspector', '--cli', 'node', this.serverPath, ...args], {
stdio: 'pipe'
});
let output = '';
let errorOutput = '';
proc.stdout?.on('data', (data) => {
output += data.toString();
});
proc.stderr?.on('data', (data) => {
errorOutput += data.toString();
});
proc.on('close', (code) => {
const duration = Date.now() - startTime;
if (code === 0) {
resolve({ success: true, output, duration });
}
else {
resolve({ success: false, output: errorOutput || output, duration });
}
});
proc.on('error', (err) => {
const duration = Date.now() - startTime;
resolve({ success: false, output: '', error: err.message, duration });
});
});
}
async runTest(name, args, validate) {
process.stdout.write(`${COLORS.blue}[TEST]${COLORS.reset} ${name}... `);
const startTime = Date.now();
try {
const result = await this.runCommand(args);
if (!result.success) {
console.log(`${COLORS.red}FAILED${COLORS.reset}`);
this.results.push({ name, success: false, error: result.output, duration: Date.now() - startTime });
return;
}
if (!validate(result.output)) {
console.log(`${COLORS.red}FAILED (validation)${COLORS.reset}`);
this.results.push({ name, success: false, error: 'Validation failed', duration: Date.now() - startTime });
return;
}
console.log(`${COLORS.green}PASSED${COLORS.reset} (${Date.now() - startTime}ms)`);
this.results.push({ name, success: true, duration: Date.now() - startTime });
}
catch (error) {
console.log(`${COLORS.red}ERROR${COLORS.reset}`);
this.results.push({
name,
success: false,
error: error instanceof Error ? error.message : String(error),
duration: Date.now() - startTime
});
}
}
async testToolsList() {
await this.runTest('List all tools', ['--method', 'tools/list'], (output) => {
const tools = JSON.parse(output);
const toolNames = tools.tools?.map((t) => t.name) || [];
const expectedTools = [
'crawl_fetch_markdown',
'crawl_fetch',
'crawl_read',
'crawl_read_batch',
'search_searx',
'launch_chrome_cdp',
'connect_cdp',
'launch_local',
'chrome_status',
'close',
'shutdown_chrome_cdp'
];
return expectedTools.every(tool => toolNames.includes(tool));
});
}
async testCrawlRead() {
await this.runTest('crawl_read single URL', ['--method', 'tools/call', '--tool-name', 'crawl_read', '--tool-arg', `url=${TEST_CONFIG.testUrls.valid[0]}`], (output) => {
const result = JSON.parse(output);
return result.content && result.content[0] && result.content[0].text.includes('#');
});
}
async testCrawlReadBatch() {
await this.runTest('crawl_read_batch multiple URLs', ['--method', 'tools/call', '--tool-name', 'crawl_read_batch', '--tool-arg', `urls=${JSON.stringify(TEST_CONFIG.batchUrls.small)}`], (output) => {
const result = JSON.parse(output);
return result.content && result.content[0] && result.content[0].text.length > 50;
});
}
async testCrawlFetchMarkdown() {
await this.runTest('crawl_fetch_markdown', ['--method', 'tools/call', '--tool-name', 'crawl_fetch_markdown', '--tool-arg', `url=${TEST_CONFIG.testUrls.valid[0]}`], (output) => {
const result = JSON.parse(output);
return result.content && result.content[0] && result.content[0].text.includes('#');
});
}
async testCrawlFetch() {
await this.runTest('crawl_fetch with options', ['--method', 'tools/call', '--tool-name', 'crawl_fetch', '--tool-arg', `url=${TEST_CONFIG.testUrls.valid[1]}`, '--tool-arg', 'options={"pages": 1, "maxResults": 5}'], (output) => {
const result = JSON.parse(output);
return result.content && result.content[0] && result.content[0].text.includes('#');
});
}
async testSearchSearX() {
await this.runTest('search_searx basic query', ['--method', 'tools/call', '--tool-name', 'search_searx', '--tool-arg', `query=${TEST_CONFIG.searchQueries[0]}`, '--tool-arg', 'maxResults=5'], (output) => {
const result = JSON.parse(output);
return result.content && result.content[0];
});
}
async testSearchWithCategory() {
await this.runTest('search_searx with category', ['--method', 'tools/call', '--tool-name', 'search_searx', '--tool-arg', 'query=javascript', '--tool-arg', 'category=it', '--tool-arg', 'maxResults=3'], (output) => {
const result = JSON.parse(output);
return result.content && result.content[0];
});
}
async testInvalidUrl() {
await this.runTest('crawl_read with invalid URL (error handling)', ['--method', 'tools/call', '--tool-name', 'crawl_read', '--tool-arg', `url=${TEST_CONFIG.testUrls.invalid[0]}`], (output) => {
const result = JSON.parse(output);
return result.content && (result.content[0].text.includes('Error') ||
result.content[0].text.includes('Failed') ||
result.structuredContent?.error);
});
}
async runAllTests() {
console.log(`${COLORS.cyan}╔═══════════════════════════════════════╗${COLORS.reset}`);
console.log(`${COLORS.cyan}║ MCP Server Test Suite ║${COLORS.reset}`);
console.log(`${COLORS.cyan}╚═══════════════════════════════════════╝${COLORS.reset}\n`);
await this.testToolsList();
await this.testCrawlRead();
await this.testCrawlReadBatch();
await this.testCrawlFetchMarkdown();
await this.testCrawlFetch();
await this.testSearchSearX();
await this.testSearchWithCategory();
await this.testInvalidUrl();
this.printSummary();
}
printSummary() {
console.log(`\n${COLORS.cyan}╔═══════════════════════════════════════╗${COLORS.reset}`);
console.log(`${COLORS.cyan}║ Test Summary ║${COLORS.reset}`);
console.log(`${COLORS.cyan}╚═══════════════════════════════════════╝${COLORS.reset}\n`);
const passed = this.results.filter(r => r.success).length;
const failed = this.results.filter(r => !r.success).length;
const total = this.results.length;
console.log(`Total Tests: ${total}`);
console.log(`${COLORS.green}Passed: ${passed}${COLORS.reset}`);
console.log(`${COLORS.red}Failed: ${failed}${COLORS.reset}`);
console.log(`Success Rate: ${((passed / total) * 100).toFixed(1)}%\n`);
if (failed > 0) {
console.log(`${COLORS.red}Failed Tests:${COLORS.reset}`);
this.results
.filter(r => !r.success)
.forEach(r => {
console.log(` - ${r.name}`);
if (r.error) {
console.log(` Error: ${r.error.substring(0, 100)}...`);
}
});
console.log();
}
const totalDuration = this.results.reduce((sum, r) => sum + r.duration, 0);
console.log(`Total Duration: ${(totalDuration / 1000).toFixed(2)}s`);
process.exit(failed > 0 ? 1 : 0);
}
}
async function main() {
const serverPath = process.argv[2] || TEST_CONFIG.serverPath;
const tester = new MCPTester(serverPath);
await tester.runAllTests();
}
main().catch((error) => {
console.error(`${COLORS.red}Fatal error:${COLORS.reset}`, error);
process.exit(1);
});