UNPKG

@robertdouglass/mcp-tester

Version:

Advanced testing framework for Model Context Protocol (MCP) servers with enhanced error handling, performance testing, and comprehensive tool validation

1,239 lines (1,085 loc) 38.3 kB
#!/usr/bin/env node const { Client } = require('@modelcontextprotocol/sdk/client/index.js'); const { StdioClientTransport } = require('@modelcontextprotocol/sdk/client/stdio.js'); const { SSEClientTransport } = require('@modelcontextprotocol/sdk/client/sse.js'); const { StreamableHTTPClientTransport } = require('@modelcontextprotocol/sdk/client/streamableHttp.js'); const fs = require('fs').promises; const path = require('path'); // Custom error classes for better error handling class MCPTestError extends Error { constructor(message, code, details = {}) { super(message); this.name = 'MCPTestError'; this.code = code; this.details = details; } } class ConnectionError extends MCPTestError { constructor(message, transport, details) { super(message, 'CONNECTION_ERROR', { transport, ...details }); this.name = 'ConnectionError'; } } class TestTimeoutError extends MCPTestError { constructor(testName, timeout) { super(`Test "${testName}" timed out after ${timeout}ms`, 'TEST_TIMEOUT', { testName, timeout }); this.name = 'TestTimeoutError'; } } /** * Advanced MCP Test Framework with enhanced capabilities */ class MCPTestFrameworkAdvanced { constructor(options = {}) { this.options = { verbose: options.verbose || false, timeout: options.timeout || 30000, outputDir: options.outputDir || './test-results', retryAttempts: options.retryAttempts || 0, retryDelay: options.retryDelay || 1000, validateSchemas: options.validateSchemas !== false, performanceThresholds: options.performanceThresholds || { toolCall: 5000, // ms discovery: 1000, // ms } }; this.results = []; this.metrics = { startTime: Date.now(), connectionAttempts: 0, totalTestsRun: 0, totalAssertions: 0, }; } log(message, level = 'info') { if (this.options.verbose || level === 'error' || level === 'warning') { const timestamp = new Date().toISOString(); const levelEmoji = { 'info': 'ℹ️', 'success': '✅', 'warning': '⚠️', 'error': '❌', 'debug': '🔍' }; console.log(`[${timestamp}] ${levelEmoji[level] || ''} ${message}`); } } /** * Create transport based on type with enhanced error handling */ async createTransport(transportConfig) { const { type, ...config } = transportConfig; this.log(`Creating ${type} transport`, 'debug'); switch (type) { case 'stdio': if (!config.command) { throw new MCPTestError('stdio transport requires "command" parameter', 'INVALID_CONFIG'); } return new StdioClientTransport({ command: config.command, args: config.args || [], env: config.env }); case 'sse': if (!config.url) { throw new MCPTestError('SSE transport requires "url" parameter', 'INVALID_CONFIG'); } return new SSEClientTransport(config.url, { requestInit: { headers: config.headers || {} } }); case 'streamableHttp': if (!config.url) { throw new MCPTestError('streamableHttp transport requires "url" parameter', 'INVALID_CONFIG'); } return new StreamableHTTPClientTransport(config.url, { requestInit: { headers: config.headers || {} } }); default: throw new MCPTestError(`Unknown transport type: ${type}`, 'INVALID_TRANSPORT'); } } /** * Connect to server with retry logic */ async connectToServer(transportConfig) { const transportType = transportConfig.type || 'stdio'; let lastError = null; for (let attempt = 0; attempt <= this.options.retryAttempts; attempt++) { this.metrics.connectionAttempts++; if (attempt > 0) { this.log(`Retrying connection (attempt ${attempt + 1}/${this.options.retryAttempts + 1})`, 'warning'); await new Promise(resolve => setTimeout(resolve, this.options.retryDelay)); } try { this.log(`Connecting to MCP server using ${transportType} transport`, 'info'); const transport = await this.createTransport(transportConfig); const client = new Client({ name: 'mcp-test-framework-advanced', version: '2.0.0', }, { capabilities: {} }); await client.connect(transport); this.log(`Successfully connected via ${transportType}`, 'success'); // Verify connection by calling a basic method try { await client.listTools(); } catch (verifyError) { throw new ConnectionError( 'Connection established but server not responding correctly', transportType, { verifyError: verifyError.message } ); } return { client, transport }; } catch (error) { lastError = error; this.log(`Connection attempt ${attempt + 1} failed: ${error.message}`, 'error'); } } throw new ConnectionError( `Failed to connect after ${this.options.retryAttempts + 1} attempts`, transportType, { lastError: lastError?.message } ); } /** * Enhanced test server method with comprehensive test suite */ async testServer(transportConfig, tests = {}) { const result = { name: tests.name || 'MCP Server Test', transport: transportConfig.type, config: this.sanitizeConfig(transportConfig), startTime: new Date().toISOString(), tests: [], metrics: {} }; let client, transport; try { const connection = await this.connectToServer(transportConfig); client = connection.client; transport = connection.transport; // Run discovery tests by default if (tests.testDiscovery !== false) { await this.runDiscoveryTests(client, result); } // Run stability tests by default if (tests.testStability !== false) { await this.runStabilityTests(client, result); } // Run protocol compliance tests if (tests.testProtocolCompliance) { await this.runProtocolComplianceTests(client, result); } // Run performance tests if (tests.testPerformance) { await this.runPerformanceTests(client, result); } // Run error handling tests if (tests.testErrorHandling) { await this.runErrorHandlingTests(client, result); } // Run custom tests if (tests.customTests) { for (const test of tests.customTests) { await this.runCustomTest(client, test, result); } } // Run tool tests with enhanced validation if (tests.toolTests) { for (const toolTest of tests.toolTests) { await this.runEnhancedToolTest(client, toolTest, result); } } // Run transport-specific tests if (tests.transportTests?.[transportConfig.type]) { for (const test of tests.transportTests[transportConfig.type]) { await this.runCustomTest(client, test, result); } } result.status = 'passed'; } catch (error) { result.status = 'failed'; result.error = { message: error.message, code: error.code, details: error.details }; this.log(`Test suite failed: ${error.message}`, 'error'); } finally { if (client) { try { await client.close(); } catch (closeError) { this.log(`Error closing client: ${closeError.message}`, 'warning'); } } result.endTime = new Date().toISOString(); result.duration = Date.now() - new Date(result.startTime).getTime(); result.metrics = this.calculateMetrics(result); this.results.push(result); } return result; } /** * Calculate comprehensive metrics for test results */ calculateMetrics(result) { const metrics = { totalTests: result.tests.length, passedTests: result.tests.filter(t => t.status === 'passed').length, failedTests: result.tests.filter(t => t.status === 'failed').length, avgTestDuration: 0, slowestTest: null, fastestTest: null, }; if (result.tests.length > 0) { const durations = result.tests.map(t => t.duration).filter(d => d != null); metrics.avgTestDuration = Math.round(durations.reduce((a, b) => a + b, 0) / durations.length); const sortedTests = result.tests .filter(t => t.duration != null) .sort((a, b) => a.duration - b.duration); if (sortedTests.length > 0) { metrics.fastestTest = { name: sortedTests[0].name, duration: sortedTests[0].duration }; metrics.slowestTest = { name: sortedTests[sortedTests.length - 1].name, duration: sortedTests[sortedTests.length - 1].duration }; } } return metrics; } /** * Enhanced tool test with schema validation and performance checks */ async runEnhancedToolTest(client, toolTest, result) { const testResult = await this.executeTest( `Tool Test: ${toolTest.toolName}`, async () => { // First, verify tool exists const tools = await client.listTools(); const tool = tools.tools.find(t => t.name === toolTest.toolName); if (!tool) { throw new MCPTestError( `Tool "${toolTest.toolName}" not found`, 'TOOL_NOT_FOUND', { availableTools: tools.tools.map(t => t.name) } ); } // Validate input schema if provided if (this.options.validateSchemas && tool.inputSchema) { const validationResult = this.validateAgainstSchema( toolTest.arguments, tool.inputSchema ); if (!validationResult.valid) { const errorMessage = [ 'Invalid tool arguments:', ...validationResult.errors, '', 'Suggestions:', ...validationResult.suggestions ].join('\n '); throw new MCPTestError( errorMessage, 'SCHEMA_VALIDATION_FAILED', { errors: validationResult.errors, suggestions: validationResult.suggestions } ); } } // Execute tool call with performance tracking const startTime = Date.now(); let callResult; try { callResult = await client.callTool({ name: toolTest.toolName, arguments: toolTest.arguments || {} }); } catch (toolError) { throw new MCPTestError( `Tool execution failed: ${toolError.message}`, 'TOOL_EXECUTION_FAILED', { tool: toolTest.toolName, arguments: toolTest.arguments, error: toolError.message } ); } const callDuration = Date.now() - startTime; // Check performance threshold if (callDuration > this.options.performanceThresholds.toolCall) { this.log( `Tool ${toolTest.toolName} exceeded performance threshold: ${callDuration}ms > ${this.options.performanceThresholds.toolCall}ms`, 'warning' ); } // Validate response structure if (!callResult || !Array.isArray(callResult.content)) { throw new MCPTestError( 'Invalid tool response structure', 'INVALID_RESPONSE', { response: callResult } ); } // Run custom assertions const assertionResults = []; if (toolTest.assertions) { for (let i = 0; i < toolTest.assertions.length; i++) { this.metrics.totalAssertions++; try { await toolTest.assertions[i](callResult); assertionResults.push({ index: i, status: 'passed' }); } catch (assertError) { assertionResults.push({ index: i, status: 'failed', error: assertError.message }); throw new MCPTestError( `Assertion ${i + 1} failed: ${assertError.message}`, 'ASSERTION_FAILED', { assertionIndex: i, tool: toolTest.toolName } ); } } } return { tool: toolTest.toolName, arguments: toolTest.arguments, duration: callDuration, performance: { withinThreshold: callDuration <= this.options.performanceThresholds.toolCall, threshold: this.options.performanceThresholds.toolCall }, responseSize: JSON.stringify(callResult).length, contentTypes: callResult.content.map(c => c.type), assertionResults, resultPreview: this.generateResultPreview(callResult) }; } ); result.tests.push(testResult); } /** * Generate a safe preview of tool results */ generateResultPreview(callResult) { if (!callResult.content || callResult.content.length === 0) { return 'No content'; } const firstContent = callResult.content[0]; if (firstContent.type === 'text') { const text = firstContent.text || ''; return text.length > 100 ? text.substring(0, 100) + '...' : text; } return `${firstContent.type} content (${callResult.content.length} items)`; } /** * Enhanced schema validation with user-friendly error messages */ validateAgainstSchema(data, schema) { const errors = []; const suggestions = []; // Check required fields if (schema.required) { for (const field of schema.required) { if (!(field in data)) { errors.push(`Missing required field: ${field}`); // Add suggestion based on schema if (schema.properties && schema.properties[field]) { const prop = schema.properties[field]; let suggestion = `Add "${field}": `; if (prop.enum) { suggestion += `one of [${prop.enum.map(v => `"${v}"`).join(', ')}]`; } else if (prop.type === 'string') { suggestion += `"your-value"`; } else if (prop.type === 'number') { suggestion += `123`; } else if (prop.type === 'boolean') { suggestion += `true`; } else { suggestion += `<${prop.type}>`; } if (prop.description) { suggestion += ` // ${prop.description}`; } suggestions.push(suggestion); } } } } // Check enum values if (schema.properties) { for (const [fieldName, fieldSchema] of Object.entries(schema.properties)) { if (fieldName in data && fieldSchema.enum) { const value = data[fieldName]; if (!fieldSchema.enum.includes(value)) { errors.push(`Invalid value for ${fieldName}: "${value}"`); suggestions.push(`Use one of: ${fieldSchema.enum.map(v => `"${v}"`).join(', ')}`); } } } } return { valid: errors.length === 0, errors, suggestions }; } /** * Run protocol compliance tests */ async runProtocolComplianceTests(client, result) { const complianceTests = [ { name: 'Valid JSON-RPC responses', fn: async () => { // Test that server returns proper JSON-RPC formatted responses const tools = await client.listTools(); if (!Array.isArray(tools.tools)) { throw new Error('Invalid tools response format'); } return { compliant: true }; } }, { name: 'Error handling compliance', fn: async () => { // Test that server returns proper error codes try { await client.callTool({ name: '__invalid_tool__', arguments: {} }); throw new Error('Server should have returned an error'); } catch (error) { // Expected error - check it has proper structure return { compliant: true, errorHandled: true, errorMessage: error.message }; } } } ]; for (const test of complianceTests) { await this.runCustomTest(client, test, result); } } /** * Run performance tests */ async runPerformanceTests(client, result) { const perfTests = [ { name: 'Tool discovery performance', fn: async () => { const iterations = 10; const times = []; for (let i = 0; i < iterations; i++) { const start = Date.now(); await client.listTools(); times.push(Date.now() - start); } const avgTime = times.reduce((a, b) => a + b, 0) / times.length; const maxTime = Math.max(...times); const minTime = Math.min(...times); return { iterations, avgTime: Math.round(avgTime), maxTime, minTime, withinThreshold: avgTime <= this.options.performanceThresholds.discovery }; } }, { name: 'Concurrent request handling', fn: async () => { const concurrentRequests = 10; const start = Date.now(); const promises = Array(concurrentRequests).fill(null).map(() => client.listTools() ); await Promise.all(promises); const totalTime = Date.now() - start; return { concurrentRequests, totalTime, avgTimePerRequest: Math.round(totalTime / concurrentRequests) }; } } ]; for (const test of perfTests) { await this.runCustomTest(client, test, result); } } /** * Run error handling tests */ async runErrorHandlingTests(client, result) { const errorTests = [ { name: 'Invalid tool name handling', fn: async () => { try { await client.callTool({ name: 'this_tool_definitely_does_not_exist_123456789', arguments: {} }); return { handled: false, error: 'Server did not reject invalid tool' }; } catch (error) { return { handled: true, errorMessage: error.message }; } } }, { name: 'Malformed arguments handling', fn: async () => { const tools = await client.listTools(); if (tools.tools.length === 0) { return { skipped: true, reason: 'No tools available to test' }; } const firstTool = tools.tools[0]; try { // Send invalid arguments (circular reference) const circularObj = { a: 1 }; circularObj.self = circularObj; await client.callTool({ name: firstTool.name, arguments: { invalid: circularObj } }); return { handled: false }; } catch (error) { return { handled: true, errorType: error.name }; } } } ]; for (const test of errorTests) { await this.runCustomTest(client, test, result); } } /** * Enhanced discovery tests with detailed information */ async runDiscoveryTests(client, result) { const discoveryTests = [ { name: 'List Tools', fn: async () => { const start = Date.now(); const tools = await client.listTools(); const duration = Date.now() - start; const toolsSummary = tools.tools.map(t => ({ name: t.name, hasDescription: !!t.description, hasInputSchema: !!t.inputSchema, })); return { count: tools.tools.length, duration, tools: toolsSummary.slice(0, 10), // First 10 for summary schemaCompleteness: toolsSummary.filter(t => t.hasInputSchema).length / toolsSummary.length }; } }, { name: 'List Resources', fn: async () => { const start = Date.now(); const resources = await client.listResources(); const duration = Date.now() - start; return { count: resources.resources.length, duration, resources: resources.resources.slice(0, 10).map(r => ({ name: r.name, uri: r.uri, mimeType: r.mimeType })) }; } }, { name: 'List Prompts', fn: async () => { const start = Date.now(); const prompts = await client.listPrompts(); const duration = Date.now() - start; return { count: prompts.prompts.length, duration, prompts: prompts.prompts.slice(0, 10).map(p => ({ name: p.name, hasDescription: !!p.description, argumentCount: p.arguments?.length || 0 })) }; } } ]; for (const test of discoveryTests) { const testResult = await this.executeTest(test.name, test.fn); result.tests.push(testResult); } } /** * Enhanced stability tests */ async runStabilityTests(client, result) { const stabilityTests = [ { name: 'Rapid Sequential Requests', fn: async () => { const requests = 20; // Increased from 10 const results = []; const startTotal = Date.now(); for (let i = 0; i < requests; i++) { const start = Date.now(); await client.listTools(); results.push(Date.now() - start); } const totalDuration = Date.now() - startTotal; const avgTime = results.reduce((a, b) => a + b, 0) / results.length; const variance = this.calculateVariance(results); return { requests, totalDuration, avgTime: Math.round(avgTime), minTime: Math.min(...results), maxTime: Math.max(...results), variance: Math.round(variance), stable: variance < 100 // Low variance indicates stability }; } }, { name: 'Concurrent Requests', fn: async () => { const requests = 10; // Increased from 5 const start = Date.now(); const promises = []; for (let i = 0; i < requests; i++) { promises.push(client.listTools()); } const results = await Promise.allSettled(promises); const duration = Date.now() - start; const successful = results.filter(r => r.status === 'fulfilled').length; const failed = results.filter(r => r.status === 'rejected').length; return { requests, successful, failed, duration, avgTime: Math.round(duration / requests), successRate: (successful / requests) * 100 }; } }, { name: 'Memory Stability', fn: async () => { // Test that repeated calls don't cause memory issues const iterations = 50; const memoryUsage = []; for (let i = 0; i < iterations; i++) { if (i % 10 === 0) { memoryUsage.push(process.memoryUsage().heapUsed); } await client.listTools(); } // Check if memory usage is stable (not continuously increasing) const memoryIncrease = memoryUsage[memoryUsage.length - 1] - memoryUsage[0]; const avgIncrease = memoryIncrease / memoryUsage.length; return { iterations, memoryStable: avgIncrease < 1000000, // Less than 1MB average increase totalMemoryIncrease: Math.round(memoryIncrease / 1024 / 1024) + 'MB' }; } } ]; for (const test of stabilityTests) { const testResult = await this.executeTest(test.name, test.fn); result.tests.push(testResult); } } /** * Calculate variance for stability metrics */ calculateVariance(numbers) { const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length; const squaredDiffs = numbers.map(n => Math.pow(n - mean, 2)); return squaredDiffs.reduce((a, b) => a + b, 0) / numbers.length; } /** * Execute a single test with enhanced error handling and timeout */ async executeTest(name, fn) { const startTime = Date.now(); const test = { name, startTime: new Date().toISOString() }; this.metrics.totalTestsRun++; try { this.log(`Running test: ${name}`, 'info'); // Create a timeout promise const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(new TestTimeoutError(name, this.options.timeout)), this.options.timeout); }); // Race between test execution and timeout test.result = await Promise.race([fn(), timeoutPromise]); test.status = 'passed'; test.duration = Date.now() - startTime; this.log(`✓ ${name} (${test.duration}ms)`, 'success'); } catch (error) { test.status = 'failed'; test.error = error.message; test.errorCode = error.code; test.errorDetails = error.details; test.duration = Date.now() - startTime; this.log(`✗ ${name}: ${error.message}`, 'error'); if (error.details) { this.log(` Details: ${JSON.stringify(error.details)}`, 'debug'); } } return test; } /** * Sanitize configuration for logging (remove sensitive data) */ sanitizeConfig(config) { const sanitized = { ...config }; // Remove sensitive headers if (sanitized.headers) { const safeHeaders = {}; for (const [key, value] of Object.entries(sanitized.headers)) { if (key.toLowerCase().includes('auth') || key.toLowerCase().includes('token') || key.toLowerCase().includes('key')) { safeHeaders[key] = '[REDACTED]'; } else { safeHeaders[key] = value; } } sanitized.headers = safeHeaders; } // Remove auth tokens if (sanitized.auth) { sanitized.auth = '[REDACTED]'; } return sanitized; } /** * Run custom test */ async runCustomTest(client, test, result) { const testResult = await this.executeTest(test.name, async () => { return await test.fn(client); }); result.tests.push(testResult); } /** * Generate comprehensive report */ async generateReport() { const report = { timestamp: new Date().toISOString(), version: '2.0.0', duration: Date.now() - this.metrics.startTime, metrics: { connectionAttempts: this.metrics.connectionAttempts, totalTestsRun: this.metrics.totalTestsRun, totalAssertions: this.metrics.totalAssertions, }, summary: { total: this.results.length, passed: this.results.filter(r => r.status === 'passed').length, failed: this.results.filter(r => r.status === 'failed').length, byTransport: this.getTransportSummary(), totalTests: this.results.reduce((acc, r) => acc + (r.tests?.length || 0), 0), passedTests: this.results.reduce((acc, r) => acc + (r.tests?.filter(t => t.status === 'passed').length || 0), 0 ), failedTests: this.results.reduce((acc, r) => acc + (r.tests?.filter(t => t.status === 'failed').length || 0), 0 ) }, results: this.results, recommendations: this.generateRecommendations() }; // Save to file try { await fs.mkdir(this.options.outputDir, { recursive: true }); const filename = `mcp-test-report-${Date.now()}.json`; const filepath = path.join(this.options.outputDir, filename); await fs.writeFile(filepath, JSON.stringify(report, null, 2)); this.log(`Report saved to: ${filepath}`, 'success'); } catch (error) { this.log(`Failed to save report: ${error.message}`, 'error'); } return report; } /** * Generate recommendations based on test results */ generateRecommendations() { const recommendations = []; // Analyze performance const slowTests = []; this.results.forEach(result => { result.tests?.forEach(test => { if (test.duration > 1000) { slowTests.push({ name: test.name, duration: test.duration }); } }); }); if (slowTests.length > 0) { recommendations.push({ type: 'performance', severity: 'warning', message: `${slowTests.length} tests took longer than 1 second`, details: slowTests }); } // Check for missing capabilities const hasTools = this.results.some(r => r.tests?.some(t => t.name === 'List Tools' && t.result?.count > 0) ); const hasResources = this.results.some(r => r.tests?.some(t => t.name === 'List Resources' && t.result?.count > 0) ); const hasPrompts = this.results.some(r => r.tests?.some(t => t.name === 'List Prompts' && t.result?.count > 0) ); if (!hasTools && !hasResources && !hasPrompts) { recommendations.push({ type: 'capability', severity: 'info', message: 'Server does not expose any tools, resources, or prompts', suggestion: 'Consider implementing at least one capability' }); } // Check stability const unstableTests = this.results.flatMap(r => r.tests?.filter(t => t.name === 'Rapid Sequential Requests' && t.result?.variance > 100 ) || [] ); if (unstableTests.length > 0) { recommendations.push({ type: 'stability', severity: 'warning', message: 'High variance detected in response times', suggestion: 'Server may have performance consistency issues' }); } return recommendations; } /** * Get transport summary for report */ getTransportSummary() { const summary = {}; for (const result of this.results) { const transport = result.transport; if (!summary[transport]) { summary[transport] = { total: 0, passed: 0, failed: 0 }; } summary[transport].total++; if (result.status === 'passed') { summary[transport].passed++; } else { summary[transport].failed++; } } return summary; } /** * Print formatted summary */ printSummary(report) { console.log('\n' + '='.repeat(50)); console.log('📊 MCP TEST REPORT SUMMARY'); console.log('='.repeat(50)); console.log(`\n⏱️ Total Duration: ${Math.round(report.duration / 1000)}s`); console.log(`🔌 Connection Attempts: ${report.metrics.connectionAttempts}`); console.log(`🧪 Total Tests Run: ${report.metrics.totalTestsRun}`); console.log(`✅ Total Assertions: ${report.metrics.totalAssertions}`); console.log(`\n📈 Test Results:`); console.log(` Server Test Suites: ${report.summary.total}`); console.log(` ✅ Passed: ${report.summary.passed}`); console.log(` ❌ Failed: ${report.summary.failed}`); console.log(`\n📋 Individual Tests:`); console.log(` Total: ${report.summary.totalTests}`); console.log(` ✅ Passed: ${report.summary.passedTests}`); console.log(` ❌ Failed: ${report.summary.failedTests}`); if (Object.keys(report.summary.byTransport).length > 0) { console.log(`\n🚀 By Transport:`); for (const [transport, stats] of Object.entries(report.summary.byTransport)) { console.log(` ${transport}: ${stats.passed}/${stats.total} passed`); } } if (report.recommendations && report.recommendations.length > 0) { console.log(`\n💡 Recommendations:`); report.recommendations.forEach(rec => { const icon = rec.severity === 'error' ? '❌' : rec.severity === 'warning' ? '⚠️' : 'ℹ️'; console.log(` ${icon} ${rec.message}`); if (rec.suggestion) { console.log(` → ${rec.suggestion}`); } }); } console.log('\n' + '='.repeat(50)); } /** * Test multiple server configurations */ async testServerMultiTransport(configs, tests) { console.log(`\n🔄 Testing ${configs.length} transport configurations...\n`); for (const config of configs) { await this.testServer(config, tests); } return this.generateReport(); } } // Export for use as a library module.exports = { MCPTestFrameworkAdvanced }; // CLI interface (backward compatible) if (require.main === module) { const args = process.argv.slice(2); if (args.length < 1 || args.includes('--help') || args.includes('-h')) { console.log(` 🧪 MCP Test Framework v2.1.1 USAGE: mcp-test-framework-advanced <transport-type> <config...> [options] TRANSPORT TYPES: auto <url> Auto-detect transport type stdio <command> [args...] Test stdio transport sse <url> [options] Test SSE transport streamableHttp <url> [options] Test StreamableHTTP transport OPTIONS: --verbose Show detailed output --timeout <ms> Set test timeout (default: 30000) --retry <attempts> Number of connection retries (default: 0) --performance Run performance tests --compliance Run protocol compliance tests --error-handling Run error handling tests --header "Key: Value" Add HTTP header --auth "token" Add auth token EXAMPLES: mcp-test-framework-advanced auto http://localhost:3000/mcp --verbose mcp-test-framework-advanced stdio node ./server.js --performance mcp-test-framework-advanced sse http://localhost:3000 --header "Authorization: Bearer xyz" OUTPUT: Test results are saved to ./test-results/ as JSON files `); process.exit(0); } const verbose = args.includes('--verbose'); const performance = args.includes('--performance'); const compliance = args.includes('--compliance'); const errorHandling = args.includes('--error-handling'); let timeout = 30000; const timeoutIndex = args.indexOf('--timeout'); if (timeoutIndex !== -1 && args[timeoutIndex + 1]) { timeout = parseInt(args[timeoutIndex + 1]); } let retryAttempts = 0; const retryIndex = args.indexOf('--retry'); if (retryIndex !== -1 && args[retryIndex + 1]) { retryAttempts = parseInt(args[retryIndex + 1]); } const transportType = args[0]; (async () => { let transportConfig = { type: transportType }; // Handle auto-detection if (transportType === 'auto') { if (!args[1]) { console.error('Error: URL required for auto transport detection'); process.exit(1); } const { detectTransport, verifyTransport } = require('./auto-detect-transport.js'); console.log('🔍 Auto-detecting transport type...'); const detected = await detectTransport(args[1]); if (detected.transport === 'unknown') { console.error(`❌ Could not detect transport: ${detected.reason}`); process.exit(1); } console.log(`📡 Initial detection: ${detected.transport} (${detected.confidence}% confidence)`); const verified = await verifyTransport(args[1], detected.transport); if (verified) { transportConfig.type = verified.transport; transportConfig.url = args[1]; transportConfig.headers = {}; console.log(`✅ Confirmed transport: ${verified.transport}\n`); } else { console.error('❌ Could not verify transport type'); process.exit(1); } } // Parse transport-specific configuration else if (transportType === 'stdio') { transportConfig.command = args[1]; transportConfig.args = args.slice(2).filter(arg => !arg.startsWith('--') && arg !== transportConfig.command ); } else if (transportType === 'sse' || transportType === 'streamableHttp') { transportConfig.url = args[1]; transportConfig.headers = {}; // Parse headers for (let i = 2; i < args.length; i++) { if (args[i] === '--header' && i + 1 < args.length) { const headerMatch = args[i + 1].match(/^([^:]+):\s*(.+)$/); if (headerMatch) { transportConfig.headers[headerMatch[1]] = headerMatch[2]; } i++; } else if (args[i] === '--auth' && i + 1 < args.length) { transportConfig.headers['Authorization'] = `Bearer ${args[i + 1]}`; i++; } } } const framework = new MCPTestFrameworkAdvanced({ verbose, timeout, retryAttempts }); const tests = { name: 'MCP Server Comprehensive Test Suite', testDiscovery: true, testStability: true, testPerformance: performance, testProtocolCompliance: compliance, testErrorHandling: errorHandling }; try { await framework.testServer(transportConfig, tests); const report = await framework.generateReport(); framework.printSummary(report); process.exit(report.summary.failed > 0 ? 1 : 0); } catch (error) { console.error('❌ Test framework error:', error.message); if (verbose && error.stack) { console.error(error.stack); } process.exit(1); } })(); }