Skip to main content
Glama
dom-selector-fix.ts11.3 kB
/** * @document MCP Server DOM Selector Fix * @version 1.0.0 * @status active * @author Claude * @created 2025-06-29 * @last_updated 2025-06-29 */ /** * DOM Selector Fix for EuConquisto Composer MCP Server * * Based on authenticated diagnostic results from June 29, 2025: * - Authentication: ✅ Working (JWT token corrected) * - Nova Composição selectors: ✅ 9 working options identified * - Page analysis: 3 buttons detected including target button * * This fix updates the browser automation selectors to use * the validated working selectors from the diagnostic. */ export class OptimizedDOMSelectors { /** * VERIFIED WORKING SELECTORS - June 29, 2025 * These selectors were tested and confirmed working in the live environment */ static readonly NOVA_COMPOSICAO_SELECTORS = [ // TOP PRIORITY: Most reliable selectors (exact text match) 'text="Nova composição"', // ✅ Exact text match - highest reliability 'button:has-text("Nova composição")', // ✅ Button with exact text // HIGH PRIORITY: Case variations that work 'text=NOVA COMPOSIÇÃO', // ✅ All caps version 'button:has-text("NOVA COMPOSIÇÃO")', // ✅ All caps button 'button:has-text("Nova Composição")', // ✅ Title case // MEDIUM PRIORITY: Partial text matches 'button:text("NOVA")', // ✅ Partial match - broader 'button:text("Nova")', // ✅ Partial match - title case // FALLBACK: Generic button selector (use with text filtering) 'button[class*="btn"]', // ✅ Button with btn class 'button' // ✅ Any button (requires text filtering) ]; /** * HAMBURGER MENU SELECTORS (For metadata editing) * These need to be tested after Nova Composição is clicked */ static readonly HAMBURGER_MENU_SELECTORS = [ '[data-testid="hamburger-menu"]', '.hamburger-menu', '[aria-label*="menu"]', 'button[class*="hamburger"]', 'button[class*="menu"]', '[data-cy="hamburger-menu"]', // Fallback: look for menu icons 'button:has-text("⋮")', 'button:has-text("≡")', 'button:has-text("…")' ]; /** * CONFIGURAÇÕES MENU SELECTORS */ static readonly CONFIGURACOES_SELECTORS = [ 'a:has-text("Configurações")', 'button:has-text("Configurações")', '[data-testid="settings"]', '[data-cy="settings"]', 'li:has-text("Configurações")', '.menu-item:has-text("Configurações")' ]; /** * SALVAR BUTTON SELECTORS */ static readonly SALVAR_SELECTORS = [ 'button:has-text("Salvar")', '[data-testid="save-composition"]', '.save-composition-btn', '[data-cy="save"]', 'button[aria-label*="salvar"]', 'button[title*="Salvar"]' ]; /** * IMPROVED SELECTOR STRATEGY * Try selectors in priority order with proper error handling */ static async findElementWithFallback(page: any, selectorGroup: string[], timeout: number = 5000): Promise<any> { for (const selector of selectorGroup) { try { console.log(`🔍 Trying selector: ${selector}`); const element = await page.waitForSelector(selector, { timeout }); // Additional validation for buttons with text if (selector.includes('Nova') || selector.includes('NOVA')) { const text = await element.textContent(); if (text && (text.includes('Nova') || text.includes('NOVA'))) { console.log(`✅ Found working selector: ${selector} with text: "${text.trim()}"`); return element; } } else { console.log(`✅ Found element with selector: ${selector}`); return element; } } catch (error) { console.log(`❌ Selector failed: ${selector}`); continue; } } throw new Error(`No working selectors found from: ${selectorGroup.join(', ')}`); } /** * SAFE CLICK WITH RETRY * Enhanced click method with waiting and retry logic */ static async safeClick(page: any, selectorGroup: string[], description: string): Promise<boolean> { try { console.log(`🎯 Attempting to click: ${description}`); const element = await this.findElementWithFallback(page, selectorGroup); // Ensure element is visible and enabled await element.waitForElementState('visible'); await element.waitForElementState('stable'); // Scroll element into view if needed await element.scrollIntoViewIfNeeded(); // Click with retry logic let clickAttempts = 0; const maxAttempts = 3; while (clickAttempts < maxAttempts) { try { await element.click(); console.log(`✅ Successfully clicked: ${description}`); return true; } catch (clickError) { clickAttempts++; console.log(`⚠️ Click attempt ${clickAttempts} failed for ${description}, retrying...`); if (clickAttempts < maxAttempts) { await page.waitForTimeout(1000); // Wait before retry } } } throw new Error(`Failed to click ${description} after ${maxAttempts} attempts`); } catch (error) { console.error(`❌ Failed to click ${description}:`, error); return false; } } /** * BUTTON TEXT VALIDATION * Additional validation to ensure we're clicking the right button */ static async validateButtonText(element: any, expectedTexts: string[]): Promise<boolean> { try { const actualText = await element.textContent(); if (!actualText) return false; const normalizedText = actualText.trim().toLowerCase(); return expectedTexts.some(expected => normalizedText.includes(expected.toLowerCase()) ); } catch (error) { return false; } } } /** * UPDATED COMPOSITION LIFECYCLE METHODS * Drop-in replacements for the existing MCP server methods */ export class FixedCompositionLifecycle { /** * Fixed Nova Composição creation method */ static async createNewComposition(page: any): Promise<{ success: boolean; message: string; compositionId?: string }> { try { console.log('🚀 Creating new composition with optimized selectors...'); // Wait for page to be ready await page.waitForLoadState('networkidle', { timeout: 30000 }); await page.waitForTimeout(3000); // Use optimized selector strategy const clicked = await OptimizedDOMSelectors.safeClick( page, OptimizedDOMSelectors.NOVA_COMPOSICAO_SELECTORS, 'Nova Composição button' ); if (!clicked) { throw new Error('Could not click Nova Composição button with any selector'); } // Wait for navigation or modal await page.waitForTimeout(2000); const currentURL = page.url(); const compositionId = this.extractCompositionIdFromURL(currentURL); return { success: true, message: 'New composition created successfully with optimized selectors', compositionId: compositionId || 'new' }; } catch (error) { console.error('❌ Failed to create new composition:', error); return { success: false, message: `Failed to create new composition: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Fixed metadata editing method */ static async editCompositionMetadata(page: any, metadata: any): Promise<{ success: boolean; message: string }> { try { console.log('🔧 Opening composition metadata editor with optimized selectors...'); // Open hamburger menu const menuOpened = await OptimizedDOMSelectors.safeClick( page, OptimizedDOMSelectors.HAMBURGER_MENU_SELECTORS, 'Hamburger menu' ); if (!menuOpened) { throw new Error('Could not open hamburger menu'); } await page.waitForTimeout(1000); // Click Configurações const configOpened = await OptimizedDOMSelectors.safeClick( page, OptimizedDOMSelectors.CONFIGURACOES_SELECTORS, 'Configurações option' ); if (!configOpened) { throw new Error('Could not open Configurações'); } await page.waitForTimeout(2000); // Fill metadata fields (same logic as before) if (metadata.title) { await this.fillField(page, ['[name="title"]', '#title', '[data-testid="title"]'], metadata.title); } if (metadata.description) { await this.fillField(page, ['[name="description"]', '#description', '[data-testid="description"]', 'textarea'], metadata.description); } if (metadata.author) { await this.fillField(page, ['[name="author"]', '#author', '[data-testid="author"]'], metadata.author); } // Save changes const saved = await OptimizedDOMSelectors.safeClick( page, OptimizedDOMSelectors.SALVAR_SELECTORS, 'Save button' ); return { success: saved, message: saved ? 'Composition metadata updated successfully' : 'Failed to save metadata' }; } catch (error) { console.error('❌ Failed to edit composition metadata:', error); return { success: false, message: `Failed to edit metadata: ${error instanceof Error ? error.message : 'Unknown error'}` }; } } /** * Helper method for filling form fields */ private static async fillField(page: any, selectors: string[], value: string): Promise<void> { for (const selector of selectors) { try { await page.waitForSelector(selector, { timeout: 3000 }); await page.fill(selector, value); console.log(`✅ Filled field with selector: ${selector}`); return; } catch (error) { continue; } } console.warn(`⚠️ Could not find field for selectors: ${selectors.join(', ')}`); } /** * Extract composition ID from URL */ private static extractCompositionIdFromURL(url: string): string | null { try { const match = url.match(/\/composer\/([A-Za-z0-9+/=]+)/); return match ? match[1] : null; } catch (error) { return null; } } } /** * INTEGRATION INSTRUCTIONS * * To apply this fix to the existing MCP server: * * 1. Replace the createNewComposition method in composition-lifecycle.ts * with FixedCompositionLifecycle.createNewComposition * * 2. Replace the editCompositionMetadata method with * FixedCompositionLifecycle.editCompositionMetadata * * 3. Import OptimizedDOMSelectors for any additional selector needs * * 4. Update the JWT token in composition-lifecycle.ts with the corrected version * * 5. Rebuild the project: npm run build * * 6. Restart Claude Desktop to reload the MCP server */ /** * TESTING CHECKLIST * * After applying the fix, test: * ✅ Authentication (JWT token working) * ✅ Navigation to composer * ✅ Nova Composição button click * ✅ Metadata editing flow * ✅ Save functionality * ✅ Error handling and fallbacks */

Latest Blog Posts

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/rkm097git/euconquisto-composer-mcp-poc'

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