Skip to main content
Glama
claude-native-mcp-server.ts16 kB
#!/usr/bin/env node /** * Claude Native MCP Server * * This server leverages Claude Desktop's natural content generation and widget selection abilities. * No templates, no regex patterns - just pure content-to-widget mapping. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, CallToolResult, } from '@modelcontextprotocol/sdk/types.js'; import { chromium, Browser, Page, BrowserContext } from 'playwright'; import { readFileSync } from 'fs'; import { join } from 'path'; import { cwd } from 'process'; const __dirname = cwd(); // Log to stderr to avoid interfering with JSON protocol const log = (...args: any[]) => console.error('[Claude-Native-MCP]', ...args); interface BrowserSession { page: Page; browser: Browser; context: BrowserContext; } // Educational content structure that Claude will provide interface ClaudeEducationalContent { title: string; gradeLevel: string; subject: string; duration: number; widgets: Array<{ type: string; content: any; order: number; }>; } // Browser manager for Composer integration class ComposerBrowserManager { private browser: Browser | null = null; async withSession<T>( callback: (session: BrowserSession) => Promise<T> ): Promise<T> { try { this.browser = await chromium.launch({ headless: false, args: [ '--disable-blink-features=AutomationControlled', '--no-sandbox', '--disable-dev-shm-usage' ], timeout: 120000 }); const context = await this.browser.newContext({ viewport: { width: 1280, height: 720 }, locale: 'pt-BR', permissions: ['notifications'] }); const page = await context.newPage(); page.setDefaultTimeout(60000); page.setDefaultNavigationTimeout(120000); const session: BrowserSession = { page, browser: this.browser, context }; const result = await callback(session); // Keep browser open indefinitely for user interaction log('Browser window will remain open for user interaction'); // Return result immediately but don't close browser return result; } catch (error) { if (this.browser) { await this.browser.close(); } throw error; } } } // Maps Claude's natural content structure to Composer JSON format class ClaudeToComposerMapper { public mapToComposerFormat(claudeContent: ClaudeEducationalContent): any { const timestamp = Date.now(); return { version: "1.1", metadata: { title: claudeContent.title, description: `Conteúdo educacional criado por Claude para ${claudeContent.gradeLevel}`, thumb: null, tags: [ claudeContent.gradeLevel, claudeContent.subject, `${claudeContent.duration}-minutos`, "claude-generated" ] }, interface: { content_language: "pt_br", index_option: "buttons", font_family: "Lato", show_summary: "enabled", finish_btn: "enabled" }, structure: this.mapWidgets(claudeContent.widgets, timestamp), assets: [] }; } private mapWidgets(widgets: ClaudeEducationalContent['widgets'], timestamp: number): any[] { return widgets .sort((a, b) => a.order - b.order) .map((widget, index) => { const widgetId = `${widget.type}-${timestamp}-${index}`; switch (widget.type) { case 'header': return this.mapHeaderWidget(widgetId, widget.content); case 'text': return this.mapTextWidget(widgetId, widget.content); case 'video': return this.mapVideoWidget(widgetId, widget.content); case 'flashcards': return this.mapFlashcardsWidget(widgetId, widget.content); case 'quiz': return this.mapQuizWidget(widgetId, widget.content); case 'image': return this.mapImageWidget(widgetId, widget.content); case 'accordion': return this.mapAccordionWidget(widgetId, widget.content); default: log(`Unknown widget type: ${widget.type}, mapping as text`); return this.mapTextWidget(widgetId, widget.content); } }); } private mapHeaderWidget(id: string, content: any): any { return { id, type: "head-1", content_title: null, primary_color: content.primaryColor || "#FFFFFF", secondary_color: content.secondaryColor || "#aa2c23", category: `<p>${content.category || 'Aula'}</p>`, background_image: content.backgroundImage || "https://pocs.digitalpages.com.br/rdpcomposer/media/head-1/background.png", avatar: content.avatar || "https://pocs.digitalpages.com.br/rdpcomposer/media/head-1/avatar.png", avatar_border_color: content.avatarBorderColor || "#00643e", author_name: `<p>${content.authorName || 'Professor'}</p>`, author_office: `<p>${content.authorOffice || 'Educador'}</p>`, show_category: true, show_author_name: true, show_divider: true, dam_assets: [] }; } private mapTextWidget(id: string, content: any): any { return { id, type: "text-1", content_title: content.title || null, padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", text: content.html || `<p>${content.text || ''}</p>`, dam_assets: [] }; } private mapVideoWidget(id: string, content: any): any { return { id, type: "video-1", content_title: content.title || null, padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", video: content.url || "https://pocs.digitalpages.com.br/rdpcomposer/media/video-1/video-1.mp4", dam_assets: [] }; } private mapFlashcardsWidget(id: string, content: any): any { return { id, type: "flashcards-1", content_title: content.title || null, padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", card_height: 240, card_width: 240, border_color: "#00643e", items: content.cards?.map((card: any, index: number) => ({ id: `card-${index + 1}`, front_card: { text: card.front, centered_image: null, fullscreen_image: null }, back_card: { text: card.back, centered_image: null, fullscreen_image: null }, opened: false })) || [], dam_assets: [] }; } private mapQuizWidget(id: string, content: any): any { return { id, type: "quiz-1", content_title: content.title || null, padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", primary_color: "#2d7b45", remake: "enable", max_attempts: content.maxAttempts || 3, utilization: { enabled: false, percentage: null }, feedback: { type: "default" }, questions: content.questions?.map((q: any, index: number) => ({ id: `question-${index + 1}`, question: `<p>${q.question}</p>`, image: null, video: null, answered: false, feedback_default: { text: null, image: null, video: null, media_max_width: null }, feedback_correct: { text: q.feedbackCorrect || "<p>Correto! Muito bem!</p>", image: null, video: null, media_max_width: null }, feedback_incorrect: { text: q.feedbackIncorrect || "<p>Tente novamente!</p>", image: null, video: null, media_max_width: null }, no_correct_answer: false, no_feedback: false, choices: q.choices?.map((choice: any, choiceIndex: number) => ({ id: `choice-${choiceIndex + 1}`, correct: choice.correct || false, text: `<p>${choice.text}</p>` })) || [] })) || [], dam_assets: [] }; } private mapImageWidget(id: string, content: any): any { return { id, type: "image-1", content_title: content.title || null, padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", image: content.url || "https://pocs.digitalpages.com.br/rdpcomposer/media/image-1/default.jpg", caption: content.caption || null, dam_assets: [] }; } private mapAccordionWidget(id: string, content: any): any { return { id, type: "accordion-1", content_title: content.title || null, padding_top: 35, padding_bottom: 35, background_color: "#FFFFFF", items: content.items?.map((item: any, index: number) => ({ id: `accordion-item-${index + 1}`, title: item.title, content: item.content, opened: index === 0 // First item open by default })) || [], dam_assets: [] }; } } // Main MCP Server class ClaudeNativeMCPServer { private server: Server; private browserManager: ComposerBrowserManager; private mapper: ClaudeToComposerMapper; constructor() { this.server = new Server( { name: 'euconquisto-claude-native', version: '1.0.0', }, { capabilities: { tools: {}, }, } ); this.browserManager = new ComposerBrowserManager(); this.mapper = new ClaudeToComposerMapper(); this.setupToolHandlers(); } private setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: 'save-educational-composition', description: 'Save Claude-generated educational composition to EuConquisto Composer', inputSchema: { type: 'object', properties: { composition: { type: 'object', description: 'Educational composition with Claude-selected widgets', properties: { title: { type: 'string' }, gradeLevel: { type: 'string' }, subject: { type: 'string' }, duration: { type: 'number' }, widgets: { type: 'array', items: { type: 'object', properties: { type: { type: 'string' }, content: { type: 'object' }, order: { type: 'number' } } } } }, required: ['title', 'widgets'] } }, required: ['composition'], }, }, { name: 'get-available-widgets', description: 'Get list of available Composer widgets and their capabilities', inputSchema: { type: 'object', properties: {} } } ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'save-educational-composition': return await this.saveEducationalComposition(args?.composition as ClaudeEducationalContent); case 'get-available-widgets': return await this.getAvailableWidgets(); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { throw new McpError( ErrorCode.InternalError, `Error executing tool ${name}: ${error instanceof Error ? error.message : String(error)}` ); } }); } private async saveEducationalComposition(claudeComposition: ClaudeEducationalContent): Promise<CallToolResult> { // Map Claude's content to Composer format const composerData = this.mapper.mapToComposerFormat(claudeComposition); log('📚 Mapped Claude composition to Composer format:', { title: composerData.metadata.title, widgetCount: composerData.structure.length, widgetTypes: composerData.structure.map((w: any) => w.type) }); // Save to Composer via browser const result = await this.browserManager.withSession(async (session: BrowserSession) => { // Navigate to JWT redirect server on localhost:8080 // This server handles JWT authentication and redirects to authenticated Composer const loginUrl = 'http://localhost:8080'; log('Navigating to JWT redirect server:', loginUrl); await session.page.goto(loginUrl, { waitUntil: 'domcontentloaded', timeout: 120000 }); log('Navigated to Composer'); // Wait for app initialization await session.page.waitForTimeout(3000); // Inject composition into localStorage await session.page.evaluate((data: any) => { localStorage.setItem('rdp-composer-data', JSON.stringify(data)); console.log('Composition saved to localStorage'); }, composerData); // Navigate to editor via JWT redirect server // The redirect server will handle authentication and navigate to the composer const editorUrl = 'http://localhost:8080'; await session.page.goto(editorUrl, { waitUntil: 'domcontentloaded', timeout: 120000 }); await session.page.waitForTimeout(5000); log('Composition loaded in editor'); const currentURL = session.page.url(); return currentURL; }); return { content: [ { type: 'text', text: `✅ **Composição salva com sucesso!** 📚 **Título:** ${claudeComposition.title} 🎓 **Nível:** ${claudeComposition.gradeLevel} 📖 **Matéria:** ${claudeComposition.subject} ⏱️ **Duração:** ${claudeComposition.duration} minutos 🔗 **URL:** ${result} 🎨 **Widgets criados por Claude:** ${claudeComposition.widgets.map((w, i) => `${i + 1}. ${w.type}`).join('\n')} A composição está aberta no Composer e pronta para uso!` } ] }; } private async getAvailableWidgets(): Promise<CallToolResult> { return { content: [ { type: 'text', text: `📦 **Widgets Disponíveis no Composer:** **1. header** - Cabeçalho da aula - category: Categoria/disciplina - authorName: Nome do professor - authorOffice: Cargo/função **2. text** - Conteúdo textual - title: Título opcional - html: Conteúdo HTML formatado **3. video** - Vídeo educacional - title: Título do vídeo - url: URL do vídeo **4. flashcards** - Cartões de memorização - cards: Array com front/back para cada cartão **5. quiz** - Questionário interativo - questions: Array de perguntas com choices - maxAttempts: Tentativas permitidas **6. image** - Imagem ilustrativa - url: URL da imagem - caption: Legenda opcional **7. accordion** - Conteúdo expansível - items: Array com title/content 💡 **Dica:** Você pode criar composições ricas combinando estes widgets na ordem que melhor atender aos objetivos pedagógicos!` } ] }; } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); log('🚀 Claude Native MCP Server running'); log('💡 This server leverages Claude Desktop natural content generation'); } } // Run the server const server = new ClaudeNativeMCPServer(); server.run().catch((error) => { log('Server error:', 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