claude-native-mcp-server.ts•16 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);
});