browser-automation-api-direct-save-v4.0.3.js•17.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);