Reuben_OS / mcp-server.js
Reubencf's picture
Fix quiz system: separate answers, backward compat, and bug fixes
7f6d612
#!/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();