Skip to main content
Glama

Roblox MCP Unified Server

by Rxuser2
server.jsโ€ข16.3 kB
const express = require('express'); const cors = require('cors'); const helmet = require('helmet'); const rateLimit = require('express-rate-limit'); const path = require('path'); const fs = require('fs-extra'); const config = require('./config'); const DatabaseService = require('./database'); const RobloxToolsService = require('./robloxToolsService'); class MCPServer { constructor() { this.app = express(); this.db = new DatabaseService(config.dbPath); this.robloxTools = new RobloxToolsService(); this.logger = config.initializeLogger(); this.setupMiddleware(); this.setupRoutes(); } setupMiddleware() { // Railway proxy configuration untuk Railway deployment this.app.set('trust proxy', 1); // Security headers if (config.enableSecurityHeaders) { this.app.use(helmet()); } // CORS this.app.use(cors({ origin: config.corsOrigin === '*' ? true : config.corsOrigin, credentials: true })); // Rate limiting with Railway proxy support if (config.enableRateLimiting) { const limiter = rateLimit({ windowMs: config.rateLimitWindowMs, max: config.rateLimitMaxRequests, message: { error: 'Too many requests from this IP, please try again later.', code: 'RATE_LIMIT_EXCEEDED' }, standardHeaders: true, legacyHeaders: false, // Railway proxy-friendly configuration keyGenerator: (req) => { // Use real client IP instead of proxy IP return req.ip || req.connection.remoteAddress || 'unknown'; } }); this.app.use('/api/', limiter); } // Body parsing this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); // Request logging this.app.use((req, res, next) => { this.logger.info(`${req.method} ${req.path} - ${req.ip}`); next(); }); } setupRoutes() { // Enhanced health check dengan performance metrics this.app.get('/health', (req, res) => { const dbStatus = this.db ? 'connected' : 'disconnected'; const memoryUsage = process.memoryUsage(); res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), memory: { rss: Math.round(memoryUsage.rss / 1024 / 1024) + ' MB', heapUsed: Math.round(memoryUsage.heapUsed / 1024 / 1024) + ' MB', heapTotal: Math.round(memoryUsage.heapTotal / 1024 / 1024) + ' MB' }, database: { status: dbStatus, path: config.dbPath }, version: require('../package.json').version, railway: { proxy: req.app.get('trust proxy') ? 'enabled' : 'disabled', rateLimit: config.enableRateLimiting ? 'enabled' : 'disabled' } }); }); // API routes (Web UI friendly - no auth required for read operations) this.app.post('/api/create_script', this.validateRequest, this.handleCreateScript.bind(this)); this.app.get('/api/list_scripts', this.handleListScripts.bind(this)); // No auth for list this.app.put('/api/update_script', this.validateRequest, this.handleUpdateScript.bind(this)); this.app.delete('/api/delete_script', this.validateRequest, this.handleDeleteScript.bind(this)); this.app.get('/api/get_project_status', this.handleGetProjectStatus.bind(this)); // No auth for status this.app.post('/api/validate_script', this.handleValidateScript.bind(this)); // No auth for validation this.app.post('/api/backup_project', this.validateRequest, this.handleBackupProject.bind(this)); this.app.post('/api/restore_project', this.validateRequest, this.handleRestoreProject.bind(this)); // MCP Protocol support this.app.post('/mcp/function', this.validateRequest, this.handleMCPFunction.bind(this)); // HMAC Secret endpoint untuk MCP client configuration this.app.get('/api/get_hmac_secret', (req, res) => { res.json({ hmac_secret: config.hmacSecret, timestamp: new Date().toISOString(), note: 'Keep this secret secure and use it to configure your MCP client' }); }); // Performance monitoring endpoint this.app.get('/api/performance', (req, res) => { const fs = require('fs'); let dbSize = 'unknown'; try { if (fs.existsSync(config.dbPath)) { const stats = fs.statSync(config.dbPath); dbSize = Math.round(stats.size / 1024) + ' KB'; } } catch (error) { this.logger.warn('Could not get database size:', error.message); } const performance = { memory: process.memoryUsage(), uptime: process.uptime(), timestamp: new Date().toISOString(), database: { path: config.dbPath, size: dbSize }, server: { nodeEnv: config.nodeEnv, port: config.port, proxy: req.app.get('trust proxy') ? 'enabled' : 'disabled' } }; res.json(performance); }); // Serve static files for web interface this.app.use(express.static(path.join(__dirname, '../public'))); // Catch all route - serve index.html for SPA this.app.get('*', (req, res) => { // Skip API routes and health check if (req.path.startsWith('/api/') || req.path === '/health' || req.path.startsWith('/mcp/')) { return res.status(404).json({ error: 'API endpoint not found' }); } res.sendFile(path.join(__dirname, '../public/index.html')); }); // Error handling this.app.use(this.errorHandler.bind(this)); } validateRequest(req, res, next) { try { // Add request timeout untuk database operations req.setTimeout(config.dbQueryTimeout || 30000, () => { res.status(408).json({ success: false, error: 'Request timeout - operation took too long', code: 'REQUEST_TIMEOUT' }); }); const signature = req.headers['x-signature'] || req.headers['authorization']?.replace('Bearer ', ''); if (!signature) { return res.status(401).json({ success: false, error: 'Missing signature', code: 'MISSING_SIGNATURE' }); } const data = JSON.stringify(req.body); const timestamp = req.headers['x-timestamp'] || ''; // Check timestamp (prevent replay attacks) const now = Date.now(); const requestTime = parseInt(timestamp); if (timestamp && Math.abs(now - requestTime) > 300000) { // 5 minutes return res.status(401).json({ success: false, error: 'Request timestamp too old', code: 'TIMESTAMP_EXPIRED' }); } // Validate HMAC const expectedSignature = config.generateHmac(data + timestamp); if (!config.validateHmac(data + timestamp, signature)) { return res.status(401).json({ success: false, error: 'Invalid signature', code: 'INVALID_SIGNATURE' }); } next(); } catch (error) { this.logger.error('Request validation error:', error); res.status(500).json({ success: false, error: 'Request validation failed', code: 'VALIDATION_ERROR' }); } } // Handler methods for all 8 tools async handleCreateScript(req, res) { try { const { name, content, script_type = 'lua', project_id = 'default' } = req.body; if (!name || !content) { return res.status(400).json({ success: false, error: 'Name and content are required', code: 'MISSING_PARAMETERS' }); } const result = await this.robloxTools.createScript(name, content, script_type, project_id); res.json(result); } catch (error) { this.logger.error('Create script error:', error); res.status(500).json({ success: false, error: error.message, code: 'CREATE_SCRIPT_ERROR' }); } } async handleListScripts(req, res) { try { const { project_id = 'default' } = req.query; const result = await this.robloxTools.listScripts(project_id); res.json(result); } catch (error) { this.logger.error('List scripts error:', error); res.status(500).json({ success: false, error: error.message, code: 'LIST_SCRIPTS_ERROR' }); } } async handleUpdateScript(req, res) { try { const { name, content, project_id = 'default' } = req.body; if (!name || !content) { return res.status(400).json({ success: false, error: 'Name and content are required', code: 'MISSING_PARAMETERS' }); } const result = await this.robloxTools.updateScript(name, content, project_id); res.json(result); } catch (error) { this.logger.error('Update script error:', error); res.status(500).json({ success: false, error: error.message, code: 'UPDATE_SCRIPT_ERROR' }); } } async handleDeleteScript(req, res) { try { const { name, project_id = 'default' } = req.body; if (!name) { return res.status(400).json({ success: false, error: 'Name is required', code: 'MISSING_PARAMETERS' }); } const result = await this.robloxTools.deleteScript(name, project_id); res.json(result); } catch (error) { this.logger.error('Delete script error:', error); res.status(500).json({ success: false, error: error.message, code: 'DELETE_SCRIPT_ERROR' }); } } async handleGetProjectStatus(req, res) { try { const { project_id = 'default' } = req.query; const result = await this.robloxTools.getProjectStatus(project_id); res.json(result); } catch (error) { this.logger.error('Get project status error:', error); res.status(500).json({ success: false, error: error.message, code: 'GET_PROJECT_STATUS_ERROR' }); } } async handleValidateScript(req, res) { try { const { content, script_type = 'lua' } = req.body; if (!content) { return res.status(400).json({ valid: false, error: 'Content is required', code: 'MISSING_PARAMETERS' }); } const result = await this.robloxTools.validateScript(content, script_type); res.json(result); } catch (error) { this.logger.error('Validate script error:', error); res.status(500).json({ valid: false, error: error.message, code: 'VALIDATE_SCRIPT_ERROR' }); } } async handleBackupProject(req, res) { try { const { project_id = 'default' } = req.body; const result = await this.robloxTools.backupProject(project_id); res.json(result); } catch (error) { this.logger.error('Backup project error:', error); res.status(500).json({ success: false, error: error.message, code: 'BACKUP_PROJECT_ERROR' }); } } async handleRestoreProject(req, res) { try { const { project_id, backup_path = null } = req.body; if (!project_id) { return res.status(400).json({ success: false, error: 'project_id is required', code: 'MISSING_PARAMETERS' }); } const result = await this.robloxTools.restoreProject(project_id, backup_path); res.json(result); } catch (error) { this.logger.error('Restore project error:', error); res.status(500).json({ success: false, error: error.message, code: 'RESTORE_PROJECT_ERROR' }); } } // MCP Protocol handler async handleMCPFunction(req, res) { try { const { function: funcName, parameters = {} } = req.body; const functionMap = { 'create_script': 'createScript', 'list_scripts': 'listScripts', 'update_script': 'updateScript', 'delete_script': 'deleteScript', 'get_project_status': 'getProjectStatus', 'validate_script': 'validateScript', 'backup_project': 'backupProject', 'restore_project': 'restoreProject' }; const method = functionMap[funcName]; if (!method) { return res.status(400).json({ success: false, error: `Unknown function: ${funcName}`, code: 'UNKNOWN_FUNCTION' }); } // Map parameters based on function let args = []; switch (funcName) { case 'create_script': args = [parameters.name, parameters.content, parameters.script_type, parameters.project_id]; break; case 'update_script': args = [parameters.name, parameters.content, parameters.project_id]; break; case 'delete_script': args = [parameters.name, parameters.project_id]; break; case 'get_project_status': args = [parameters.project_id]; break; case 'validate_script': args = [parameters.content, parameters.script_type]; break; case 'backup_project': args = [parameters.project_id]; break; case 'restore_project': args = [parameters.project_id, parameters.backup_path]; break; case 'list_scripts': args = [parameters.project_id]; break; } const result = await this.robloxTools[method](...args); res.json(result); } catch (error) { this.logger.error('MCP function error:', error); res.status(500).json({ success: false, error: error.message, code: 'MCP_FUNCTION_ERROR' }); } } errorHandler(err, req, res, next) { this.logger.error('Unhandled error:', err); res.status(500).json({ success: false, error: 'Internal server error', code: 'INTERNAL_ERROR', ...(config.isDevelopment && { stack: err.stack }) }); } async start() { try { // Initialize database await this.db.initialize(); await this.robloxTools.initialize(); const server = this.app.listen(config.port, () => { console.log('๐Ÿš€ Roblox MCP Node.js Server Started'); console.log('================================='); config.printSummary(); console.log('================================='); console.log('๐Ÿ› ๏ธ Available Endpoints:'); console.log(' POST /api/create_script'); console.log(' GET /api/list_scripts'); console.log(' PUT /api/update_script'); console.log(' DELETE /api/delete_script'); console.log(' GET /api/get_project_status'); console.log(' POST /api/validate_script'); console.log(' POST /api/backup_project'); console.log(' POST /api/restore_project'); console.log(' POST /mcp/function'); console.log(' GET /health'); console.log('================================='); console.log('๐ŸŽฏ Ready for MCP connections!'); if (config.railwayStaticUrl) { console.log(`๐ŸŒ Public URL: ${config.railwayStaticUrl}`); } }); // Graceful shutdown process.on('SIGTERM', () => { this.logger.info('SIGTERM received, shutting down gracefully'); server.close(() => { this.logger.info('Process terminated'); process.exit(0); }); }); } catch (error) { this.logger.error('Failed to start server:', error); process.exit(1); } } } // CLI initialization for database if (require.main === module && process.argv.includes('--init')) { const db = new DatabaseService(); db.initialize().then(() => { console.log('โœ… Database initialized successfully'); process.exit(0); }).catch(err => { console.error('โŒ Database initialization failed:', err); process.exit(1); }); } // Start server when run directly if (require.main === module) { const server = new MCPServer(); server.start(); } module.exports = MCPServer;

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Rxuser2/Roblox-MCP-Unified'

If you have feedback or need assistance with the MCP directory API, please join our Discord server