UNPKG

mcp-server-tester-sse-http-stdio

Version:

MCP Server Tester with SSE support - Test MCP servers using HTTP, SSE, and STDIO transports

153 lines (152 loc) 6.38 kB
/** * Console formatter that provides rich human-readable test output * Inspired by the old inspector output format */ import { formatConversation } from '../utils/conversationFormatter.js'; export class ConsoleFormatter { options; currentModel; hasShownVersion = false; constructor(options = {}) { this.options = options; } onEvent(event) { switch (event.type) { case 'suite_start': this.handleSuiteStart(event.data); break; case 'section_start': this.handleSectionStart(event.data); break; case 'tool_discovery': this.handleToolDiscovery(event.data); break; case 'progress': this.handleProgress(event.data); break; case 'test_start': this.handleTestStart(event.data); break; case 'test_complete': this.handleTestComplete(event.data); break; case 'suite_complete': this.handleSuiteComplete(event.data); break; } } handleSuiteStart(_data) { // Show version info once at the very beginning if (!this.hasShownVersion) { const version = this.options.version || '1.0.7'; console.log(`\nMCP Server Tester v${version}`); this.hasShownVersion = true; } } handleSectionStart(data) { console.log(`\n${data.title}`); } handleToolDiscovery(data) { const { expectedTools, foundTools, passed } = data; const icon = passed ? '✅' : '❌'; if (passed) { console.log(`${icon} Tool discovery: ${foundTools.length}/${expectedTools.length} expected tools found (${foundTools.join(', ')})`); } else { const missing = expectedTools.filter((tool) => !foundTools.includes(tool)); console.log(`${icon} Tool discovery: ${foundTools.length}/${expectedTools.length} expected tools found (missing: ${missing.join(', ')})`); } } handleProgress(data) { if (data.model && data.model !== this.currentModel) { this.currentModel = data.model; // Model changes are now handled by section headers } else if (data.message) { // Only show progress messages in debug mode if (this.options.debug) { console.log(data.message); } } } handleTestStart(_data) { // For now, we don't show individual test starts // Could add debug mode later that shows "Running: test_name..." } handleTestComplete(data) { const { name, passed, errors, prompt, messages, scorer_results } = data; if (passed) { console.log(`✅ ${name}: PASSED`); } else { console.log(`❌ ${name}: FAILED`); if (prompt) { console.log(` Prompt: "${prompt}"`); } if (errors && errors.length > 0) { errors.forEach((error) => { console.log(` • ${error}`); }); } } // Show conversation in debug mode if (this.options.debug && messages && messages.length > 0) { console.log(); console.log('─'.repeat(60)); console.log('\x1b[36mDEBUG: Conversation\x1b[0m'); console.log(); const formattedLines = formatConversation(messages, prompt); // Remove the top and bottom horizontal lines from formatConversation since we're adding our own const conversationContent = formattedLines.slice(1, -1); conversationContent.forEach(line => console.log(line)); console.log('─'.repeat(60)); console.log(); } // Show scorer results in debug mode if (this.options.debug && scorer_results && scorer_results.length > 0) { console.log('\x1b[36mDEBUG: Scorer Results\x1b[0m'); console.log(); scorer_results.forEach(result => { const status = result.passed ? 'PASSED' : 'FAILED'; if (result.type === 'regex') { console.log(`\x1b[90mREGEX Scorer: ${status}\x1b[0m`); console.log(`\x1b[90mPattern: ${result.details?.replace("Pattern '", '').replace("' found", '').replace("' not found", '')}\x1b[0m`); } else if (result.type === 'llm-judge') { const scoreText = result.score !== undefined ? ` (${result.score}/${result.details?.match(/threshold: ([\d.]+)/)?.[1] || '0.7'})` : ''; console.log(`\x1b[90mLLM Judge: ${status}${scoreText}\x1b[0m`); // Extract criteria and rationale from details const criteriaMatch = result.details?.match(/Criteria: (.+?)\. Rationale:/); const rationaleMatch = result.details?.match(/Rationale: (.+)$/); if (criteriaMatch) { console.log(`\x1b[90mCriteria: ${criteriaMatch[1]}\x1b[0m`); } if (rationaleMatch) { const rationale = rationaleMatch[1]; const truncated = rationale.length > 150 ? `${rationale.substring(0, 150)}...` : rationale; console.log(`\x1b[90mRationale: ${truncated}\x1b[0m`); } } else { console.log(`\x1b[90m${String(result.type).toUpperCase()} Scorer: ${status}\x1b[0m`); if (result.details) { console.log(`\x1b[90mDetails: ${result.details}\x1b[0m`); } } console.log(); }); console.log('─'.repeat(60)); console.log(); } } handleSuiteComplete(data) { const { total, passed, duration } = data; const durationInSeconds = (duration / 1000).toFixed(1); console.log(`\n📊 Results: ${passed}/${total} tests passed (${durationInSeconds}s)`); } flush() { // Console output is synchronous, nothing to flush } }