Reuben_OS / mcp-server.js
Reubencf's picture
Simplify MCP responses: remove URLs, show only folder locations
9faf78d
raw
history blame
16.3 kB
#!/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 = 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();