composition-lifecycle-backup.ts•26.4 kB
/**
* EuConquisto Composer MCP Server - Composition Lifecycle Tools
*
* @version 0.1.4
* @created 2025-06-09
* @updated 2025-06-09
* @author EuConquisto Development Team
*
* @description Implements the complete composition lifecycle management:
* 1. Create new composition via "Nova composição" button
* 2. Edit composition metadata via hamburger menu → "Configurações"
* 3. Save composition via "Salvar" button with URL-based persistence
*
* This implementation uses browser automation to interact with the Composer interface
* and provides URL encoding/decoding for composition persistence.
*/
import { z } from "zod";
import { chromium, Browser, Page, BrowserContext } from "playwright";
/**
* Browser automation client for Composer interface interactions
*/
export class ComposerClient {
private browser: Browser | BrowserContext | null = null;
private page: Page | null = null;
private readonly baseURL = "https://composer.euconquisto.com/#/embed";
private readonly orgId = "36c92686-c494-ec11-a22a-dc984041c95d";
private readonly jwtToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImFkbWluLmRlc2Vudm9sdmltZW50b0BldWNvbnF1aXN0by5jb20iLCJuYW1lIjoiQWRtaW4gRGV2Iiwib2lkIjoiNWZiM2RlYzYtYzQ5NC1lYzExLWEyMmEtZGM5ODQwNDFjOTVkIiwiZGlyZWN0b3J5IjoiYjBmZWY4NjAtYzQ5NC1lYzExLWEyMmEtZGM5ODQwNDFjOTVkIiwiYXBpbSI6IkVFQzUzQTI0LUVDMEUtNDFCOS05NDA1LTg2QTE3NTAwREIzNCIsImRpcm4iOiJEZXNlbnZvbHZpbWVudG8iLCJyb2xlIjpbIlJldmlld0NvbnRyaWJ1dG9yIiwiQ29udHJpYnV0b3JHbG9iYWwiLCJBZG1pbmlzdHJhdG9yR2xvYmFsIl0sImRjbiI6WyJINHNJQUFBQUFBQUFBMkptWUdBd1pRQ0JaQXVqRkdNREM0dFYyMXRTRS1vZHA5NElpMnVVYTNqMXNxRXl6YWhrN3ZxWlN5MG5sLV94c3cyb0xEN05jT2ppRThWelhyZDBEamF3XzQyVW1fZmxrT1loZzlVN0ZKOGx5N1dWQVFBQUFQX18iLCJINHNJQUFBQUFBQUFBMkptWUdBd1pRQ0JaQXVqRkdNREM0c2xCWS0zTjB5WkU1S1NNSFhKTzhjcm0wNS1remgtYVBzTXhtMnZvOTBxYzhXTjktX08wRzRMbmo5dDRqb2R4bnZNdVkycHJEd1NrYlkyamJOMnpuNXJvemJmR3dBQUFQX18iLCJINHNJQUFBQUFBQUFBMkptWUdBd1pRQ0JaQXVqRkdNREM0dnpkMWU1SDFGTzVhenRyR1RuZXZ6emx0U0djM3VFT1IteHExWXZNVHF5NGstaThXR0gwd292cC1ka3FjNm82d2xSZkM0d3k5MVcxSHFqOFU4aEMtRWZMa0pSQUFBQUFQX18iLCJINHNJQUFBQUFBQUFBMkptWUdBd1pRQ0JaQXVqRkdNREM0dU5XMnUyS3A5dXZhUGhkOFNpOE9KNV8yTmRmZnNTSC0temZHTXVmX084Wm5GbXFXRE5qeE1ubnBWelRkVTBPVF9mUldtNVVhTkxlV2oyTjY5Y3JRS0x1SmJQQUFBQUFQX18iLCJINHNJQUFBQUFBQUFBMkptWUdBd1pRQ0JaQXVqRkdNREM0czY2NE5odVhwck52S3FiV1RuUEtGNTU2UzRycXdXNDJ1SnkzbjNGVkxFVjZ4VlljdU9kTnVrZXliMjdKbjRlOS1GaGNNdW1iOTZ1cUx0aEptSDJrX0h0U29MQUFBQUFQX18iLCJINHNJQUFBQUFBQUFBMkptWUdBd1pRQ0JaQXVqRkdNREN3dWpCWjhyQW81RUxEaHRvQ2ZUdmVQcTVET2hpd1dtM3A2eFktTDdIYkpTb2RFU2I2SUVWQ3BQcXpETTJ2WlR6VzlsMXBrWlBFb3Zkbl8tV3R2LWY0bkt3dmEtU0FBQUFBRF9fdyJdLCJzY3AiOlsiSDRzSUFBQUFBQUFBQTJKbVlHRHdZQUFCMDZRMDA5VFU1RlNKTmZzV1NKNDRWWGZBeHZYeDVEc3lzaFhPNmllVGk0cWVYbTZaVVN2QzZ2djNST0hNRDBkVHBDVHUzekFfVUMtWVVIOHIyMzNlS29mQ0hZR3UwWG83aWswWGROWVVxdmotV0hfclhiU3VVYnJZM1VZQUFBQUFfXzgiLCJINHNJQUFBQUFBQUFBd0RTQWkzOUF3QUFBck1BQUFBQUFHRTVaalF6TkRsa2E0TWozbjdQejY0dmpPSWl5LVlNb3dhRDdlMWpqcDZJakE2SVFZMi14TFNOTWFSZWNrTTdzWEJKamR2SkpoeUF5TnZJVHQ5aUFFWHcybVVzM3lIbGc2UHNtSld1Mm1EU2UwbXlrYnQ0OGl1NTlkYTIxMVBNdkxyRzdxOV9teDlhYWI0SjdZbWlNODg2OTVUclk3WFY1Nmlsb3UwdnQ4VmdBMWptbkdYd0paa0ZDTXhYaUlhcGl5S01TVGlfa3BnREdQdlRlYUNNdE5GSXlIU0gwUTZ3bTFwRkJBYzRMQ1dqRThMeWtnc1ZtbEJxRG9aek1oZTVnY21sZHl0VW1FVFltakIyakVaUHdvd0M2NFRZb1JudDJOWkpmRDdDZXg1T3FqY1ptVU5tNjMtVlRYOFh0UEtza1BpSnpIb3kzcHF5dXVycnVrTndZenducHpwak51OEdKZXQ4V2RoMl85ckNoVnpPUnR4N1N5UXEyb2prUmJfWEphenFtTnEwMEVTcTNSZUVSX0pQZmhGOGg4bGlxcGVWeE1FSG5RUGFmMXp2N3FVQ3JzTWtrVjc1TmxnRGVUZ1YvWGVyX0Rud3FkM05VNXNPVVJXWVU5RE1uTkxXUTVMTGRBNDYxUlFRSjVUNU5MMGQ2UGZqVWV4dkZTR0RQdW5jcHAxeDA0TlBOM2ttTmdMSlF3eDZLZjRxLVBZbDVXWmFQUTlXU0NLNHBmeHY3ZTJRZEw0X1IwQ1c3NUdQRHh1bEZQZG1tcmRrc0k2RVhqUTFkQTdkZlNFUnJoZHdvOUhCN0xGMzRYSENnMWg3Z2I4UWVKdmhMTXRjUmtlUzVpQkctOGNfNkdCQU9YUDdQR2ptQ1NUZzV2QUFBQURfX3ciXSwibmJmIjoxNzQ4ODc2OTU1LCJleHAiOjE3NTE0Njg5NTUsImlhdCI6MTc0ODg3Njk1NSwiaXNzIjoiaHR0cHM6Ly9hcGkuZGlnaXRhbHBhZ2VzLmNvbS5iciIsImF1ZCI6IkV1Q29ucXVpc3RvIn0.iTUfl6-mwLwFaxYYPf6PufRSYbSJlw3tKejmbc5G42g"; // TASK-001 FIX: Using exact JWT token from user's working URL
/**
* Initialize browser automation
*/
async initialize(): Promise<void> {
try {
// TASK-001 SOLUTION: Brave-like configuration that hides automation detection
this.browser = await chromium.launch({
headless: false,
args: [
'--no-sandbox',
'--disable-dev-shm-usage',
'--enable-features=VizDisplayCompositor',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
],
timeout: 60000
});
const context = await this.browser.newContext({
viewport: { width: 1280, height: 720 },
permissions: ['notifications'],
colorScheme: 'light',
timezoneId: 'America/New_York'
});
this.page = await context.newPage();
// TASK-001 Enhanced: Longer timeouts for SPA loading
this.page.setDefaultTimeout(60000);
this.page.setDefaultNavigationTimeout(120000);
// TASK-001 Enhanced: Add error handling and logging
this.page.on('console', msg => console.log(`PAGE: ${msg.text()}`));
this.page.on('pageerror', error => console.error(`PAGE ERROR: ${error.message}`));
this.page.on('requestfailed', request => console.error(`REQUEST FAILED: ${request.url()}`));
console.log('✅ Enhanced browser automation initialized with manual-matching context');
} catch (error) {
console.error('Failed to initialize browser automation:', error);
throw error;
}
}
/**
* Navigate to Composer with authentication
*/
async navigateToComposer(path: string = 'home'): Promise<void> {
if (!this.page) {
throw new Error('Browser not initialized. Call initialize() first.');
}
const embedURL = `${this.baseURL}/auth-with-token/pt_br/${path}/${this.orgId}/${this.jwtToken}`;
try {
console.log('🌐 TASK-001: Navigating to Composer with enhanced SPA handling...');
console.log(`🔗 URL: ${embedURL.substring(0, 80)}...`);
// TASK-001 Enhanced: Navigate with better wait conditions
const response = await this.page.goto(embedURL, {
waitUntil: 'domcontentloaded', // Don't wait for all resources
timeout: 120000
});
if (!response || !response.ok()) {
throw new Error(`Navigation failed with status: ${response?.status()}`);
}
console.log('⏳ Waiting for JavaScript application to initialize...');
// TASK-001 Enhanced: Progressive waiting for SPA initialization
let attempts = 0;
const maxAttempts = 12; // 60 seconds total (5s * 12)
while (attempts < maxAttempts) {
attempts++;
console.log(` 🔄 Attempt ${attempts}/${maxAttempts}: Checking for app initialization...`);
// Wait for JavaScript to execute
await this.page.waitForTimeout(5000);
// Check if app has initialized by looking for interactive elements
const hasInteractiveElements = await this.page.evaluate(() => {
const buttons = (globalThis as any).document.querySelectorAll('button, a, [role="button"], input[type="button"]').length;
const hasContent = (globalThis as any).document.body.textContent &&
(globalThis as any).document.body.textContent.length > 100 &&
!(globalThis as any).document.body.textContent.includes('You need to enable JavaScript');
return buttons > 0 || hasContent;
});
if (hasInteractiveElements) {
console.log('✅ JavaScript application initialized successfully');
break;
}
if (attempts === maxAttempts) {
console.warn('⚠️ App may not have fully initialized, but continuing...');
}
}
// Take debug screenshot
await this.page.screenshot({
path: `debug-navigation-${Date.now()}.png`,
fullPage: true
});
console.log('✅ Navigation completed with enhanced SPA handling');
} catch (error) {
console.error('❌ Enhanced navigation failed:', error);
// Take error screenshot
if (this.page) {
await this.page.screenshot({
path: `error-navigation-${Date.now()}.png`,
fullPage: true
});
}
throw error;
}
}
/**
* Create a new composition by clicking "Nova composição" button
*/
async createNewComposition(): Promise<{ success: boolean; message: string; compositionId?: string }> {
if (!this.page) {
throw new Error('Browser not initialized');
}
try {
console.log('Creating new composition...');
// TASK-001 Enhanced: Wait for page and JavaScript app to be ready
console.log('⏳ Ensuring application is fully loaded...');
// Wait for network to settle
try {
await this.page.waitForLoadState('networkidle', { timeout: 30000 });
} catch (error) {
console.log('⚠️ Network still busy, but continuing...');
}
// Additional wait for JavaScript frameworks to initialize
await this.page.waitForTimeout(3000);
// TASK-001 SOLUTION: Target exact "NOVA COMPOSIÇÃO" button from working screenshot
const newCompositionSelectors = [
// Exact text match from screenshot
'text=NOVA COMPOSIÇÃO',
'button:has-text("NOVA COMPOSIÇÃO")',
'[role="button"]:has-text("NOVA COMPOSIÇÃO")',
// Common variations
'text="Nova Composição"',
'button:has-text("Nova Composição")',
'text="Nova composição"',
'button:has-text("Nova composição")',
// CSS selectors for button styling seen in screenshot
'button[class*="btn"]',
'.btn-primary',
'button[style*="background"]',
// Fallback patterns
'button:text("NOVA")',
'button:text("Nova")',
'a:text("NOVA COMPOSIÇÃO")',
// Generic button search
'button',
'[role="button"]'
];
let clicked = false;
for (const selector of newCompositionSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
await this.page.click(selector);
clicked = true;
console.log(`Clicked new composition button with selector: ${selector}`);
break;
} catch (error) {
// Try next selector
continue;
}
}
if (!clicked) {
// Fallback: search for any button containing "Nova" or "composição"
const fallbackSelector = 'button';
const buttons = await this.page.locator(fallbackSelector).all();
for (const button of buttons) {
const text = await button.textContent();
if (text && (text.includes('Nova') || text.includes('composição'))) {
await button.click();
clicked = true;
console.log(`Clicked new composition button via text search: "${text}"`);
break;
}
}
}
if (!clicked) {
throw new Error('Could not find "Nova composição" button');
}
// Wait for navigation or modal to appear
await this.page.waitForTimeout(2000);
// Check if we're now in composition editor or a modal appeared
const currentURL = this.page.url();
const compositionId = this.extractCompositionIdFromURL(currentURL);
return {
success: true,
message: 'New composition created successfully',
compositionId: compositionId || 'new'
};
} catch (error) {
console.error('Failed to create new composition:', error);
return {
success: false,
message: `Failed to create new composition: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Edit composition metadata via hamburger menu → "Configurações"
*/
async editCompositionMetadata(metadata: {
title?: string;
description?: string;
author?: string;
tags?: string[];
}): Promise<{ success: boolean; message: string }> {
if (!this.page) {
throw new Error('Browser not initialized');
}
try {
console.log('Opening composition metadata editor...');
// Look for hamburger menu button
const hamburgerSelectors = [
'[data-testid="hamburger-menu"]',
'.hamburger-menu',
'[aria-label*="menu"]',
'button[class*="hamburger"]',
'button[class*="menu"]',
'[data-cy="hamburger-menu"]'
];
let menuOpened = false;
for (const selector of hamburgerSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
await this.page.click(selector);
menuOpened = true;
console.log(`Opened hamburger menu with selector: ${selector}`);
break;
} catch (error) {
continue;
}
}
if (!menuOpened) {
// Fallback: look for three dots or lines pattern
const menuIcons = await this.page.locator('button').all();
for (const icon of menuIcons) {
const innerHTML = await icon.innerHTML();
if (innerHTML.includes('⋮') || innerHTML.includes('≡') ||
innerHTML.includes('menu') || innerHTML.includes('bars')) {
await icon.click();
menuOpened = true;
console.log('Opened hamburger menu via icon search');
break;
}
}
}
if (!menuOpened) {
throw new Error('Could not find hamburger menu button');
}
// Wait for menu to appear
await this.page.waitForTimeout(1000);
// Look for "Configurações" option
const configSelectors = [
'a:has-text("Configurações")',
'button:has-text("Configurações")',
'[data-testid="settings"]',
'[data-cy="settings"]',
'li:has-text("Configurações")',
'.menu-item:has-text("Configurações")'
];
let configOpened = false;
for (const selector of configSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 3000 });
await this.page.click(selector);
configOpened = true;
console.log(`Opened configurations with selector: ${selector}`);
break;
} catch (error) {
continue;
}
}
if (!configOpened) {
// Fallback: search menu items for text containing "Configurações"
const menuItems = await this.page.locator('a, button, li').all();
for (const item of menuItems) {
const text = await item.textContent();
if (text && text.includes('Configurações')) {
await item.click();
configOpened = true;
console.log(`Opened configurations via text search: "${text}"`);
break;
}
}
}
if (!configOpened) {
throw new Error('Could not find "Configurações" option in menu');
}
// Wait for configuration modal/page to load
await this.page.waitForTimeout(2000);
// Fill metadata fields
if (metadata.title) {
await this.fillField(['[name="title"]', '#title', '[data-testid="title"]'], metadata.title);
}
if (metadata.description) {
await this.fillField(['[name="description"]', '#description', '[data-testid="description"]', 'textarea'], metadata.description);
}
if (metadata.author) {
await this.fillField(['[name="author"]', '#author', '[data-testid="author"]'], metadata.author);
}
if (metadata.tags && metadata.tags.length > 0) {
await this.fillField(['[name="tags"]', '#tags', '[data-testid="tags"]'], metadata.tags.join(', '));
}
// Save changes
const saveSelectors = [
'button:has-text("Salvar")',
'button:has-text("Save")',
'[data-testid="save"]',
'.save-btn',
'button[type="submit"]'
];
for (const selector of saveSelectors) {
try {
await this.page.click(selector);
console.log(`Saved metadata with selector: ${selector}`);
break;
} catch (error) {
continue;
}
}
return {
success: true,
message: 'Composition metadata updated successfully'
};
} catch (error) {
console.error('Failed to edit composition metadata:', error);
return {
success: false,
message: `Failed to edit metadata: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Save composition via "Salvar" button
*/
async saveComposition(): Promise<{ success: boolean; message: string; compositionURL?: string }> {
if (!this.page) {
throw new Error('Browser not initialized');
}
try {
console.log('Saving composition...');
// Look for "Salvar" button
const saveSelectors = [
'button:has-text("Salvar")',
'[data-testid="save-composition"]',
'.save-composition-btn',
'[data-cy="save"]',
'button[aria-label*="salvar"]',
'button[title*="Salvar"]'
];
let saved = false;
for (const selector of saveSelectors) {
try {
await this.page.waitForSelector(selector, { timeout: 5000 });
await this.page.click(selector);
saved = true;
console.log(`Saved composition with selector: ${selector}`);
break;
} catch (error) {
continue;
}
}
if (!saved) {
// Fallback: search for any button containing "Salvar"
const buttons = await this.page.locator('button').all();
for (const button of buttons) {
const text = await button.textContent();
if (text && text.includes('Salvar')) {
await button.click();
saved = true;
console.log(`Saved composition via text search: "${text}"`);
break;
}
}
}
if (!saved) {
throw new Error('Could not find "Salvar" button');
}
// Wait for save operation to complete
await this.page.waitForTimeout(3000);
// Get the updated URL (should contain encoded composition data)
const currentURL = this.page.url();
return {
success: true,
message: 'Composition saved successfully',
compositionURL: currentURL
};
} catch (error) {
console.error('Failed to save composition:', error);
return {
success: false,
message: `Failed to save composition: ${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
/**
* Helper method to fill form fields with multiple selector fallbacks
*/
private async fillField(selectors: string[], value: string): Promise<void> {
for (const selector of selectors) {
try {
await this.page!.waitForSelector(selector, { timeout: 3000 });
await this.page!.fill(selector, value);
console.log(`Filled field with selector: ${selector}`);
return;
} catch (error) {
continue;
}
}
console.warn(`Could not find field for selectors: ${selectors.join(', ')}`);
}
/**
* Extract composition ID from URL
*/
private extractCompositionIdFromURL(url: string): string | null {
try {
// Look for encoded composition data in URL
const match = url.match(/\/composer\/([A-Za-z0-9+/=]+)/);
return match ? match[1] : null;
} catch (error) {
return null;
}
}
/**
* Close browser automation
*/
async close(): Promise<void> {
try {
if (this.browser) {
await this.browser.close();
this.browser = null;
this.page = null;
console.log('Browser automation closed');
}
} catch (error) {
console.error('Error closing browser:', error);
}
}
}
/**
* URL codec for composition persistence
*/
export class CompositionURLCodec {
/**
* Encode composition data to URL format
*/
static encode(compositionData: any): string {
try {
const jsonString = JSON.stringify(compositionData);
const base64 = Buffer.from(jsonString).toString('base64');
return base64;
} catch (error) {
console.error('Failed to encode composition data:', error);
throw new Error('Composition encoding failed');
}
}
/**
* Decode composition data from URL format
*/
static decode(encodedData: string): any {
try {
const jsonString = Buffer.from(encodedData, 'base64').toString('utf-8');
const compositionData = JSON.parse(jsonString);
return compositionData;
} catch (error) {
console.error('Failed to decode composition data:', error);
throw new Error('Composition decoding failed');
}
}
/**
* Extract composition data from full URL
*/
static extractFromURL(url: string): any | null {
try {
const match = url.match(/\/composer\/([A-Za-z0-9+/=]+)/);
if (match) {
return this.decode(match[1]);
}
return null;
} catch (error) {
console.error('Failed to extract composition from URL:', error);
return null;
}
}
}
/**
* MCP Tool schemas for composition lifecycle
*/
export const CompositionSchemas = {
createComposition: z.object({
navigate: z.boolean().optional().describe("Navigate to Composer first (default: true)")
}),
editMetadata: z.object({
title: z.string().optional().describe("Composition title"),
description: z.string().optional().describe("Composition description"),
author: z.string().optional().describe("Composition author"),
tags: z.array(z.string()).optional().describe("Composition tags")
}),
saveComposition: z.object({
returnURL: z.boolean().optional().describe("Return the composition URL (default: true)")
})
};
/**
* Composition lifecycle tool implementations for MCP server
*/
export class CompositionLifecycleTools {
private static client: ComposerClient | null = null;
/**
* Get or create composer client instance
*/
private static async getClient(): Promise<ComposerClient> {
if (!this.client) {
this.client = new ComposerClient();
await this.client.initialize();
}
return this.client;
}
/**
* Create new composition tool
*/
static async createNewComposition(params: z.infer<typeof CompositionSchemas.createComposition>) {
try {
const client = await this.getClient();
if (params.navigate !== false) {
await client.navigateToComposer();
}
const result = await client.createNewComposition();
return {
content: [{
type: "text",
text: JSON.stringify({
success: result.success,
message: result.message,
compositionId: result.compositionId,
nextStep: "Use edit-composition-metadata to set title, description, etc.",
timestamp: new Date().toISOString()
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text",
text: JSON.stringify({
success: false,
message: `Failed to create composition: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: new Date().toISOString()
}, null, 2)
}]
};
}
}
/**
* Edit composition metadata tool
*/
static async editCompositionMetadata(params: z.infer<typeof CompositionSchemas.editMetadata>) {
try {
const client = await this.getClient();
const result = await client.editCompositionMetadata(params);
return {
content: [{
type: "text" as const,
text: JSON.stringify({
success: result.success,
message: result.message,
metadata: params,
nextStep: "Use save-composition to persist the changes",
timestamp: new Date().toISOString()
}, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text" as const,
text: JSON.stringify({
success: false,
message: `Failed to edit metadata: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: new Date().toISOString()
}, null, 2)
}]
};
}
}
/**
* Save composition tool
*/
static async saveComposition(params: z.infer<typeof CompositionSchemas.saveComposition>) {
try {
const client = await this.getClient();
const result = await client.saveComposition();
const responseData: any = {
success: result.success,
message: result.message,
timestamp: new Date().toISOString()
};
if (params.returnURL !== false && result.compositionURL) {
responseData.compositionURL = result.compositionURL;
// Try to extract composition data from URL
const compositionData = CompositionURLCodec.extractFromURL(result.compositionURL);
if (compositionData) {
responseData.compositionData = compositionData;
}
}
return {
content: [{
type: "text" as const,
text: JSON.stringify(responseData, null, 2)
}]
};
} catch (error) {
return {
content: [{
type: "text" as const,
text: JSON.stringify({
success: false,
message: `Failed to save composition: ${error instanceof Error ? error.message : 'Unknown error'}`,
timestamp: new Date().toISOString()
}, null, 2)
}]
};
}
}
/**
* Cleanup resources
*/
static async cleanup(): Promise<void> {
if (this.client) {
await this.client.close();
this.client = null;
}
}
}