Skip to main content
Glama
browser-automation-mcp-server-v2.ts17 kB
#!/usr/bin/env node /** * Browser Automation MCP Server v2 - Estado Limpo * Versão melhorada com gestão de estado entre execuções * @version 2.1.0 */ 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, Browser, Page, BrowserContext } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; import { cwd } from 'process'; // Usar console.error para todo logging const log = (...args: any[]) => console.error('[MCP-v2]', new Date().toISOString(), ...args); class BrowserAutomationMCPServerV2 { private server: Server; private browser: Browser | null = null; private jwtToken: string | null = null; private composerBaseUrl = 'https://composer.euconquisto.com'; private activePages: Set<Page> = new Set(); constructor() { this.server = new Server( { name: 'euconquisto-composer-browser-automation', version: '2.1.0', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); this.loadJWTToken(); // Limpar recursos ao encerrar process.on('SIGINT', async () => { log('Encerrando servidor...'); await this.cleanup(); process.exit(0); }); // Limpar páginas antigas periodicamente setInterval(() => this.cleanupOldPages(), 60000); // A cada minuto } private async cleanup() { // Fechar todas as páginas abertas for (const page of this.activePages) { try { await page.close(); } catch (e) { // Ignorar erros ao fechar } } this.activePages.clear(); // Fechar o browser if (this.browser) { try { await this.browser.close(); } catch (e) { // Ignorar erros ao fechar } this.browser = null; } } private async cleanupOldPages() { const pagesToRemove: Page[] = []; for (const page of this.activePages) { try { // Verificar se a página ainda está aberta await page.title(); } catch (e) { // Página já fechada pagesToRemove.push(page); } } // Remover páginas fechadas do conjunto pagesToRemove.forEach(page => this.activePages.delete(page)); if (pagesToRemove.length > 0) { log(`Limpeza: removidas ${pagesToRemove.length} páginas fechadas`); } } private loadJWTToken() { try { const jwtPath = join(cwd(), 'correct-jwt-new.txt'); this.jwtToken = readFileSync(jwtPath, 'utf8').trim(); log('Token JWT carregado com sucesso'); } catch (error: any) { log('Aviso: Não foi possível carregar token JWT:', error.message); log('Continuando em modo demo...'); } } private async ensureBrowser(): Promise<Browser> { if (!this.browser || !this.browser.isConnected()) { log('Iniciando navegador...'); this.browser = await chromium.launch({ headless: false, args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage', '--disable-blink-features=AutomationControlled' ] }); log('Navegador iniciado com sucesso'); } return this.browser; } private async createNewContext(): Promise<BrowserContext> { const browser = await this.ensureBrowser(); // Criar um novo contexto isolado para cada composição const context = await browser.newContext({ viewport: { width: 1280, height: 720 }, locale: 'pt-BR' }); return context; } private async createCompositionInBrowser(prompt: string, title?: string): Promise<any> { log('=== Iniciando nova criação de composição ==='); // Criar novo contexto isolado const context = await this.createNewContext(); const page = await context.newPage(); this.activePages.add(page); try { // URL de autenticação const authUrl = this.jwtToken ? `${this.composerBaseUrl}/#/embed/auth-with-token/pt_br/home/36c92686-c494-ec11-a22a-dc984041c95d/${this.jwtToken}` : `${this.composerBaseUrl}?demo=true`; log('Navegando para o Composer...'); log('URL:', authUrl.substring(0, 50) + '...'); // Navegar com timeout maior await page.goto(authUrl, { waitUntil: 'networkidle', timeout: 30000 }); log('Página carregada, aguardando interface...'); await page.waitForTimeout(5000); // Tirar screenshot para debug const screenshotPath = join(cwd(), `debug-${Date.now()}.png`); await page.screenshot({ path: screenshotPath }); log(`Screenshot salva em: ${screenshotPath}`); // Gerar conteúdo const composition = this.generateComposition(prompt, title); // Tentar criar nova composição const created = await this.tryCreateNewComposition(page); if (created) { log('Nova composição criada, tentando adicionar widgets...'); // Tentar adicionar widgets await this.tryAddWidgets(page, composition); } // Resultado final const result = { success: true, message: 'Composição processada com sucesso', url: page.url(), composition: composition, timestamp: new Date().toISOString(), sessionId: Date.now() }; log('=== Composição concluída ==='); log('URL final:', page.url()); // Manter página aberta por 45 segundos para visualização setTimeout(async () => { try { this.activePages.delete(page); await page.close(); await context.close(); log('Contexto e página fechados após timeout'); } catch (e) { // Ignorar erros } }, 45000); return result; } catch (error: any) { log('ERRO na criação da composição:', error.message); // Limpar em caso de erro this.activePages.delete(page); try { await page.close(); await context.close(); } catch (e) { // Ignorar erros ao fechar } throw new Error(`Falha ao criar composição: ${error.message}`); } } private async tryCreateNewComposition(page: Page): Promise<boolean> { // Lista expandida de seletores possíveis const selectors = [ // Seletores por data-testid '[data-testid="new-composition"]', '[data-testid="create-composition"]', '[data-testid="add-composition"]', // Seletores por texto 'button:has-text("Nova Composição")', 'button:has-text("Nova composição")', 'button:has-text("New Composition")', 'button:has-text("Criar Composição")', 'button:has-text("+")', // Seletores por aria-label '[aria-label="Nova composição"]', '[aria-label="Criar nova composição"]', '[aria-label="Add new composition"]', // Seletores por classe '.new-composition', '.new-composition-button', '.create-composition', '.add-composition', // Seletores genéricos 'button[title*="Nova"]', 'button[title*="Criar"]', 'a[href*="new"]', 'a[href*="create"]' ]; for (const selector of selectors) { try { log(`Tentando selector: ${selector}`); const element = await page.waitForSelector(selector, { timeout: 2000, state: 'visible' }); if (element) { await element.click(); log(`✓ Clicado com sucesso usando: ${selector}`); await page.waitForTimeout(2000); return true; } } catch (e) { // Continuar com próximo selector } } log('⚠️ Não foi possível encontrar botão de nova composição'); return false; } private async tryAddWidgets(page: Page, composition: any): Promise<void> { // Seletores para adicionar widgets const addWidgetSelectors = [ '[data-testid="add-widget"]', 'button:has-text("Add Widget")', 'button:has-text("Adicionar Widget")', '[aria-label="Add widget"]', '.add-widget-button', 'button[title*="widget"]' ]; for (const widget of composition.widgets) { let added = false; for (const selector of addWidgetSelectors) { try { const button = await page.waitForSelector(selector, { timeout: 1000, state: 'visible' }); if (button) { await button.click(); log(`Widget adicionado usando: ${selector}`); added = true; await page.waitForTimeout(1000); break; } } catch (e) { // Tentar próximo selector } } if (!added) { log(`⚠️ Não foi possível adicionar widget: ${widget.type}`); } } } private generateComposition(prompt: string, title?: string): any { const subject = this.detectSubject(prompt); const gradeLevel = this.detectGradeLevel(prompt); const topic = this.extractTopic(prompt); log(`Gerando composição - Matéria: ${subject}, Nível: ${gradeLevel}, Tópico: ${topic}`); return { title: title || `Aula: ${topic}`, subject: subject, gradeLevel: gradeLevel, widgets: [ { type: 'header-1', content: { text: `<h1>${title || `Aula sobre ${topic}`}</h1>` } }, { type: 'text-1', content: { text: this.generateEducationalContent(prompt, subject, gradeLevel, topic) } } ] }; } private generateEducationalContent(prompt: string, subject: string, gradeLevel: string, topic: string): string { const intro = `<h2>Introdução</h2> <p>Esta aula sobre <strong>${topic}</strong> foi desenvolvida especialmente para estudantes do nível ${this.translateGradeLevel(gradeLevel)}.</p>`; const objectives = `<h2>Objetivos de Aprendizagem</h2> <ul> <li>Compreender os conceitos fundamentais de ${topic}</li> <li>Aplicar o conhecimento em situações práticas</li> <li>Desenvolver habilidades de análise e pensamento crítico</li> </ul>`; const content = `<h2>Conteúdo Principal</h2> <p>${this.generateSubjectSpecificContent(subject, topic, gradeLevel)}</p>`; const activities = `<h2>Atividades Práticas</h2> <p>Para consolidar o aprendizado, realize as seguintes atividades:</p> <ol> <li>Exercício de fixação sobre ${topic}</li> <li>Discussão em grupo sobre aplicações práticas</li> <li>Projeto individual de pesquisa</li> </ol>`; return `${intro}\n\n${objectives}\n\n${content}\n\n${activities}`; } private generateSubjectSpecificContent(subject: string, topic: string, gradeLevel: string): string { const contentMap: Record<string, string> = { 'matematica': `No estudo de ${topic}, é fundamental compreender os conceitos matemáticos base, as fórmulas aplicáveis e a resolução prática de problemas. A matemática nos permite modelar e resolver situações do mundo real.`, 'fisica': `A física de ${topic} envolve a compreensão das leis naturais, forças e interações. Através de experimentos e cálculos, podemos prever e explicar fenômenos físicos.`, 'quimica': `Em química, ${topic} abrange reações, propriedades moleculares e transformações da matéria. O conhecimento químico é essencial para compreender processos naturais e industriais.`, 'biologia': `O estudo biológico de ${topic} revela a complexidade dos sistemas vivos, desde células até ecossistemas. A biologia nos ajuda a entender a vida em todas as suas formas.`, 'historia': `A análise histórica de ${topic} nos permite compreender eventos passados, suas causas e consequências. O estudo da história é fundamental para entender o presente e planejar o futuro.`, 'portugues': `No contexto da língua portuguesa, ${topic} envolve análise textual, gramática e expressão. O domínio do idioma é essencial para comunicação efetiva.`, 'geral': `O estudo de ${topic} proporciona conhecimentos fundamentais e habilidades práticas aplicáveis em diversas áreas do conhecimento e da vida cotidiana.` }; return contentMap[subject] || contentMap['geral']; } private detectSubject(prompt: string): string { const lowerPrompt = prompt.toLowerCase(); const subjects = { 'matemática': 'matematica', 'matemática': 'matematica', 'math': 'matematica', 'física': 'fisica', 'fisica': 'fisica', 'physics': 'fisica', 'química': 'quimica', 'quimica': 'quimica', 'chemistry': 'quimica', 'biologia': 'biologia', 'biology': 'biologia', 'história': 'historia', 'historia': 'historia', 'history': 'historia', 'português': 'portugues', 'portugues': 'portugues', 'portuguese': 'portugues' }; for (const [key, value] of Object.entries(subjects)) { if (lowerPrompt.includes(key)) { return value; } } return 'geral'; } private detectGradeLevel(prompt: string): string { const lowerPrompt = prompt.toLowerCase(); if (lowerPrompt.match(/\b(fundamental|elementar|básico|criança|6º|7º|8º|9º)\b/)) { return 'fundamental'; } if (lowerPrompt.match(/\b(médio|secundário|adolescente|1º ano|2º ano|3º ano)\b/)) { return 'medio'; } if (lowerPrompt.match(/\b(superior|universitário|faculdade|graduação)\b/)) { return 'superior'; } return 'medio'; // Padrão } private extractTopic(prompt: string): string { // Remover palavras comuns para extrair o tópico principal const stopWords = ['criar', 'crie', 'uma', 'aula', 'sobre', 'de', 'para', 'com', 'em', 'o', 'a', 'os', 'as']; const words = prompt.toLowerCase() .split(/\s+/) .filter(word => word.length > 3 && !stopWords.includes(word)); // Pegar as 2-3 palavras mais relevantes return words.slice(0, 3).join(' ') || 'conteúdo educacional'; } private translateGradeLevel(level: string): string { const translations: Record<string, string> = { 'fundamental': 'Ensino Fundamental', 'medio': 'Ensino Médio', 'superior': 'Ensino Superior' }; return translations[level] || level; } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create-intelligent-composition', description: 'Criar uma composição educacional com automação de navegador no EuConquisto Composer', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'O prompt de conteúdo educacional em linguagem natural' }, title: { type: 'string', description: 'Título opcional para a composição' } }, required: ['prompt'] } } ] })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === 'create-intelligent-composition') { try { const args = request.params.arguments as any; log('=== Nova solicitação de composição ==='); log('Prompt:', args.prompt); log('Título:', args.title || '(gerado automaticamente)'); const result = await this.createCompositionInBrowser( args.prompt, args.title ); return { content: [ { type: 'text', text: JSON.stringify(result, null, 2) } ] }; } catch (error: any) { log('ERRO no handler:', error); throw new McpError( ErrorCode.InternalError, `Falha ao criar composição: ${error.message}` ); } } throw new McpError( ErrorCode.MethodNotFound, `Ferramenta desconhecida: ${request.params.name}` ); }); } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); log('Servidor MCP de Automação de Navegador v2 em execução'); log('Recursos: Gestão de estado aprimorada, contextos isolados, limpeza automática'); } } // Iniciar o servidor const server = new BrowserAutomationMCPServerV2(); server.run().catch((error) => { console.error('Falha ao iniciar servidor:', error); process.exit(1); });

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