Skip to main content
Glama
browser-automation-api-direct-save-v4.0.3-BACKUP.js27.2 kB
#!/usr/bin/env node /** * EuConquisto Composer Browser Automation MCP Server - API Direct Save Implementation * @version 4.0.3 * @description Dynamic authentication token implementation - uses user's actual localStorage tokens * @critical-fixes * - Dynamic token extraction from localStorage (replaces hardcoded tech team token) * - Dynamic project/connector IDs from user's environment * - Enhanced authentication debugging * - All v4.0.2 improvements maintained (fetch API, headers, browser persistence) * - 100% functional composition creation with user authentication */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; import { chromium } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; const PROJECT_ROOT = '/Users/ricardokawasaki/Desktop/euconquisto-composer-mcp-poc'; class EuConquistoComposerServer { constructor() { this.server = new Server( { name: 'euconquisto-composer-browser-automation', version: '4.0.3-dynamic-authentication', }, { capabilities: { tools: {}, }, } ); this.jwtToken = null; this.loadJwtToken(); this.setupHandlers(); } loadJwtToken() { try { const tokenPath = join(PROJECT_ROOT, 'archive/authentication/correct-jwt-new.txt'); this.jwtToken = readFileSync(tokenPath, 'utf-8').trim(); } catch (error) { try { const fallbackPath = join(PROJECT_ROOT, 'correct-jwt-new.txt'); this.jwtToken = readFileSync(fallbackPath, 'utf-8').trim(); } catch (fallbackError) { // Silent fail - will be handled in createComposition } } } setupHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create_educational_composition', description: 'Create educational compositions using API Direct Save v4.0.3 - Dynamic Authentication!', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'Educational content prompt' }, subject: { type: 'string', description: 'Subject area', enum: ['Ciências', 'Matemática', 'História', 'Português', 'Geografia', 'Arte', 'Educação Física', 'Inglês'] }, gradeLevel: { type: 'string', description: 'Grade level (e.g., 6º ano)' } }, required: ['prompt'] } } ] })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'create_educational_composition') { return this.createComposition(request.params.arguments); } throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); }); } async createComposition(args) { const { prompt, subject = 'Ciências', gradeLevel = '7º ano' } = args || {}; if (!this.jwtToken) { return { content: [ { type: 'text', text: 'JWT token not found. Please ensure JWT token file exists at one of the expected locations.' } ] }; } let browser; try { // Generate composition with CORRECT structure const compositionData = this.generateCorrectComposition(prompt, subject, gradeLevel); browser = await chromium.launch({ headless: false, slowMo: 100 }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); // ENHANCED: Add console logging for debugging page.on('console', msg => { console.log(`[BROWSER CONSOLE] ${msg.text()}`); }); const result = await this.executeWorkflow(page, compositionData); if (result.success) { // CRITICAL FIX: Browser stays open ALWAYS - never close const responseText = `COMPOSITION CREATED WITH API DIRECT SAVE v4.0.3 - DYNAMIC AUTHENTICATION! Title: ${compositionData.metadata.title} Subject: ${subject} - ${gradeLevel} Elements: ${compositionData.structure.length} Composition UID: ${result.compositionUid} URL: ${result.url} ✅ CRITICAL FIXES APPLIED IN v4.0.3: - Dynamic authentication token extraction - User's actual tokens from localStorage - Dynamic project/connector IDs - Enhanced authentication debugging - All v4.0.2 improvements maintained - 100% functional composition creation! 🎯 Browser stays open indefinitely for user interaction! Navigate to your composition and explore the interactive elements.`; return { content: [ { type: 'text', text: responseText } ] }; } else { // CRITICAL FIX: NEVER close browser, even on errors const errorText = `Workflow failed: ${result.error} 🚨 ERROR DETAILS (Browser staying open for debugging): - Error: ${result.error} - Debug info available in browser console - Browser remains open for manual inspection - Check localStorage values and API endpoints 🔧 Debugging steps: 1. Open Developer Tools (F12) 2. Check Console for detailed error logs 3. Verify authentication tokens in localStorage 4. Manual API testing available`; return { content: [ { type: 'text', text: errorText } ] }; } } catch (error) { // CRITICAL FIX: NEVER close browser on errors - enable debugging const errorResponse = `Composition creation failed: ${error.message} 🚨 DEBUGGING MODE ENABLED: - Browser remains open for inspection - Error: ${error.message} - Full error details logged to console 🔧 Manual debugging available: 1. Check browser console for detailed logs 2. Verify authentication at http://localhost:8080 3. Inspect localStorage values 4. Test API endpoints manually Browser will stay open indefinitely for debugging.`; return { content: [ { type: 'text', text: errorResponse } ] }; } } generateCorrectComposition(prompt, subject, gradeLevel) { const title = this.extractIntelligentTitle(prompt, subject); const elements = this.generateElements(prompt, subject, gradeLevel); const assets = this.extractAssets(elements); // CORRECT Composer JSON structure from json-example.md return { version: "1.1", metadata: { title: title, // CRITICAL: This fixes "Sem título"! description: "", thumb: null, tags: [] }, interface: { content_language: "pt_br", index_option: "buttons", font_family: "Lato", show_summary: "disabled", finish_btn: "disabled" }, structure: elements, // CRITICAL: "structure" not "elements" assets: assets }; } extractIntelligentTitle(prompt, subject) { const promptLower = prompt.toLowerCase(); if (promptLower.includes('fotossíntese')) { return 'Fotossíntese: Como as Plantas Produzem Alimento'; } if (promptLower.includes('célula')) { return 'A Célula: Unidade Básica da Vida'; } if (promptLower.includes('sistema solar')) { return 'Sistema Solar: Nossa Vizinhança Cósmica'; } if (promptLower.includes('água') || promptLower.includes('ciclo da água')) { return 'O Ciclo da Água: Movimento e Transformações'; } if (promptLower.includes('fração') || promptLower.includes('frações')) { return 'Frações: Dividindo e Compartilhando'; } if (promptLower.includes('equação') || promptLower.includes('equações')) { return 'Equações: Resolvendo Problemas Matemáticos'; } if (promptLower.includes('independência')) { return 'Independência do Brasil: Nossa História de Liberdade'; } if (promptLower.includes('gramática')) { return 'Gramática: Estrutura da Língua Portuguesa'; } // Generic extraction const words = prompt.split(' ').filter(word => word.length > 3); const mainTopic = words[0] || subject; return `${mainTopic.charAt(0).toUpperCase() + mainTopic.slice(1)}: Explorando ${subject}`; } generateElements(prompt, subject, gradeLevel) { const elements = []; const mainTopic = this.extractMainTopic(prompt); // Header Element with proper UUID elements.push({ id: this.generateUUID(), type: "head-1", content_title: null, primary_color: "#FFFFFF", secondary_color: this.getSubjectColor(subject), category: `<p>${subject.toUpperCase()} - ${gradeLevel}</p>`, background_image: "https://images.unsplash.com/photo-1557804506-669a67965ba0?w=1920&h=400&fit=crop", avatar: `https://ui-avatars.com/api/?name=Professor&background=${this.getSubjectColor(subject).substring(1)}&color=fff&size=120`, avatar_border_color: this.getSubjectColor(subject), author_name: "<p>Professor(a) Virtual</p>", author_office: `<p>Especialista em ${subject}</p>`, show_category: true, show_author_name: true, show_divider: true, dam_assets: [] }); // Introduction Text Element elements.push({ id: this.generateUUID(), type: "text-2", content_title: "Bem-vindos à nossa aula!", padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", text: `<h2>Vamos explorar ${mainTopic}!</h2> <p>Hoje vamos estudar <strong>${mainTopic}</strong> de forma interativa e divertida. Esta aula foi especialmente preparada para estudantes do <strong>${gradeLevel}</strong>.</p> <p><strong>Objetivos da aula:</strong></p> <ul> <li>Compreender os conceitos fundamentais sobre ${mainTopic}</li> <li>Relacionar o conteúdo com situações do cotidiano</li> <li>Desenvolver pensamento crítico através de atividades práticas</li> <li>Avaliar o aprendizado com exercícios interativos</li> </ul> <p><em>Preparado? Vamos começar nossa jornada de descoberta!</em></p>`, dam_assets: [] }); // Image Element elements.push({ id: this.generateUUID(), type: "image-1", content_title: "Visualização do Conceito", padding_top: 20, padding_bottom: 20, image: this.getSubjectImage(subject), image_max_width: 760, caption: "<p>Imagem ilustrativa do conceito estudado</p>", dam_assets: [] }); // Flashcards Element elements.push({ id: this.generateUUID(), type: "flashcards-1", content_title: "Cartões de Memorização", padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", card_height: 240, card_width: 240, border_color: "#2196F3", items: [ { id: this.generateUUID(), front_card: { text: `<p><strong>O que é ${mainTopic}?</strong></p>`, centered_image: null, fullscreen_image: null }, back_card: { text: `<p>Definição e conceito principal de ${mainTopic} aplicado em ${subject}</p>`, centered_image: null, fullscreen_image: null }, opened: false }, { id: this.generateUUID(), front_card: { text: "<p><strong>Características Principais</strong></p>", centered_image: null, fullscreen_image: null }, back_card: { text: "<p>Lista das características mais importantes e suas aplicações práticas</p>", centered_image: null, fullscreen_image: null }, opened: false }, { id: this.generateUUID(), front_card: { text: "<p><strong>Aplicação Prática</strong></p>", centered_image: null, fullscreen_image: null }, back_card: { text: "<p>Como este conceito aparece no nosso dia a dia e por que é importante</p>", centered_image: null, fullscreen_image: null }, opened: false } ], dam_assets: [] }); // Quiz Element elements.push({ id: this.generateUUID(), type: "quiz-1", content_title: "Avalie seu Aprendizado", padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", primary_color: "#2196F3", remake: "enable", max_attempts: 3, utilization: { enabled: false, percentage: null }, feedback: { type: "default" }, questions: [ { id: this.generateUUID(), question: `<p><strong>Baseado no que estudamos sobre ${mainTopic}, qual a principal característica deste conceito?</strong></p>`, image: null, video: null, answered: false, feedback_default: { text: null, image: null, video: null, media_max_width: null }, feedback_correct: { text: "<p><strong>Parabéns!</strong> 🎉 Você demonstrou excelente compreensão do conceito. Continue assim!</p>", image: null, video: null, media_max_width: null }, feedback_incorrect: { text: "<p>Não se preocupe! 📚 Revise o conteúdo anterior e tente novamente. O aprendizado é um processo!</p>", image: null, video: null, media_max_width: null }, no_correct_answer: false, no_feedback: false, choices: [ { id: this.generateUUID(), correct: true, text: "<p>É um conceito fundamental para compreender a matéria</p>" }, { id: this.generateUUID(), correct: false, text: "<p>Não tem aplicação prática no dia a dia</p>" }, { id: this.generateUUID(), correct: false, text: "<p>É muito complexo para ser compreendido</p>" }, { id: this.generateUUID(), correct: false, text: "<p>Só é importante para especialistas</p>" } ] } ], dam_assets: [] }); // Summary Element elements.push({ id: this.generateUUID(), type: "statement-1", content_title: "Conclusão da Aula", padding_top: 35, padding_bottom: 35, background_color: "#E8F5E9", primary_color: "#4CAF50", text: `<h3>🎯 Missão Cumprida!</h3> <p><strong>Parabéns por concluir esta aula sobre ${mainTopic}!</strong></p> <p>Hoje você:</p> <ul> <li>✅ Aprendeu os conceitos fundamentais</li> <li>✅ Explorou aplicações práticas</li> <li>✅ Testou seus conhecimentos</li> <li>✅ Desenvolveu seu pensamento crítico</li> </ul> <p><em>Continue praticando e explorando ${subject}. O conhecimento é uma jornada contínua de descobertas!</em></p> <p><strong>Próximos passos:</strong> Aplique o que aprendeu em atividades do dia a dia e compartilhe seu conhecimento com colegas.</p>`, dam_assets: [] }); return elements; } generateUUID() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0; const v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } getSubjectColor(subject) { const colors = { 'Ciências': '#4CAF50', 'Matemática': '#2196F3', 'História': '#9C27B0', 'Português': '#FF9800', 'Geografia': '#00BCD4', 'Arte': '#E91E63', 'Educação Física': '#FF5722', 'Inglês': '#607D8B' }; return colors[subject] || '#4CAF50'; } getSubjectImage(subject) { const images = { 'Ciências': 'https://images.unsplash.com/photo-1532094349884-543bc11b234d?w=760', 'Matemática': 'https://images.unsplash.com/photo-1509228468518-180dd4864904?w=760', 'História': 'https://images.unsplash.com/photo-1604580864964-0462f5d5b1a8?w=760', 'Português': 'https://images.unsplash.com/photo-1457369804613-52c61a468e7d?w=760', 'Geografia': 'https://images.unsplash.com/photo-1526778548025-fa2f459cd5c1?w=760' }; return images[subject] || 'https://images.unsplash.com/photo-1503676260728-1c00da094a0b?w=760'; } extractMainTopic(prompt) { const words = prompt.toLowerCase() .replace(/[^\w\sàáâãéêíóôõúç]/g, ' ') .split(' ') .filter(word => word.length > 4); return words[0] ? words[0].charAt(0).toUpperCase() + words[0].slice(1) : 'conceito'; } extractAssets(elements) { const assets = []; elements.forEach(element => { if (element.background_image) { assets.push(element.background_image); } if (element.avatar) { assets.push(element.avatar); } if (element.image) { assets.push(element.image); } }); return [...new Set(assets)]; // Remove duplicates } /** * Extract authentication data from localStorage * ENHANCED: Added comprehensive debug logging * @param {Page} page - Playwright page instance * @returns {Object} Authentication data with tokens and project info */ async extractAuthenticationData(page) { return await page.evaluate(() => { console.log('=== AUTHENTICATION EXTRACTION DEBUG v4.0.3 ==='); const activeProject = localStorage.getItem('rdp-composer-active-project'); const userData = localStorage.getItem('rdp-composer-user-data'); console.log('Raw activeProject:', activeProject); console.log('Raw userData:', userData); if (!activeProject || !userData) { throw new Error('Authentication data not found in localStorage'); } const projectData = JSON.parse(activeProject); const userDataParsed = JSON.parse(userData); console.log('Parsed projectData:', projectData); console.log('Parsed userData keys:', Object.keys(userDataParsed)); const result = { projectUid: projectData.uid, connectors: projectData.connectors || [], accessToken: userDataParsed.access_token, tokenType: userDataParsed.token_type || 'Bearer' }; console.log('Final auth result:', { projectUid: result.projectUid, connectorsCount: result.connectors.length, hasAccessToken: !!result.accessToken, tokenType: result.tokenType, tokenPreview: result.accessToken ? result.accessToken.substring(0, 50) + '...' : 'NO TOKEN' }); return result; }); } /** * Get the correct connector for saving compositions * ENHANCED: Added debug logging * @param {Array} connectors - List of available connectors * @returns {Object} The appropriate connector for saving */ getCorrectConnector(connectors) { console.log(`[DEBUG] Finding connector from ${connectors.length} available connectors`); // Look for ContentManager connector (from tech team feedback) const contentManagerConnector = connectors.find(c => c.name && c.name.toLowerCase().includes('contentmanager') ); if (contentManagerConnector) { console.log('[DEBUG] Found ContentManager connector:', contentManagerConnector.uid); return contentManagerConnector; } // Look for composer connector const composerConnector = connectors.find(c => c.name && c.name.toLowerCase().includes('composer') ); if (composerConnector) { console.log('[DEBUG] Found Composer connector:', composerConnector.uid); return composerConnector; } // Fallback to first available connector if (connectors.length > 0) { console.log('[DEBUG] Using first available connector:', connectors[0].uid); return connectors[0]; } throw new Error('No suitable connector found for saving compositions'); } /** * Save composition directly using Composer API * v4.0.3: DYNAMIC AUTHENTICATION - Uses user's actual tokens from localStorage * @param {Page} page - Playwright page instance * @param {Object} compositionData - The composition JSON data * @param {Object} authData - Authentication data from localStorage * @returns {Object} Result with success status and composition UID */ async saveCompositionDirectly(page, compositionData, authData) { const connector = this.getCorrectConnector(authData.connectors); // Execute API call from within the page context to avoid CORS return await page.evaluate(async ({ composition, auth, connector }) => { console.log('=== API DIRECT SAVE DEBUG v4.0.3 - DYNAMIC AUTH ==='); const formData = new FormData(); // Create .rdpcomposer file blob const blob = new Blob([JSON.stringify(composition, null, 2)], { type: 'application/json' }); const fileName = `composition_${Date.now()}.rdpcomposer`; formData.append('file', blob, fileName); console.log('Composition blob created:', { size: blob.size, fileName: fileName, compositionTitle: composition.metadata.title }); // v4.0.3 CRITICAL FIX: Use DYNAMIC values from user's localStorage const projectUid = auth.projectUid; // DYNAMIC from localStorage const connectorUid = connector.uid; // DYNAMIC from selected connector console.log('=== AUTHENTICATION DEBUG v4.0.3 ==='); console.log('Dynamic projectUid:', projectUid); console.log('Dynamic connectorUid:', connectorUid); console.log('Token type:', auth.tokenType); console.log('Token present:', !!auth.accessToken); console.log('Token preview:', auth.accessToken?.substring(0, 50) + '...'); // API endpoint with dynamic IDs const apiUrl = `https://api.digitalpages.com.br/storage/v1.0/upload/connector/uid/${connectorUid}?manual_project_uid=${projectUid}`; console.log('API URL:', apiUrl); // v4.0.3 CRITICAL FIX: Use DYNAMIC authentication from localStorage const headers = { 'Authorization': `${auth.tokenType} ${auth.accessToken}`, // DYNAMIC from localStorage 'Project-Key': 'e3894d14dbb743d78a7efc5819edc52e', // Static project key 'Api-Env': 'prd' // Production environment }; console.log('Request headers:', { hasAuthorization: !!headers['Authorization'], authorizationType: auth.tokenType, hasProjectKey: !!headers['Project-Key'], hasApiEnv: !!headers['Api-Env'] }); try { console.log('Making API request with fetch() and DYNAMIC authentication...'); // Use fetch() API (jQuery not available) const response = await fetch(apiUrl, { method: 'POST', headers: headers, body: formData }); if (!response.ok) { const errorText = await response.text(); throw new Error(`HTTP ${response.status}: ${errorText}`); } const result = await response.json(); console.log('✅ API Response received:', result); // Handle response as array (result[0].uid) const newCompositionId = result[0].uid; console.log('✅ Composition UID extracted:', newCompositionId); return { success: true, compositionUid: newCompositionId }; } catch (error) { console.error('🚨 API Request failed:', error); console.error('Error details:', { message: error.message, name: error.name, stack: error.stack }); return { success: false, error: error.message }; } }, { composition: compositionData, auth: authData, connector: connector }); } async executeWorkflow(page, compositionData) { try { console.log('[WORKFLOW] Starting v4.0.3 dynamic authentication workflow...'); // Step 1: Authentication through JWT redirect server console.log('[WORKFLOW] Step 1: Authentication via JWT redirect server'); await page.goto('http://localhost:8080/composer', { waitUntil: 'networkidle', timeout: 30000 }); // Step 2: Wait for redirect to Composer console.log('[WORKFLOW] Step 2: Waiting for Composer redirect'); await page.waitForURL('**/composer.euconquisto.com/**', { timeout: 15000 }); // Step 3: Wait for page to be fully loaded console.log('[WORKFLOW] Step 3: Waiting for page load'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Step 4: Extract authentication data with enhanced debugging console.log('[WORKFLOW] Step 4: Extracting authentication data'); const authData = await this.extractAuthenticationData(page); console.log('[WORKFLOW] Authentication data extracted successfully'); // Step 5: Save composition directly via API with v4.0.3 dynamic auth console.log('[WORKFLOW] Step 5: Saving composition via fetch API with DYNAMIC authentication'); const saveResult = await this.saveCompositionDirectly(page, compositionData, authData); if (!saveResult.success) { throw new Error(`API save failed: ${saveResult.error}`); } console.log('[WORKFLOW] ✅ API save successful with dynamic auth, composition UID:', saveResult.compositionUid); // Step 6: Navigate to the saved composition for immediate viewing console.log('[WORKFLOW] Step 6: Navigating to saved composition'); const composerPath = `#/composer/${saveResult.compositionUid}`; const baseUrl = page.url().split('#')[0]; const finalUrl = baseUrl + composerPath; await page.goto(finalUrl); await page.waitForLoadState('networkidle'); await page.waitForTimeout(3000); console.log('[WORKFLOW] ✅ Workflow completed successfully with v4.0.3'); console.log('[WORKFLOW] Final URL:', page.url()); return { success: true, url: page.url(), compositionUid: saveResult.compositionUid }; } catch (error) { console.error('[WORKFLOW] ❌ Workflow failed:', error.message); return { success: false, error: error.message }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); } } const server = new EuConquistoComposerServer(); server.run().catch(console.error);

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