Skip to main content
Glama
browser-automation-api-direct-save-v4.0.3.js17.5 kB
#!/usr/bin/env node /** * EuConquisto Composer Browser Automation MCP Server - Claude Guided * @version 4.1.0-claude-guided * @description Claude-guided content generation with working navigation flow */ 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'; // CLAUDE GUIDED COMPOSER MODULE import { createClaudeGuidedComposer } from './claude-guided-composer.js'; const PROJECT_ROOT = '/Users/ricardokawasaki/Desktop/euconquisto-composer-mcp-poc'; class EuConquistoComposerServer { constructor() { this.server = new Server( { name: 'euconquisto-composer-browser-automation', version: '4.1.0-claude-guided', }, { capabilities: { tools: {}, }, } ); // CLAUDE GUIDED COMPOSER COMPONENT this.claudeGuidedComposer = createClaudeGuidedComposer(); console.error('[INIT] Claude Guided Composer system initialized'); 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: 'get_lesson_guidance', description: 'STEP 1: Get comprehensive guidance for creating educational content. This provides widget options and pedagogical frameworks. After using this tool, you MUST use create_educational_composition to actually save the lesson.', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'Educational content prompt or topic' }, 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'] } }, { name: 'create_educational_composition', description: 'STEP 2: Save the lesson to EuConquisto Composer. Use this after getting guidance and creating your lesson content. Provide the complete lesson data including widgets and metadata.', inputSchema: { type: 'object', properties: { lessonData: { type: 'object', description: 'Complete lesson data with widgets array and metadata object' } }, required: ['lessonData'] } } ] })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'get_lesson_guidance') { return this.provideLessonGuidance(request.params.arguments); } else if (request.params.name === 'create_educational_composition') { return this.createCompositionFromElements(request.params.arguments); } throw new McpError( ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}` ); }); } async provideLessonGuidance(args) { const { prompt, subject = 'Ciências', gradeLevel = '7º ano' } = args || {}; console.error('[GUIDANCE] Providing lesson guidance for Claude'); const guidance = await this.claudeGuidedComposer.provideLessonGuidance(prompt, subject, gradeLevel); return { content: [ { type: 'text', text: JSON.stringify(guidance, null, 2) } ] }; } async createCompositionFromElements(args) { const { lessonData } = 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.' } ] }; } if (!lessonData || !lessonData.widgets) { return { content: [ { type: 'text', text: 'Invalid lesson data. Please provide lessonData with widgets array and metadata.' } ] }; } console.error('[COMPOSITION] Creating composition from Claude-provided elements and metadata'); let browser; try { // Convert Claude's output to Composer format const composition = await this.claudeGuidedComposer.formatForComposer(lessonData); // DEBUG: Write composition data to file try { const fs = await import('fs'); const debugPath = `/Users/ricardokawasaki/Desktop/debug-claude-composition-${Date.now()}.json`; fs.writeFileSync(debugPath, JSON.stringify(composition, null, 2)); console.error(`[DEBUG] Composition data written to: ${debugPath}`); } catch (e) { console.error('Debug composition write failed:', e.message); } browser = await chromium.launch({ headless: false, slowMo: 100 }); const context = await browser.newContext({ viewport: { width: 1920, height: 1080 } }); const page = await context.newPage(); page.on('console', msg => { console.error(`[BROWSER CONSOLE] ${msg.text()}`); }); const result = await this.executeWorkflow(page, composition); if (result.success && result.url && result.compositionUid) { const responseText = `✅ LESSON CREATED SUCCESSFULLY! Title: ${composition.metadata.title} Widgets: ${composition.structure.length} Widget Types: ${composition.structure.map(e => e.type).join(', ')} Composition UID: ${result.compositionUid} URL: ${result.url} 🎯 Browser stays open for interaction!`; return { content: [ { type: 'text', text: responseText } ] }; } else { // Get actual error details and page URL for debugging const currentUrl = page ? page.url() : 'No page URL available'; const errorText = `❌ Composition creation failed: ${result.error || 'Unknown error'} 🔧 DEBUG INFO: - Current page URL: ${currentUrl} - Workflow success: ${result.success} - Has composition UID: ${!!result.compositionUid} - Has URL: ${!!result.url} Browser staying open for debugging. Check browser console for detailed errors.`; return { content: [ { type: 'text', text: errorText } ] }; } } catch (error) { const errorResponse = `❌ Composition creation failed: ${error.message} Browser remains open for debugging. Check console for detailed error information.`; return { content: [ { type: 'text', text: errorResponse } ] }; } } /** * Extract authentication data from localStorage */ async extractAuthenticationData(page) { return await page.evaluate(() => { console.error('=== AUTHENTICATION EXTRACTION DEBUG v4.0.3 ==='); const activeProject = localStorage.getItem('rdp-composer-active-project'); const userData = localStorage.getItem('rdp-composer-user-data'); console.error('Raw activeProject:', activeProject); console.error('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.error('Parsed projectData:', projectData); console.error('Parsed userData keys:', Object.keys(userDataParsed)); const result = { projectUid: projectData.uid, connectors: projectData.connectors || [], accessToken: userDataParsed.access_token, tokenType: userDataParsed.token_type || 'Bearer' }; console.error('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 */ getCorrectConnector(connectors) { console.error(`[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.error('[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.error('[DEBUG] Found Composer connector:', composerConnector.uid); return composerConnector; } // Fallback to first available connector if (connectors.length > 0) { console.error('[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 with dynamic authentication */ 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.error('=== 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.error('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.error('=== AUTHENTICATION DEBUG v4.0.3 ==='); console.error('Dynamic projectUid:', projectUid); console.error('Dynamic connectorUid:', connectorUid); console.error('Token type:', auth.tokenType); console.error('Token present:', !!auth.accessToken); console.error('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.error('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.error('Request headers:', { hasAuthorization: !!headers['Authorization'], authorizationType: auth.tokenType, hasProjectKey: !!headers['Project-Key'], hasApiEnv: !!headers['Api-Env'] }); try { console.error('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.error('✅ API Response received:', JSON.stringify(result, null, 2)); // Handle response - check structure let newCompositionId; if (Array.isArray(result) && result[0] && result[0].uid) { newCompositionId = result[0].uid; } else if (result.uid) { newCompositionId = result.uid; } else if (result.id) { newCompositionId = result.id; } else if (result.compositionId) { newCompositionId = result.compositionId; } else { console.error('❌ Unexpected API response structure:', result); throw new Error('Could not extract composition ID from API response'); } console.error('✅ Composition UID extracted:', newCompositionId); console.error('✅ UID type:', typeof newCompositionId); console.error('✅ UID length:', newCompositionId.length); 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.error('[WORKFLOW] Starting v4.0.3 dynamic authentication workflow...'); // Step 1: Authentication through JWT redirect server console.error('[WORKFLOW] Step 1: Authentication via JWT redirect server'); await page.goto('http://localhost:8080', { waitUntil: 'networkidle', timeout: 30000 }); // Step 2: Wait for redirect to Composer console.error('[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.error('[WORKFLOW] Step 3: Waiting for page load'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(2000); // Step 4: Extract authentication data with enhanced debugging console.error('[WORKFLOW] Step 4: Extracting authentication data'); const authData = await this.extractAuthenticationData(page); console.error('[WORKFLOW] Authentication data extracted successfully'); // Step 5: Save composition directly via API with v4.0.3 dynamic auth console.error('[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.error('[WORKFLOW] ✅ API save successful with dynamic auth, composition UID:', saveResult.compositionUid); // Step 6: Navigate to the saved composition for immediate viewing console.error('[WORKFLOW] Step 6: Navigating to saved composition'); const composerPath = `#/composer/${saveResult.compositionUid}`; const baseUrl = page.url().split('#')[0]; const finalUrl = baseUrl + composerPath; console.error('[WORKFLOW] Navigating to final URL:', finalUrl); await page.goto(finalUrl); await page.waitForLoadState('networkidle'); await page.waitForTimeout(3000); console.error('[WORKFLOW] ✅ Workflow completed successfully with v4.0.3'); console.error('[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