dom-selector-fix.ts•11.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
*/