Spaces:
Running
Running
| // 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 = process.env.REUBENOS_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. Use your passkey for secure storage or save to public folder.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| fileName: { | |
| type: 'string', | |
| description: 'File name (e.g., main.dart, document.tex, report.pdf, quiz.json)', | |
| }, | |
| content: { | |
| type: 'string', | |
| description: 'File content to save', | |
| }, | |
| passkey: { | |
| type: 'string', | |
| description: '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', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| passkey: { | |
| type: 'string', | |
| description: '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', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| fileName: { | |
| type: 'string', | |
| description: 'Name of the file to delete', | |
| }, | |
| passkey: { | |
| type: 'string', | |
| description: 'Your passkey (required for secure 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', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| passkey: { | |
| type: 'string', | |
| description: 'Your passkey for secure quiz storage', | |
| }, | |
| isPublic: { | |
| type: 'boolean', | |
| description: 'Make quiz public (default: false)', | |
| }, | |
| 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' } }, | |
| correctAnswer: { 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: ['quizData'], | |
| }, | |
| }, | |
| { | |
| name: 'read_file', | |
| description: 'Read the content of a file from secure storage or public folder. 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: 'Your passkey (required for secure files)', | |
| }, | |
| isPublic: { | |
| type: 'boolean', | |
| description: 'Set to true if reading from public folder. Default: false', | |
| }, | |
| }, | |
| required: ['fileName'], | |
| }, | |
| }, | |
| ], | |
| })); | |
| 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); | |
| 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)' }], | |
| }; | |
| } | |
| const response = await fetch(API_ENDPOINT, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| passkey, | |
| action: 'save_file', | |
| fileName, | |
| content, | |
| isPublic, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| const location = isPublic ? 'Public Folder' : `Secure Data (passkey: ${passkey})`; | |
| // Check if PDF was generated for .tex files | |
| let pdfNote = ''; | |
| if (fileName.endsWith('.tex') && data.pdfGenerated) { | |
| pdfNote = `\nπ PDF generated: ${data.pdfFileName}`; | |
| } | |
| return { | |
| content: [ | |
| { | |
| type: 'text', | |
| text: `β File saved: ${fileName}\nπ Saved to: ${location}${pdfNote}`, | |
| }, | |
| ], | |
| }; | |
| } 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, isPublic = false } = args; | |
| if (!quizData || !quizData.questions || quizData.questions.length === 0) { | |
| return { | |
| content: [{ type: 'text', text: 'β Quiz must have at least one question' }], | |
| }; | |
| } | |
| if (!isPublic && !passkey) { | |
| return { | |
| content: [{ type: 'text', text: 'β Passkey is required (or set isPublic=true)' }], | |
| }; | |
| } | |
| const fullQuizData = { | |
| ...quizData, | |
| createdAt: new Date().toISOString(), | |
| passkey: passkey || 'public', | |
| version: '1.0', | |
| }; | |
| const response = await fetch(API_ENDPOINT, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| passkey, | |
| action: 'deploy_quiz', | |
| fileName: 'quiz.json', | |
| content: JSON.stringify(fullQuizData, null, 2), | |
| isPublic, | |
| }), | |
| }); | |
| const data = await response.json(); | |
| if (response.ok && data.success) { | |
| const totalPoints = quizData.questions.reduce((sum, q) => sum + (q.points || 1), 0); | |
| const location = isPublic ? 'Public Folder' : `Secure Data (passkey: ${passkey})`; | |
| return { | |
| content: [ | |
| { | |
| type: 'text', | |
| text: `β Quiz deployed: ${quizData.title}\nπ ${quizData.questions.length} questions, ${totalPoints} points\nπ Saved to: ${location}`, | |
| }, | |
| ], | |
| }; | |
| } else { | |
| return { | |
| content: [{ type: 'text', text: `β Failed: ${data.error || 'Unknown error'}` }], | |
| }; | |
| } | |
| } catch (error) { | |
| return { | |
| content: [{ type: 'text', text: `β Error: ${error.message}` }], | |
| }; | |
| } | |
| } | |
| 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(); |