UNPKG

mcp-server-tester-sse-http-stdio

Version:

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

298 lines (297 loc) • 11.2 kB
/** * Capabilities test runner for direct tool calls */ import { McpClient, createTransportOptions } from '../../shared/core/mcp-client.js'; import { isSingleToolTest, isMultiStepTest, } from '../../shared/core/types.js'; export class CapabilitiesTestRunner { mcpClient; config; serverOptions; displayManager; constructor(config, serverOptions, displayManager) { this.config = config; this.serverOptions = serverOptions; this.displayManager = displayManager; this.mcpClient = new McpClient(); } async run() { const startTime = Date.now(); try { const transportOptions = createTransportOptions(this.serverOptions.serverConfig); await this.mcpClient.connect(transportOptions); // Emit section start this.displayManager?.sectionStart('tools', 'šŸ“‹ Tools Tests'); // Run discovery tests if configured if (this.config.expected_tool_list) { await this.runDiscoveryTests(); } // Run capabilities tests const results = []; for (const test of this.config.tests) { this.displayManager?.testStart(test.name); const result = await this.runTest(test); results.push(result); this.displayManager?.testComplete(test.name, result.passed, result.errors); } const endTime = Date.now(); const duration = endTime - startTime; const summary = { total: results.length, passed: results.filter(r => r.passed).length, failed: results.filter(r => !r.passed).length, duration, results, }; return summary; } finally { await this.mcpClient.disconnect(); } } async runDiscoveryTests() { if (!this.config.expected_tool_list) { return; } // Test tool discovery const toolsResult = await this.mcpClient.sdk.listTools(); const availableTools = toolsResult.tools?.map((tool) => tool.name) || []; const missingTools = []; for (const expectedTool of this.config.expected_tool_list) { if (!availableTools.includes(expectedTool)) { missingTools.push(expectedTool); } } const passed = missingTools.length === 0; // Emit tool discovery result this.displayManager?.toolDiscovery(this.config.expected_tool_list, availableTools, passed); if (!passed) { throw new Error(`Expected tool(s) not found: ${missingTools.join(', ')}. Available tools: ${availableTools.join(', ')}`); } // Always validate tool schemas const toolsResultForValidation = await this.mcpClient.sdk.listTools(); const tools = toolsResultForValidation.tools || []; for (const tool of tools) { if (!tool.name) { throw new Error(`Tool missing name property`); } if (!tool.inputSchema) { throw new Error(`Tool '${tool.name}' missing input schema`); } } } async runTest(test) { const startTime = Date.now(); const callResults = []; const errors = []; let passed = true; if (isSingleToolTest(test)) { // Handle single tool test format const callToMake = { tool: test.tool, params: test.params, expect: test.expect, }; const callResult = await this.runCall(callToMake); callResults.push(callResult); if (!callResult.success) { passed = false; errors.push(`Tool call ${test.tool} failed: ${callResult.error}`); } } else if (isMultiStepTest(test)) { // Handle multi-step test format for (const call of test.calls) { const callResult = await this.runCall(call); callResults.push(callResult); if (!callResult.success) { passed = false; errors.push(`Tool call ${call.tool} failed: ${callResult.error}`); } } } const endTime = Date.now(); const duration = endTime - startTime; return { name: test.name, passed, errors, calls: callResults, duration, }; } async runCall(call) { const startTime = Date.now(); // Base result template to eliminate duplication const baseResult = { tool: call.tool, params: call.params, duration: 0, // Will be updated }; try { const result = await this.mcpClient.sdk.callTool({ name: call.tool, arguments: call.params, }); baseResult.duration = Date.now() - startTime; const hasError = this.hasToolError(result); // Route to appropriate handler based on expectations if (call.expect.success) { return this.handleSuccessExpectation(baseResult, result, hasError, call.expect); } else { return this.handleFailureExpectation(baseResult, result, hasError, call.expect); } } catch (error) { baseResult.duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : String(error); return this.handleThrownError(baseResult, errorMessage, call.expect); } } handleSuccessExpectation(baseResult, result, hasError, expect) { // If we got an error result but expected success, that's a failure if (hasError) { const errorMessage = this.extractErrorMessage(result); return { ...baseResult, success: false, error: errorMessage, }; } // Call succeeded as expected - validate the result const validationError = this.validateCallResult(result, expect); if (validationError) { return { ...baseResult, success: false, error: validationError, }; } // Log result for debugging if (process.env.DEBUG || this.serverOptions.debug) { console.log(`\nšŸ“¦ Result for ${baseResult.tool}:`); const resultStr = JSON.stringify(result, null, 2); if (resultStr.length > 1000) { console.log(resultStr.substring(0, 1000) + '...[truncated]'); } else { console.log(resultStr); } } return { ...baseResult, success: true, result, }; } handleFailureExpectation(baseResult, result, hasError, expect) { if (hasError) { // Call failed as expected - check error message if specified const errorMessage = this.extractErrorMessage(result); if (expect.error?.contains) { if (!errorMessage.includes(expect.error.contains)) { return { ...baseResult, success: false, error: `Expected error to contain '${expect.error.contains}', but got: ${errorMessage}`, }; } } return { ...baseResult, success: true, error: errorMessage, }; } else { // Call succeeded but was expected to fail return { ...baseResult, success: false, error: 'Expected tool call to fail, but it succeeded', result, }; } } handleThrownError(baseResult, errorMessage, expect) { // Check if the call was expected to fail if (!expect.success) { // Call failed as expected - check error message if specified if (expect.error?.contains) { if (!errorMessage.includes(expect.error.contains)) { return { ...baseResult, success: false, error: `Expected error to contain '${expect.error.contains}', but got: ${errorMessage}`, }; } } return { ...baseResult, success: true, error: errorMessage, }; } else { // Call failed but was expected to succeed return { ...baseResult, success: false, error: errorMessage, }; } } validateCallResult(result, expect) { if (!expect.result) { return null; } // Check contains validation if (expect.result.contains) { const resultStr = JSON.stringify(result); if (!resultStr.includes(expect.result.contains)) { return `Expected result to contain '${expect.result.contains}', but got: ${resultStr}`; } } // Check equals validation if (expect.result.equals !== undefined) { const resultStr = JSON.stringify(result); const expectedStr = JSON.stringify(expect.result.equals); if (resultStr !== expectedStr) { return `Expected result to equal ${expectedStr}, but got: ${resultStr}`; } } // TODO: Implement schema validation when needed if (expect.result.schema) { // This would require ajv or similar JSON schema validator console.warn('Schema validation not yet implemented'); } return null; } hasToolError(result) { // According to MCP spec, tool execution errors are indicated by isError: true if (typeof result === 'object' && result !== null) { const resultObj = result; return resultObj.isError === true; } return false; } extractErrorMessage(result) { if (typeof result === 'object' && result !== null) { const resultObj = result; // Extract error message from content if available if (Array.isArray(resultObj.content)) { for (const contentItem of resultObj.content) { if (typeof contentItem === 'object' && contentItem !== null) { const content = contentItem; if (content.type === 'text' && typeof content.text === 'string') { return content.text; } } } } // Fallback to generic error message return 'Tool execution failed'; } return 'Unknown tool error'; } }