composer-mcp-server.ts•10.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);