Skip to main content
Glama
composer-mcp-server.ts10.5 kB
#!/usr/bin/env node /** * EuConquisto Composer MCP Server * Browser automation-based implementation using localStorage */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js'; import playwright from 'playwright'; import { readFileSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; import { gunzipSync, gzipSync } from 'zlib'; import { NLPWidgetParser, CompositionWidget } from './nlp-widget-parser.js'; import { StableBrowserManager, BrowserSession } from './stable-browser-manager.js'; const currentDir = dirname(fileURLToPath(import.meta.url)); interface CompositionData { version: string; metadata: { title: string; description: string; thumb?: string | null; tags: string[]; }; interface: { content_language: string; index_option: string; font_family: string; show_summary: string; finish_btn: string; }; structure: CompositionWidget[]; assets: any[]; } class ComposerMCPServer { private server: Server; private jwtToken!: string; private baseURL = 'https://composer.euconquisto.com/#/embed'; private orgId = '36c92686-c494-ec11-a22a-dc984041c95d'; private nlpParser: NLPWidgetParser; private browserManager: StableBrowserManager; constructor() { this.server = new Server( { name: 'composer-mcp-server', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.nlpParser = new NLPWidgetParser(); this.browserManager = new StableBrowserManager(); this.loadJWTToken(); this.setupToolHandlers(); } private loadJWTToken() { try { const tokenPath = resolve(currentDir, '..', 'correct-jwt-new.txt'); this.jwtToken = readFileSync(tokenPath, 'utf8').trim(); } catch (error) { throw new Error('Failed to load JWT token: ' + error); } } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'create-composition', description: 'Create a new composition from natural language prompt', inputSchema: { type: 'object', properties: { prompt: { type: 'string', description: 'Natural language description of the composition content', }, title: { type: 'string', description: 'Title for the composition', }, description: { type: 'string', description: 'Optional description for the composition', default: '', }, }, required: ['prompt', 'title'], }, }, { name: 'edit-composition', description: 'Edit an existing composition by URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The composition URL to edit', }, changes: { type: 'string', description: 'Natural language description of changes to make', }, }, required: ['url', 'changes'], }, }, { name: 'preview-composition', description: 'Preview a composition by URL', inputSchema: { type: 'object', properties: { url: { type: 'string', description: 'The composition URL to preview', }, }, required: ['url'], }, }, { name: 'list-compositions', description: 'List available compositions for the current project', inputSchema: { type: 'object', properties: {}, }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'create-composition': return await this.createComposition(args); case 'edit-composition': return await this.editComposition(args); case 'preview-composition': return await this.previewComposition(args); case 'list-compositions': return await this.listCompositions(args); default: throw new McpError(ErrorCode.MethodNotFound, `Tool ${name} not found`); } } catch (error) { throw new McpError(ErrorCode.InternalError, `Tool execution failed: ${error}`); } }); } private async createComposition(args: any) { const { prompt, title, description = '' } = args; return await this.browserManager.withStableSession(async (session: BrowserSession) => { // Click "Nova Composição" await this.browserManager.safeClick(session.page, 'button:has-text("Nova Composição")'); await session.page.waitForTimeout(2000); // Parse prompt into widgets using NLP parser const widgets = this.nlpParser.parsePromptToWidgets(prompt); // Create composition data const compositionData: CompositionData = { version: '1.1', metadata: { title, description, thumb: null, tags: [], }, interface: { content_language: 'pt_br', index_option: 'buttons', font_family: 'Lato', show_summary: 'disabled', finish_btn: 'disabled', }, structure: widgets, assets: [], }; // Set composition in localStorage await session.page.evaluate((data: any) => { // @ts-ignore - localStorage exists in browser context localStorage.setItem('rdp-composer-data', JSON.stringify(data)); }, compositionData); // Trigger save to get URL await this.browserManager.safeClick(session.page, 'button:has-text("Salvar")'); await session.page.waitForTimeout(3000); // Get the saved composition URL const savedURL = await session.page.url(); return { content: [ { type: 'text', text: `✅ Composition created successfully!\n\n🎯 **Title:** ${title}\n📝 **Content:** ${prompt}\n🔗 **URL:** ${savedURL}\n\n📊 **Widgets Created:** ${widgets.length}\n${widgets.map(w => ` • ${w.type}: ${w.category || w.content || 'content'}`).join('\n')}`, }, ], }; }, { headless: false, slowMo: 200 }); } private async editComposition(args: any) { const { url, changes } = args; return await this.browserManager.withStableSession(async (session: BrowserSession) => { // Navigate to the composition URL await session.page.goto(url, { waitUntil: 'networkidle' }); // Get current composition data const currentData = await session.page.evaluate(() => { // @ts-ignore - localStorage exists in browser context const data = localStorage.getItem('rdp-composer-data'); return data ? JSON.parse(data) : null; }); if (!currentData) { throw new Error('No composition data found in localStorage'); } // Parse changes and apply them using NLP parser const updatedWidgets = this.nlpParser.applyChangesToWidgets(currentData.structure, changes); currentData.structure = updatedWidgets; // Update localStorage await session.page.evaluate((data: any) => { // @ts-ignore - localStorage exists in browser context localStorage.setItem('rdp-composer-data', JSON.stringify(data)); }, currentData); // Save changes await this.browserManager.safeClick(session.page, 'button:has-text("Salvar")'); await session.page.waitForTimeout(3000); const savedURL = await session.page.url(); return { content: [ { type: 'text', text: `✅ Composition updated successfully!\n\n🔄 **Changes:** ${changes}\n🔗 **New URL:** ${savedURL}`, }, ], }; }, { headless: false, slowMo: 200 }); } private async previewComposition(args: any) { const { url } = args; try { // Extract composition data from URL const urlData = this.extractCompositionFromURL(url); if (!urlData) { throw new Error('Could not extract composition data from URL'); } const composition = JSON.parse(urlData); return { content: [ { type: 'text', text: `📋 **Composition Preview**\n\n🎯 **Title:** ${composition.metadata?.title || 'Untitled'}\n📝 **Description:** ${composition.metadata?.description || 'No description'}\n\n📊 **Structure:**\n${composition.structure?.map((widget: any, i: number) => `${i + 1}. **${widget.type}**: ${widget.category || widget.content || widget.text || 'content'}` ).join('\n') || 'No widgets found'}`, }, ], }; } catch (error) { throw new Error(`Failed to preview composition: ${error}`); } } private async listCompositions(args: any) { // This would require browser automation to access the composition list // For now, return a placeholder return { content: [ { type: 'text', text: `📋 **Available Compositions**\n\nTo list compositions, you need to access the Composer interface.\nUse the create-composition tool to create new compositions.`, }, ], }; } private extractCompositionFromURL(url: string): string | null { try { const match = url.match(/\/composer\/([A-Za-z0-9+/=]+)$/); if (!match) return null; const encodedData = match[1]; const buffer = Buffer.from(encodedData, 'base64'); const decompressed = gunzipSync(buffer); return decompressed.toString(); } catch (error) { return null; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); } } const server = new ComposerMCPServer(); 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