Skip to main content
Glama

MCP Advisor

MIT License
88
64
  • Apple
  • Linux
test-helpers.ts14.7 kB
import { Page, expect } from '@playwright/test'; // Test configuration constants export const TEST_CONFIG = { baseUrl: process.env.MCP_INSPECTOR_URL || 'http://localhost:6274', authToken: process.env.MCP_AUTH_TOKEN, timeout: 20000, // Reduced from 30000 shortTimeout: 4000, // Reduced from 5000 longTimeout: 45000 // Reduced from 60000 }; // Environment setup and cleanup utilities export class EnvironmentManager { private originalEnvVars: Record<string, string | undefined> = {}; saveEnvironment() { this.originalEnvVars = { MEILISEARCH_INSTANCE: process.env.MEILISEARCH_INSTANCE, MEILISEARCH_LOCAL_HOST: process.env.MEILISEARCH_LOCAL_HOST, MEILISEARCH_MASTER_KEY: process.env.MEILISEARCH_MASTER_KEY, MEILISEARCH_INDEX_NAME: process.env.MEILISEARCH_INDEX_NAME }; } restoreEnvironment() { Object.entries(this.originalEnvVars).forEach(([key, value]) => { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } }); } setMeilisearchConfig(config: { instance?: string; host?: string; key?: string; indexName?: string; }) { if (config.instance) process.env.MEILISEARCH_INSTANCE = config.instance; if (config.host) process.env.MEILISEARCH_LOCAL_HOST = config.host; if (config.key) process.env.MEILISEARCH_MASTER_KEY = config.key; if (config.indexName) process.env.MEILISEARCH_INDEX_NAME = config.indexName; } } // Smart waiting functions export class SmartWaiter { constructor(private page: Page) {} async waitForConnection(timeout = TEST_CONFIG.timeout) { await this.page.waitForFunction(() => { const buttons = Array.from(document.querySelectorAll('button')); return buttons.some(btn => btn.textContent?.includes('List Resources') || btn.textContent?.includes('List Tools') ) || document.querySelector('[role="tab"][data-testid="tools-tab"]') !== null; }, { timeout }); } async waitForSearchResults(timeout = TEST_CONFIG.timeout) { await this.page.waitForFunction(() => { const content = document.body.textContent || ''; return content.includes('Title:') || content.includes('No results') || content.includes('error') || content.includes('failed'); }, { timeout }); } async waitForPageLoad() { await this.page.waitForLoadState('networkidle'); await this.page.waitForLoadState('domcontentloaded'); } async waitForElementToBeVisible(selector: string, timeout = TEST_CONFIG.shortTimeout) { await this.page.waitForSelector(selector, { state: 'visible', timeout }); } } // MCP connection and setup utilities export class MCPConnectionManager { constructor(private page: Page, private waiter: SmartWaiter) {} async connectToMCP(retries = 3) { const maskedToken = TEST_CONFIG.authToken ? `${TEST_CONFIG.authToken.substring(0, 4)}****` : 'undefined'; console.log(`🌐 访问: ${TEST_CONFIG.baseUrl}/?MCP_PROXY_AUTH_TOKEN=${maskedToken}`); const fullUrl = `${TEST_CONFIG.baseUrl}/?MCP_PROXY_AUTH_TOKEN=${TEST_CONFIG.authToken}`; for (let attempt = 1; attempt <= retries; attempt++) { try { await this.page.goto(fullUrl); await this.waiter.waitForPageLoad(); // Click connect button await this.page.getByRole('button', { name: 'Connect' }).click({ timeout: TEST_CONFIG.shortTimeout }); await this.page.waitForTimeout(2000); // Brief wait for connection to establish // Wait for connection to be established await this.waiter.waitForConnection(); // Check for connection errors const connectionError = this.page.getByText('Connection Error'); if (await connectionError.isVisible().catch(() => false)) { const errorText = await connectionError.textContent(); throw new Error(`MCP connection failed: ${errorText}`); } // Navigate to Tools tab if needed await this.ensureToolsTabActive(); console.log(`✅ MCP connection established on attempt ${attempt}`); return; // Success } catch (error: any) { console.warn(`⚠️ Connection attempt ${attempt} failed:`, error.message); if (attempt === retries) { console.error('❌ All connection attempts failed'); throw new Error(`Failed to connect to MCP after ${retries} attempts: ${error.message}`); } // Wait before retry await this.page.waitForTimeout(2000 * attempt); } } } private async ensureToolsTabActive() { try { const toolsTab = this.page.getByRole('tab', { name: 'Tools' }); const listToolsButton = this.page.getByRole('button', { name: 'List Tools' }); const isListToolsVisible = await listToolsButton.isVisible().catch(() => false); if (!isListToolsVisible) { await toolsTab.click(); await this.page.waitForTimeout(1000); await this.waiter.waitForElementToBeVisible('button:has-text("List Tools")'); } await listToolsButton.click(); await this.page.waitForTimeout(1000); } catch (error: any) { console.warn('⚠️ Warning: Could not activate Tools tab:', error.message); // Don't fail the test, just log the warning } } } // Search operations utilities export class SearchOperations { constructor(private page: Page, private waiter: SmartWaiter) {} get currentPage(): Page { return this.page; } async performSearch(query: string, testDescription?: string, retries = 2): Promise<number> { if (testDescription) { console.log(`🔄 测试 ${testDescription}`); } for (let attempt = 1; attempt <= retries; attempt++) { try { // Click on the search tool await this.page.getByRole('tabpanel', { name: 'Tools' }) .getByText('此工具用于寻找合适且专业MCP').first().click(); // Clear and fill in the search query const textbox = this.page.getByRole('textbox', { name: 'taskDescription' }); await textbox.clear(); await textbox.fill(query); // Start the search const startTime = Date.now(); await this.page.getByRole('button', { name: 'Run Tool' }).click(); // Wait for results using smart waiting await this.waiter.waitForSearchResults(); const endTime = Date.now(); const responseTime = endTime - startTime; if (testDescription) { console.log(`⏱️ ${testDescription}: ${responseTime}ms`); } return responseTime; } catch (error: any) { console.warn(`⚠️ Search attempt ${attempt} failed:`, error.message); if (attempt === retries) { console.error(`❌ Search failed after ${retries} attempts for query: "${query}"`); throw new Error(`Search operation failed: ${error.message}`); } // Wait before retry await this.page.waitForTimeout(1000 * attempt); } } // This should never be reached due to the throw above, but TypeScript needs it throw new Error('Search operation completed without returning a response time'); } async getSearchResults(): Promise<string[]> { try { const pageContent = await this.page.content(); return this.extractResultTitles(pageContent); } catch (error: any) { console.warn('⚠️ Warning: Could not extract search results:', error.message); return []; } } private extractResultTitles(content: string): string[] { const titleRegex = /Title:\s*([^\n]+)/g; const titles = []; let match; while ((match = titleRegex.exec(content)) !== null) { titles.push(match[1].trim()); } return titles; } } // Screenshot utilities export class ScreenshotManager { constructor(private page: Page) {} async takeScreenshot(filename: string, options?: { fullPage?: boolean }) { await this.page.screenshot({ path: `test-results/${filename}`, fullPage: options?.fullPage ?? true }); } } // Test validation utilities export class TestValidator { static validateSearchResults(results: string[], minCount = 1) { expect(results.length).toBeGreaterThanOrEqual(minCount); } static validatePageContent(content: string) { expect(content).toContain('Title:'); } static validateResponseTime(responseTime: number, maxTime = 15000) { expect(responseTime).toBeLessThan(maxTime); } static validateResultRelevance(results: string[], keywords: string[]) { const hasRelevantResults = results.some(title => keywords.some(keyword => title.toLowerCase().includes(keyword.toLowerCase()) ) ); if (!hasRelevantResults) { console.log('⚠️ 未找到预期关键词,但测试继续'); console.log('实际结果:', results); } // Don't fail the test if no relevant keywords found, just log it return hasRelevantResults; } static compareResults(localResults: string[], cloudResults: string[]) { const commonResults = localResults.filter(localTitle => cloudResults.some(cloudTitle => cloudTitle === localTitle) ); console.log(`云端结果数量: ${cloudResults.length}`); console.log(`本地结果数量: ${localResults.length}`); console.log(`共同结果数量: ${commonResults.length}`); if (commonResults.length > 0) { console.log('✅ 发现相同结果,数据同步正常'); } else { console.log('⚠️ 没有发现完全相同的结果,可能存在数据同步问题'); } console.log('云端结果:', cloudResults); console.log('本地结果:', localResults); return commonResults; } } // Configuration test utilities export class ConfigurationTester { constructor( private envManager: EnvironmentManager, private searchOps: SearchOperations, private validator: typeof TestValidator, private screenshotManager: ScreenshotManager ) {} async testConfiguration(configName: string, config: Record<string, string>) { console.log(`🧪 测试配置: ${configName}`); // Save current environment state to prevent pollution const originalEnv: Record<string, string | undefined> = {}; Object.keys(config).forEach(key => { originalEnv[key] = process.env[key]; }); try { // Set environment variables Object.entries(config).forEach(([key, value]) => { process.env[key] = value; }); // Perform test search await this.searchOps.performSearch(`配置测试: ${configName}`); // Validate response const results = await this.searchOps.getSearchResults(); if (results.length === 0) { console.warn(`⚠️ 警告: 配置 "${configName}" 没有返回搜索结果`); // Don't fail the test, just log the warning } else { this.validator.validateSearchResults(results); } console.log(`✅ 配置测试完成: ${configName}`); } catch (error: any) { console.error(`❌ 配置测试失败: ${configName} - ${error.message}`); // Take screenshot for debugging await this.screenshotManager.takeScreenshot(`config-error-${configName.replace(/\s+/g, '-')}.png`); // Re-throw to fail the test throw new Error(`Configuration test failed for "${configName}": ${error.message}`); } finally { // Always restore original environment variables to prevent pollution Object.entries(originalEnv).forEach(([key, value]) => { if (value === undefined) { delete process.env[key]; } else { process.env[key] = value; } }); } } async recoverFromFailure(failureType: string, waiter: SmartWaiter, context?: any) { console.log(`🔧 开始故障恢复: ${failureType}`); try { switch (failureType) { case 'connection': // Try to reconnect or refresh the page await this.searchOps.currentPage.reload(); await waiter.waitForPageLoad(); break; case 'search': // Clear search state and retry await this.searchOps.currentPage.getByRole('textbox', { name: 'taskDescription' }).clear(); break; case 'environment': // Restore environment using the environment manager this.envManager.restoreEnvironment(); break; default: console.warn(`⚠️ 未知的故障类型: ${failureType}`); } console.log(`✅ 故障恢复完成: ${failureType}`); } catch (error: any) { console.error(`❌ 故障恢复失败: ${failureType} - ${error.message}`); throw new Error(`Recovery failed for "${failureType}": ${error.message}`); } } async testErrorHandling(errorName: string, config: Record<string, string>) { console.log(`🚨 测试错误情况: ${errorName}`); try { // Set error configuration Object.entries(config).forEach(([key, value]) => { process.env[key] = value; }); // Perform test search await this.searchOps.performSearch(`错误处理测试: ${errorName}`); // For error handling tests, we expect either: // 1. Results from fallback mechanism // 2. A graceful error message const results = await this.searchOps.getSearchResults(); const pageContent = await this.searchOps.currentPage.content(); const hasErrorHandling = results.length > 0 || // fallback succeeded pageContent.includes('error') || pageContent.includes('failed') || pageContent.includes('timeout'); if (!hasErrorHandling) { console.warn(`⚠️ 警告: 错误处理测试 "${errorName}" 可能没有正确处理错误情况`); } // Take screenshot for debugging await this.screenshotManager.takeScreenshot(`error-handling-${errorName.replace(/\s+/g, '-')}.png`); console.log(`✅ 错误处理测试完成: ${errorName}`); } catch (error: any) { // For error handling tests, we expect some failures console.log(`✅ 错误处理测试完成: ${errorName} (捕获到预期错误)`); // Take screenshot for debugging await this.screenshotManager.takeScreenshot(`error-handling-${errorName.replace(/\s+/g, '-')}.png`); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/istarwyh/mcpadvisor'

If you have feedback or need assistance with the MCP directory API, please join our Discord server