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
JavaScript
/**
* 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
}
}