#!/usr/bin/env node // mcp-server.js - MCP Server for Reuben OS with Passkey System import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import fetch from 'node-fetch'; const BASE_URL = 'https://mcp-1st-birthday-reuben-os.hf.space'; const API_ENDPOINT = `${BASE_URL}/api/mcp-handler`; class ReubenOSMCPServer { constructor() { this.server = new Server( { name: 'reubenos-mcp-server', version: '3.0.0', description: 'MCP Server for Reuben OS with secure passkey-based file storage', icon: 'šŸ–„ļø', }, { capabilities: { tools: {}, }, } ); this.setupToolHandlers(); // Error handling this.server.onerror = (error) => console.error('[MCP Error]', error); process.on('SIGINT', async () => { await this.server.close(); process.exit(0); }); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'save_file', description: 'Save a file to Reuben OS. Passkey is OPTIONAL - provide for secure storage or leave empty for public folder.', inputSchema: { type: 'object', properties: { fileName: { type: 'string', description: 'File name (e.g., main.dart, document.tex, report.pdf)', }, content: { type: 'string', description: 'File content to save', }, passkey: { type: 'string', description: 'OPTIONAL: Your passkey for secure storage (min 4 characters). Leave empty to save to public folder.', }, isPublic: { type: 'boolean', description: 'Set to true to save to public folder (accessible to everyone). Default: false', }, }, required: ['fileName', 'content'], }, }, { name: 'list_files', description: 'List all files in your secure storage or public folder. Passkey is OPTIONAL.', inputSchema: { type: 'object', properties: { passkey: { type: 'string', description: 'OPTIONAL: Your passkey to list secure files. Leave empty to list public files.', }, isPublic: { type: 'boolean', description: 'Set to true to list public files. Default: false', }, }, }, }, { name: 'delete_file', description: 'Delete a specific file from your storage. Passkey is OPTIONAL.', inputSchema: { type: 'object', properties: { fileName: { type: 'string', description: 'Name of the file to delete', }, passkey: { type: 'string', description: 'OPTIONAL: Your passkey (required for secure files). Leave empty for public files.', }, isPublic: { type: 'boolean', description: 'Set to true if deleting from public folder. Default: false', }, }, required: ['fileName'], }, }, { name: 'deploy_quiz', description: 'Deploy an interactive quiz to Reuben OS Quiz App. Passkey is REQUIRED for all quizzes.', inputSchema: { type: 'object', properties: { passkey: { type: 'string', description: 'Your passkey for secure quiz storage (REQUIRED - min 4 characters)', }, quizData: { type: 'object', description: 'Quiz configuration', properties: { title: { type: 'string', description: 'Quiz title' }, description: { type: 'string', description: 'Quiz description' }, questions: { type: 'array', description: 'Array of questions', items: { type: 'object', properties: { id: { type: 'string' }, type: { type: 'string', enum: ['multiple_choice'] }, question: { type: 'string' }, options: { type: 'array', items: { type: 'string' } }, explanation: { type: 'string' }, points: { type: 'number' }, }, required: ['id', 'question', 'type'], }, }, timeLimit: { type: 'number', description: 'Time limit in minutes for the quiz (optional)', }, }, required: ['title', 'questions'], }, }, required: ['passkey', 'quizData'], }, }, { name: 'read_file', description: 'Read the content of a file from secure storage or public folder. Passkey is OPTIONAL. Useful for evaluating quiz answers.', inputSchema: { type: 'object', properties: { fileName: { type: 'string', description: 'Name of the file to read (e.g., quiz_answers.json)', }, passkey: { type: 'string', description: 'OPTIONAL: Your passkey (required for secure files). Leave empty for public files.', }, isPublic: { type: 'boolean', description: 'Set to true if reading from public folder. Default: false', }, }, required: ['fileName'], }, }, { name: 'generate_song_audio', description: 'Generate an AI song with audio using ElevenLabs Music API. NO passkey required - saves directly to Voice Studio app.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the song', }, style: { type: 'string', description: 'Musical style/genre (e.g., "pop", "rock", "jazz", "ballad")', }, lyrics: { type: 'string', description: 'Song lyrics (will be used to generate music)', }, }, required: ['title', 'style', 'lyrics'], }, }, { name: 'generate_story_audio', description: 'Generate audio narration for a story using ElevenLabs Text-to-Speech API. NO passkey required - saves directly to Voice Studio app.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the story', }, content: { type: 'string', description: 'Story content/text to be narrated (max 2000 characters for best performance)', }, }, required: ['title', 'content'], }, }, { name: 'analyze_quiz', description: 'Analyze quiz answers from quiz_answers.json against the quiz.json questions and provide feedback on correctness. Passkey is REQUIRED.', inputSchema: { type: 'object', properties: { passkey: { type: 'string', description: 'Your passkey for accessing the quiz files (REQUIRED)', }, }, required: ['passkey'], }, }, ], })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case 'save_file': return await this.saveFile(args); case 'list_files': return await this.listFiles(args); case 'delete_file': return await this.deleteFile(args); case 'deploy_quiz': return await this.deployQuiz(args); case 'read_file': return await this.readFile(args); case 'generate_song_audio': return await this.generateSongAudio(args); case 'generate_story_audio': return await this.generateStoryAudio(args); case 'analyze_quiz': return await this.analyzeQuiz(args); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { return { content: [ { type: 'text', text: `Error: ${error.message}`, }, ], }; } }); } async readFile(args) { try { const { fileName, passkey, isPublic = false } = args; if (!fileName) { return { content: [{ type: 'text', text: 'āŒ fileName is required' }], }; } if (!isPublic && !passkey) { return { content: [{ type: 'text', text: 'āŒ Passkey is required (or set isPublic=true)' }], }; } const ext = fileName.split('.').pop().toLowerCase(); const isDocument = ['pdf', 'docx', 'xlsx', 'xls', 'pptx'].includes(ext); if (isDocument) { // Use document processing endpoint const processUrl = `${BASE_URL}/api/documents/process`; const response = await fetch(processUrl, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fileName, key: passkey, isPublic, operation: 'read' }) }); const data = await response.json(); if (response.ok && data.success) { let textContent = ''; if (data.content.text) { textContent = data.content.text; } else if (data.content.sheets) { textContent = JSON.stringify(data.content.sheets, null, 2); } else { textContent = JSON.stringify(data.content, null, 2); } return { content: [ { type: 'text', text: `šŸ“„ Content of ${fileName} (${data.content.type}):\n\n${textContent}`, }, ], }; } else { return { content: [{ type: 'text', text: `āŒ Failed to process document: ${data.error || 'Unknown error'}` }], }; } } const url = new URL(API_ENDPOINT); if (passkey) url.searchParams.set('passkey', passkey); if (isPublic) url.searchParams.set('isPublic', 'true'); const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); if (response.ok && data.success) { const file = data.files.find(f => f.name === fileName); if (!file) { return { content: [{ type: 'text', text: `āŒ File '${fileName}' not found in ${data.location}` }], }; } return { content: [ { type: 'text', text: `šŸ“„ Content of ${fileName}:\n\n${file.content || '(Empty file)'}`, }, ], }; } else { return { content: [{ type: 'text', text: `āŒ Failed: ${data.error || 'Unknown error'}` }], }; } } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async saveFile(args) { try { const { fileName, content, passkey, isPublic = false } = args; if (!fileName || content === undefined) { return { content: [{ type: 'text', text: 'āŒ fileName and content are required' }], }; } if (!isPublic && !passkey) { return { content: [{ type: 'text', text: 'āŒ Passkey is required for secure storage (or set isPublic=true)' }], }; } // Ensure content is properly stringified if it's an object let fileContent = content; if (typeof content === 'object' && content !== null) { fileContent = JSON.stringify(content, null, 2); } // Handle special characters in content for code files const ext = fileName.split('.').pop().toLowerCase(); const isCodeFile = ['dart', 'js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'c', 'h', 'hpp', 'cs', 'swift', 'kt', 'go', 'rs', 'rb', 'php'].includes(ext); // Ensure content is a string fileContent = String(fileContent); const response = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passkey, action: 'save_file', fileName, content: fileContent, isPublic, }), }); const data = await response.json(); if (response.ok && data.success) { const location = isPublic ? 'Public Folder' : `Secure Data (passkey: ${passkey})`; const fileType = isCodeFile ? `(${ext.toUpperCase()} code file)` : ''; return { content: [ { type: 'text', text: `āœ… File saved: ${fileName} ${fileType}\nšŸ“ Saved to: ${location}\nšŸ“Š Size: ${(new TextEncoder().encode(fileContent).length / 1024).toFixed(2)} KB`, }, ], }; } else { return { content: [{ type: 'text', text: `āŒ Failed to save: ${data.error || 'Unknown error'}` }], }; } } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async listFiles(args) { try { const { passkey, isPublic = false } = args; if (!isPublic && !passkey) { return { content: [{ type: 'text', text: 'āŒ Passkey is required (or set isPublic=true)' }], }; } const url = new URL(API_ENDPOINT); if (passkey) url.searchParams.set('passkey', passkey); if (isPublic) url.searchParams.set('isPublic', 'true'); const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const data = await response.json(); if (response.ok && data.success) { if (data.files.length === 0) { return { content: [{ type: 'text', text: `šŸ“ No files found in ${data.location}` }], }; } const fileList = data.files .map(f => `šŸ“„ ${f.name} (${(f.size / 1024).toFixed(2)} KB)${f.isQuiz ? ' [QUIZ]' : ''}`) .join('\n'); return { content: [ { type: 'text', text: `šŸ“ Files in ${data.location}:\n\n${fileList}\n\nTotal: ${data.count} file(s)`, }, ], }; } else { return { content: [{ type: 'text', text: `āŒ Failed: ${data.error || 'Unknown error'}` }], }; } } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async deleteFile(args) { try { const { fileName, passkey, isPublic = false } = args; if (!fileName) { return { content: [{ type: 'text', text: 'āŒ fileName is required' }], }; } if (!isPublic && !passkey) { return { content: [{ type: 'text', text: 'āŒ Passkey is required (or set isPublic=true)' }], }; } const response = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passkey, action: 'delete_file', fileName, isPublic, content: '', // Required by API but not used }), }); const data = await response.json(); if (response.ok && data.success) { return { content: [{ type: 'text', text: `āœ… File '${fileName}' deleted successfully` }], }; } else { return { content: [{ type: 'text', text: `āŒ Failed: ${data.error || 'File not found'}` }], }; } } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async deployQuiz(args) { try { const { quizData, passkey } = args; // Passkey is REQUIRED for quizzes if (!passkey || passkey.length < 4) { return { content: [{ type: 'text', text: 'āŒ Passkey is REQUIRED for quizzes (minimum 4 characters)' }], }; } if (!quizData || !quizData.questions || quizData.questions.length === 0) { return { content: [{ type: 'text', text: 'āŒ Quiz must have at least one question' }], }; } // Strip correct answers from questions before saving quiz.json // This prevents users from seeing the answers in the quiz file const questionsWithoutAnswers = quizData.questions.map(q => { const { correctAnswer, correct, answer, ...questionWithoutAnswer } = q; return questionWithoutAnswer; }); // Save quiz.json WITHOUT correct answers (for users to take) const quizForUsers = { title: quizData.title, description: quizData.description, timeLimit: quizData.timeLimit, questions: questionsWithoutAnswers, createdAt: new Date().toISOString(), version: '1.0', }; // Save quiz_key.json WITH correct answers (for grading) - hidden file const quizAnswerKey = { title: quizData.title, questions: quizData.questions.map(q => ({ id: q.id, correctAnswer: q.correctAnswer || q.correct || q.answer, explanation: q.explanation, points: q.points || 1, })), createdAt: new Date().toISOString(), }; // Save quiz.json (without answers) const quizResponse = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passkey, action: 'deploy_quiz', fileName: 'quiz.json', content: JSON.stringify(quizForUsers, null, 2), isPublic: false, }), }); const quizResult = await quizResponse.json(); if (!quizResponse.ok || !quizResult.success) { return { content: [{ type: 'text', text: `āŒ Failed to save quiz: ${quizResult.error || 'Unknown error'}` }], }; } // Save quiz_key.json (with correct answers for grading) const keyResponse = await fetch(API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passkey, action: 'save_file', fileName: 'quiz_key.json', content: JSON.stringify(quizAnswerKey, null, 2), isPublic: false, }), }); const keyResult = await keyResponse.json(); if (!keyResponse.ok || !keyResult.success) { console.error('Failed to save quiz key:', keyResult.error); // Don't fail the whole operation, just log it } const totalPoints = quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0); return { content: [ { type: 'text', text: `āœ… Quiz deployed: ${quizData.title}\nšŸ“Š ${quizData.questions.length} questions, ${totalPoints} points\nšŸ”’ Secured with passkey: ${passkey}\nšŸ“ Answer key saved separately for grading`, }, ], }; } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async generateSongAudio(args) { try { const { title, style, lyrics } = args; // Use a default passkey internally - users don't need to provide it const passkey = 'voice_default'; if (!title || !style || !lyrics) { return { content: [{ type: 'text', text: 'āŒ title, style, and lyrics are required' }], }; } const response = await fetch(`${BASE_URL}/api/voice/generate-song`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, style, lyrics, passkey }), }); const data = await response.json(); if (response.ok && data.success) { // Save the content to server storage await fetch(`${BASE_URL}/api/voice/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passkey, content: data.content, }), }); return { content: [ { type: 'text', text: `šŸŽµ Song "${title}" generated successfully!\\nšŸŽø Style: ${style}\\nšŸ“± Open the Voice Studio app to listen to your song!`, }, ], }; } else { return { content: [{ type: 'text', text: `āŒ Failed to generate song: ${data.error || 'Unknown error'}` }], }; } } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async generateStoryAudio(args) { try { const { title, content } = args; // Use a default passkey internally - users don't need to provide it const passkey = 'voice_default'; if (!title || !content) { return { content: [{ type: 'text', text: 'āŒ title and content are required' }], }; } const response = await fetch(`${BASE_URL}/api/voice/generate-story`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title, content, passkey }), }); const data = await response.json(); if (response.ok && data.success) { // Save the content to server storage await fetch(`${BASE_URL}/api/voice/save`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ passkey, content: data.content, }), }); return { content: [ { type: 'text', text: `šŸ“– Story "${title}" audio generated successfully!\\nšŸ“± Open the Voice Studio app to listen to your story!`, }, ], }; } else { return { content: [{ type: 'text', text: `āŒ Failed to generate story audio: ${data.error || 'Unknown error'}` }], }; } } catch (error) { return { content: [{ type: 'text', text: `āŒ Error: ${error.message}` }], }; } } async analyzeQuiz(args) { try { const { passkey } = args; // Passkey is REQUIRED for quiz analysis if (!passkey || passkey.length < 4) { return { content: [{ type: 'text', text: 'āŒ Passkey is REQUIRED to access quiz files (minimum 4 characters)' }], }; } // Read all files from secure storage const quizUrl = new URL(API_ENDPOINT); quizUrl.searchParams.set('passkey', passkey); console.log('Fetching quiz files from:', quizUrl.toString()); const quizResponse = await fetch(quizUrl, { method: 'GET', headers: { 'Content-Type': 'application/json' }, }); const quizData = await quizResponse.json(); console.log('API Response:', JSON.stringify(quizData, null, 2).substring(0, 500)); if (!quizResponse.ok || !quizData.success) { return { content: [{ type: 'text', text: `āŒ Failed to read files: ${quizData.error || 'Unknown error'}` }], }; } if (!quizData.files || quizData.files.length === 0) { return { content: [{ type: 'text', text: 'āŒ No files found for this passkey. Make sure the quiz has been deployed and answered.' }], }; } // Find quiz.json (questions without answers) const quizFile = quizData.files.find(f => f.name === 'quiz.json'); if (!quizFile) { return { content: [{ type: 'text', text: `āŒ quiz.json not found. Available files: ${quizData.files.map(f => f.name).join(', ')}` }], }; } if (!quizFile.content) { return { content: [{ type: 'text', text: 'āŒ quiz.json exists but content is empty or could not be read' }], }; } // Find quiz_key.json (correct answers for grading) // For backward compatibility, if quiz_key.json doesn't exist, try to get answers from quiz.json const keyFile = quizData.files.find(f => f.name === 'quiz_key.json'); const hasAnswerKey = keyFile && keyFile.content; // For backward compatibility: if no quiz_key.json exists, we'll need to get answers from quiz.json (old format) // This happens for quizzes created before the answer-key separation was implemented // Find quiz_answers.json (user's answers) const answersFile = quizData.files.find(f => f.name === 'quiz_answers.json'); if (!answersFile) { return { content: [{ type: 'text', text: `āŒ quiz_answers.json not found. The user needs to complete the quiz first. Available files: ${quizData.files.map(f => f.name).join(', ')}` }], }; } if (!answersFile.content) { return { content: [{ type: 'text', text: 'āŒ quiz_answers.json exists but content is empty or could not be read' }], }; } console.log('Quiz file content length:', quizFile.content?.length); console.log('Key file found:', !!keyFile, 'content length:', keyFile?.content?.length); console.log('Answers file content length:', answersFile.content?.length); // Parse the quiz, answer key, and user answers let quiz, answerKey, userAnswersData; try { quiz = typeof quizFile.content === 'string' ? JSON.parse(quizFile.content) : quizFile.content; } catch (e) { return { content: [{ type: 'text', text: `āŒ Failed to parse quiz.json: ${e.message}` }], }; } // Parse answer key (from quiz_key.json if exists, or fall back to quiz.json for backward compatibility) if (hasAnswerKey) { try { answerKey = typeof keyFile.content === 'string' ? JSON.parse(keyFile.content) : keyFile.content; } catch (e) { return { content: [{ type: 'text', text: `āŒ Failed to parse quiz_key.json: ${e.message}` }], }; } } else { // Backward compatibility: create answer key from quiz.json (old format had answers in quiz.json) console.log('No quiz_key.json found - using backward compatibility mode with quiz.json'); answerKey = { title: quiz.title, questions: (quiz.questions || []).map(q => ({ id: q.id, correctAnswer: q.correctAnswer || q.correct || q.answer, explanation: q.explanation, points: q.points || 1, })), }; } try { userAnswersData = typeof answersFile.content === 'string' ? JSON.parse(answersFile.content) : answersFile.content; } catch (e) { return { content: [{ type: 'text', text: `āŒ Failed to parse quiz_answers.json: ${e.message}` }], }; } console.log('Parsed quiz:', quiz.title, 'Questions:', quiz.questions?.length); console.log('Parsed answer key:', answerKey.questions?.length, 'answers'); console.log('Parsed user answers:', JSON.stringify(userAnswersData, null, 2).substring(0, 300)); // Convert user answers array to object for easier lookup const userAnswers = {}; if (userAnswersData.answers && Array.isArray(userAnswersData.answers)) { // QuizApp format: { answers: [{ questionId, answer }], metadata: {...} } userAnswersData.answers.forEach(item => { userAnswers[item.questionId] = item.answer; }); } else if (typeof userAnswersData === 'object') { // Direct object format Object.assign(userAnswers, userAnswersData); } // Convert answer key to object for easier lookup const correctAnswers = {}; const explanations = {}; const pointsMap = {}; if (answerKey.questions && Array.isArray(answerKey.questions)) { answerKey.questions.forEach(q => { correctAnswers[q.id] = q.correctAnswer; explanations[q.id] = q.explanation; pointsMap[q.id] = q.points || 1; }); } console.log('Processed user answers:', userAnswers); console.log('Processed correct answers:', correctAnswers); // Analyze the answers let correctCount = 0; let totalPoints = 0; let maxPoints = 0; const feedback = []; const questionsArray = quiz.questions || []; if (questionsArray.length === 0) { return { content: [{ type: 'text', text: 'āŒ Quiz has no questions to analyze' }], }; } questionsArray.forEach((question, index) => { const questionId = question.id || `question_${index}`; const userAnswer = userAnswers[questionId]; const correctAnswer = correctAnswers[questionId]; const explanation = explanations[questionId]; const points = pointsMap[questionId] || 1; maxPoints += points; if (!userAnswer) { feedback.push(`āš ļø Q${index + 1}: "${question.question.substring(0, 50)}..." - Not answered`); return; } if (!correctAnswer) { // No correct answer in key - just show the user's answer for manual review feedback.push(`šŸ“ Q${index + 1}: "${question.question.substring(0, 50)}..." \n User answered: "${userAnswer}" \n (No correct answer in key - manual review needed)`); return; } // Check if answer is correct let isCorrect = false; if (Array.isArray(correctAnswer)) { isCorrect = correctAnswer.some(ca => String(ca).toLowerCase().trim() === String(userAnswer).toLowerCase().trim() ); } else { isCorrect = String(correctAnswer).toLowerCase().trim() === String(userAnswer).toLowerCase().trim(); } if (isCorrect) { correctCount++; totalPoints += points; feedback.push(`āœ… Q${index + 1}: Correct! (+${points} pts)`); } else { feedback.push(`āŒ Q${index + 1}: "${question.question.substring(0, 40)}..."\n Your answer: "${userAnswer}"\n Correct: "${correctAnswer}"${explanation ? `\n šŸ’” ${explanation}` : ''}`); } }); const percentage = maxPoints > 0 ? Math.round((totalPoints / maxPoints) * 100) : 0; const grade = percentage >= 90 ? 'A' : percentage >= 80 ? 'B' : percentage >= 70 ? 'C' : percentage >= 60 ? 'D' : 'F'; // Include metadata if available let metadataInfo = ''; if (userAnswersData.metadata) { const meta = userAnswersData.metadata; metadataInfo = `\nā±ļø Time taken: ${Math.floor((meta.timeTakenSeconds || 0) / 60)}m ${(meta.timeTakenSeconds || 0) % 60}s`; if (meta.timeExceeded) { metadataInfo += ' (Time limit exceeded!)'; } metadataInfo += `\nšŸ“… Completed: ${meta.completedAt || 'Unknown'}`; } return { content: [ { type: 'text', text: `šŸ“Š Quiz Analysis Results for "${quiz.title || 'Untitled Quiz'}" šŸ“ˆ Score: ${totalPoints}/${maxPoints} points (${percentage}%) āœ… Correct: ${correctCount}/${questionsArray.length} questions šŸŽÆ Grade: ${grade}${metadataInfo} šŸ“ Detailed Feedback: ${feedback.join('\n\n')} ${percentage >= 70 ? 'šŸŽ‰ Great job!' : 'šŸ“š Keep studying and try again!'}`, }, ], }; } catch (error) { console.error('analyzeQuiz error:', error); return { content: [{ type: 'text', text: `āŒ Error analyzing quiz: ${error.message}\n\nStack: ${error.stack}` }], }; } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('ReubenOS MCP Server running with passkey authentication...'); } } const server = new ReubenOSMCPServer(); server.run();