Skip to main content
Glama
tenant-backend-service.jsโ€ข24 kB
#!/usr/bin/env node // Tenant Backend Service - Auto-starting persistent bash service for tenants import { createServer } from 'http'; import { spawn } from 'child_process'; import { mkdir, readFile } from 'fs/promises'; import { join } from 'path'; console.error('๐Ÿš€ Tenant Backend Service starting...'); // GitHub Gist Upload Service for Network-Resilient File Sharing class GitHubGistUploader { constructor(githubPAT) { this.githubPAT = githubPAT; this.apiBase = 'https://api.github.com/gists'; } async uploadFile(filename, content, description = 'File uploaded via MCP Tenant Backend') { try { const response = await fetch(this.apiBase, { method: 'POST', headers: { 'Authorization': `token ${this.githubPAT}`, 'Content-Type': 'application/json', 'User-Agent': 'MCP-Tenant-Backend/1.0' }, body: JSON.stringify({ description, public: false, files: { [filename]: { content: content } } }) }); if (!response.ok) { const errorData = await response.text(); throw new Error(`GitHub API Error (${response.status}): ${errorData}`); } const gist = await response.json(); return { success: true, service: 'GitHub Gist', url: gist.html_url, raw_url: gist.files[filename].raw_url, id: gist.id, message: `โœ… File uploaded to GitHub Gist: ${gist.html_url}` }; } catch (error) { console.error('โŒ GitHub Gist upload failed:', error.message); return { success: false, service: 'GitHub Gist', error: error.message }; } } async testConnectivity() { try { const response = await fetch('https://api.github.com/users/octocat', { method: 'GET', headers: { 'Authorization': `token ${this.githubPAT}`, 'User-Agent': 'MCP-Tenant-Backend/1.0' } }); return response.ok; } catch { return false; } } } // Network Detection and Resilient Upload Manager class ResilientUploadManager { constructor(githubPAT) { this.githubPAT = githubPAT; this.gistUploader = new GitHubGistUploader(githubPAT); } async testServiceConnectivity(service) { switch (service) { case '0x0.st': try { const response = await fetch('https://0x0.st', { method: 'HEAD', timeout: 5000 }); return response.ok; } catch { return false; } case 'github_gist': return await this.gistUploader.testConnectivity(); default: return false; } } async uploadFromCommand(command, workspace, sessionId) { // Parse curl command to extract file information const fileMatch = command.match(/curl\s+-F\s+['"]file=([^'"]+)['"]\s+https?:\/\/([^\s]+)/); if (!fileMatch) { return { success: false, error: 'Could not parse upload command' }; } const [, filePath, targetService] = fileMatch; const filename = filePath.replace(/^@\.\//, '').replace(/^@/, ''); try { // Read the file content const fullPath = join(workspace, filePath.replace(/^@/, '')); const content = await readFile(fullPath, 'utf8'); // Try 0x0.st first, then fallback to GitHub Gist const zeroXZeroAvailable = await this.testServiceConnectivity('0x0.st'); if (targetService.includes('0x0.st') && zeroXZeroAvailable) { return await this.tryZeroXZero(command, workspace, filename, content); } else { // Fallback to GitHub Gist console.error(`๐Ÿ”„ 0x0.st not available, using GitHub Gist fallback for ${sessionId.substring(0, 8)}...`); return await this.gistUploader.uploadFile(filename, content); } } catch (error) { // If file reading fails, try original command console.error(`โš ๏ธ File read failed, trying original command for ${sessionId.substring(0, 8)}...:`, error.message); return await this.tryOriginalCommand(command, workspace); } } async tryZeroXZero(command, workspace, filename, content) { try { // Execute original 0x0.st command const result = await this.executeCommand(command, workspace); if (result.exit_code === 0) { return { success: true, service: '0x0.st', url: result.stdout.trim(), message: `โœ… File uploaded to 0x0.st: ${result.stdout.trim()}` }; } throw new Error(result.stderr || 'Upload failed'); } catch (error) { console.error('โŒ 0x0.st upload failed, trying GitHub Gist fallback:', error.message); // Fallback to GitHub Gist return await this.gistUploader.uploadFile(filename, content); } } async tryOriginalCommand(command, workspace) { const result = await this.executeCommand(command, workspace); return { success: result.exit_code === 0, service: 'original', stdout: result.stdout, stderr: result.stderr, exit_code: result.exit_code }; } async executeCommand(command, cwd) { return new Promise((resolve) => { const { spawn } = require('child_process'); const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'; const process = spawn(command, [], { cwd, shell: true, env: { ...process.env } }); let stdout = ''; let stderr = ''; process.stdout.on('data', (data) => { stdout += data.toString(); }); process.stderr.on('data', (data) => { stderr += data.toString(); }); process.on('close', (code) => { resolve({ stdout, stderr, exit_code: code || 0 }); }); process.on('error', (error) => { resolve({ stdout, stderr: error.message, exit_code: -1 }); }); // Timeout after 30 seconds setTimeout(() => { if (!process.killed) { process.kill('SIGTERM'); resolve({ stdout, stderr: 'Command timed out', exit_code: -1 }); } }, 30000); }); } } class TenantBackendService { constructor() { this.tenantProcesses = new Map(); // sessionId -> { process, workspace, port, lastActivity, timeoutTimer } this.portCounter = 4000; // Start tenant services from port 4000 this.server = null; // Configuration for session timeout this.defaultTimeout = parseInt(process.env.TENANT_SESSION_TIMEOUT) || (30 * 60 * 1000); // 30 minutes default this.enableTimeout = process.env.TENANT_TIMEOUT_ENABLED !== 'false'; // Enabled by default this.activityTracking = process.env.TENANT_ACTIVITY_TRACKING !== 'false'; // Track activity to reset timeout // Initialize resilient upload manager const githubPAT = process.env.GITHUB_PAT; this.uploadManager = githubPAT ? new ResilientUploadManager(githubPAT) : null; if (this.uploadManager) { console.error('๐ŸŒ Resilient Upload Manager initialized with GitHub Gist fallback'); } else { console.error('โš ๏ธ GitHub PAT not found - upload fallback disabled'); } } async initialize() { console.error('๐Ÿ”ง Initializing Tenant Backend Service...'); // Start the main HTTP service this.server = createServer(async (req, res) => { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.setHeader('Access-Control-Max-Age', '86400'); // Handle OPTIONS requests for CORS preflight if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } try { const url = new URL(req.url || '/', `http://${req.headers.host}`); if (req.method === 'POST' && url.pathname === '/tenant-bash') { await this.handleTenantBash(req, res); } else if (req.method === 'POST' && url.pathname === '/start-tenant') { await this.handleStartTenant(req, res); } else if (req.method === 'POST' && url.pathname === '/stop-tenant') { await this.handleStopTenant(req, res); } else if (req.method === 'GET' && url.pathname === '/status') { await this.handleStatus(req, res); } else { res.writeHead(404); res.end(JSON.stringify({ error: 'Not found' })); } } catch (error) { console.error('โŒ Tenant service error:', error.message); res.writeHead(500); res.end(JSON.stringify({ error: error.message })); } }); const PORT = process.env.TENANT_SERVICE_PORT || 3100; this.server.listen(PORT, () => { console.error(`โœ… Tenant Backend Service ready - listening on port ${PORT}`); console.error(`๐Ÿ“ก Tenant endpoint: http://localhost:${PORT}/tenant-bash`); console.error(`๐ŸŽฏ Status endpoint: http://localhost:${PORT}/status`); }); // Auto-cleanup on exit process.on('SIGINT', () => this.cleanup()); process.on('SIGTERM', () => this.cleanup()); } async handleStartTenant(req, res) { let body = ''; for await (const chunk of req) { body += chunk; } const { sessionId, workspace } = JSON.parse(body); if (!sessionId) { res.writeHead(400); res.end(JSON.stringify({ error: 'Session ID required' })); return; } // Create isolated workspace for tenant const tenantWorkspace = workspace || `/tmp/tenant-${sessionId}`; await mkdir(tenantWorkspace, { recursive: true }); // Start persistent bash process for tenant const bashShell = process.platform === 'win32' ? 'bash.exe' : '/bin/bash'; const bashProcess = spawn(bashShell, [], { cwd: tenantWorkspace, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PS1: `[tenant-${sessionId.substring(0, 8)}]\$ `, TERM: 'xterm-256color', GIT_TERMINAL_PROMPT: '0', // Prevent interactive prompts GIT_ASKPASS: 'echo', // No password prompts PWD: tenantWorkspace // Set working directory // Removed GIT_DIR and GIT_WORK_TREE to prevent conflicts with git clone operations } }); const port = this.portCounter++; this.tenantProcesses.set(sessionId, { process: bashProcess, workspace: tenantWorkspace, port: port, startTime: Date.now(), lastActivity: Date.now(), timeoutTimer: null }); // Setup activity timeout if enabled if (this.enableTimeout) { this.setupActivityTimeout(sessionId); } console.error(`๐Ÿ—๏ธ Started tenant backend: ${sessionId.substring(0, 8)}... in ${tenantWorkspace}`); // Handle bash process lifecycle bashProcess.on('exit', (code) => { console.error(`๐Ÿ”š Tenant process exited: ${sessionId.substring(0, 8)}... (code: ${code})`); this.tenantProcesses.delete(sessionId); }); bashProcess.on('error', (error) => { console.error(`โŒ Tenant process error: ${sessionId.substring(0, 8)}... - ${error.message}`); }); res.writeHead(200); res.end(JSON.stringify({ success: true, sessionId, workspace: tenantWorkspace, port, message: `Tenant backend started for ${sessionId.substring(0, 8)}...` })); } async handleTenantBash(req, res) { let body = ''; for await (const chunk of req) { body += chunk; } const { sessionId, command } = JSON.parse(body); if (!sessionId || !command) { res.writeHead(400); res.end(JSON.stringify({ error: 'Session ID and command required' })); return; } const tenant = this.tenantProcesses.get(sessionId); if (!tenant) { res.writeHead(404); res.end(JSON.stringify({ error: 'Tenant session not found' })); return; } try { // Update activity timestamp and reset timeout if tracking enabled if (this.activityTracking && this.enableTimeout) { tenant.lastActivity = Date.now(); this.resetActivityTimeout(sessionId); } // Check if this is an upload command and we have upload manager if (this.uploadManager && command.includes('curl -F') && command.includes('0x0.st')) { console.error(`๐Ÿ”„ Upload command detected, using resilient upload manager for ${sessionId.substring(0, 8)}...`); const uploadResult = await this.uploadManager.uploadFromCommand(command, tenant.workspace, sessionId); if (uploadResult.success) { res.writeHead(200); res.end(JSON.stringify({ success: true, sessionId, command, stdout: uploadResult.message || uploadResult.url, stderr: '', exit_code: 0, workspace: tenant.workspace, isolation: 'persistent_tenant_backend_with_resilient_upload', timeout_settings: { enabled: this.enableTimeout, timeout_ms: this.defaultTimeout, activity_tracking: this.activityTracking }, upload_service: uploadResult.service, upload_url: uploadResult.url, upload_raw_url: uploadResult.raw_url, fallback_used: uploadResult.service !== '0x0.st', pat_used: 'Not applicable for upload commands' })); } else { res.writeHead(500); res.end(JSON.stringify({ success: false, error: uploadResult.error, sessionId, command, workspace: tenant.workspace })); } } else { // Execute command in tenant's persistent bash process const result = await this.executeInTenantBash(tenant.process, command, tenant.workspace, sessionId); res.writeHead(200); res.end(JSON.stringify({ success: true, sessionId, command, stdout: result.stdout, stderr: result.stderr, exit_code: result.exit_code, workspace: tenant.workspace, isolation: 'persistent_tenant_backend', timeout_settings: { enabled: this.enableTimeout, timeout_ms: this.defaultTimeout, activity_tracking: this.activityTracking }, pat_used: result.pat_used })); } } catch (error) { res.writeHead(500); res.end(JSON.stringify({ success: false, error: error.message, sessionId, command })); } } async executeInTenantBash(bashProcess, command, workspace, sessionId) { return new Promise(async (resolve) => { let stdout = ''; let stderr = ''; // Enhanced git command handling with PAT support (ported from server-universal.js) let enhancedCommand = command; const githubPAT = process.env.GITHUB_PAT; const hasPAT = !!githubPAT; // Automatically configure git for GitHub operations if PAT is available if (hasPAT && command.startsWith('git')) { // For git clone, embed PAT in URL if (command.includes('git clone https://github.com/')) { enhancedCommand = command.replace( 'https://github.com/', `https://${githubPAT}@github.com/` ); console.error('๐Ÿ” Auto-configured git clone with PAT [HIDDEN]'); } // For git push, configure remote URL with PAT else if (command.includes('git push')) { // First configure the remote URL with PAT, then push enhancedCommand = ` git remote set-url origin https://${githubPAT}@github.com/$(git config --get remote.origin.url | sed 's/.*github\\.com\\///') git push origin `; console.error('๐Ÿ” Auto-configured git push with PAT [HIDDEN]'); } // For other git operations that might need authentication else if (command.includes('git pull') || command.includes('git fetch')) { enhancedCommand = command.replace( 'origin', `https://${githubPAT}@github.com/$(git config --get remote.origin.url | sed 's/.*github\\.com\\///')` ); console.error('๐Ÿ” Auto-configured git pull/fetch with PAT [HIDDEN]'); } } // Use a proper command execution approach instead of fixed timeout const { spawn } = await import('child_process'); // Use cross-platform shell detection const shell = process.platform === 'win32' ? 'cmd.exe' : '/bin/bash'; console.error(`๐Ÿ”ง Using shell: ${shell} on platform: ${process.platform}`); // Spawn a temporary process for this specific command const commandProcess = spawn(enhancedCommand, [], { cwd: workspace, shell: true, env: { ...process.env, GIT_TERMINAL_PROMPT: '0', // Prevent interactive prompts GIT_ASKPASS: 'echo', // No password prompts PWD: workspace // Set working directory // Removed GIT_DIR and GIT_WORK_TREE to prevent conflicts with git clone operations } }); let outputBuffer = ''; let errorBuffer = ''; commandProcess.stdout.on('data', (data) => { const output = data.toString(); stdout += output; outputBuffer += output; }); commandProcess.stderr.on('data', (data) => { const error = data.toString(); stderr += error; errorBuffer += error; }); commandProcess.on('close', (code) => { console.error(`๐Ÿ”š Tenant command completed: ${sessionId.substring(0, 8)}... (exit code: ${code})`); resolve({ stdout: stdout, stderr: stderr, exit_code: code || 0, pat_used: hasPAT && command.startsWith('git') ? 'Auto-configured with stored PAT' : undefined, command: command, enhanced_command: enhancedCommand !== command ? enhancedCommand : undefined }); }); commandProcess.on('error', (error) => { console.error(`โŒ Tenant command error: ${sessionId.substring(0, 8)}... - ${error.message}`); resolve({ stdout: stdout, stderr: stderr + error.message, exit_code: -1, pat_used: hasPAT && command.startsWith('git') ? 'Auto-configured with stored PAT' : undefined, command: command, enhanced_command: enhancedCommand !== command ? enhancedCommand : undefined }); }); // Add timeout safety net (longer than before) const timeout = setTimeout(() => { if (!commandProcess.killed) { console.error(`โฐ Tenant command timeout: ${sessionId.substring(0, 8)}... - terminating`); commandProcess.kill('SIGTERM'); resolve({ stdout: stdout, stderr: stderr + '\nCommand timed out after 30 seconds', exit_code: -1, pat_used: hasPAT && command.startsWith('git') ? 'Auto-configured with stored PAT' : undefined, command: command, enhanced_command: enhancedCommand !== command ? enhancedCommand : undefined, timeout: true }); } }, 30000); // 30 seconds timeout instead of 1 second commandProcess.on('close', () => { clearTimeout(timeout); }); }); } async handleStopTenant(req, res) { let body = ''; for await (const chunk of req) { body += chunk; } const { sessionId } = JSON.parse(body); if (!sessionId) { res.writeHead(400); res.end(JSON.stringify({ error: 'Session ID required' })); return; } const tenant = this.tenantProcesses.get(sessionId); if (!tenant) { res.writeHead(404); res.end(JSON.stringify({ error: 'Tenant session not found' })); return; } // Terminate tenant process tenant.process.kill('SIGTERM'); this.tenantProcesses.delete(sessionId); console.error(`๐Ÿ›‘ Stopped tenant backend: ${sessionId.substring(0, 8)}...`); res.writeHead(200); res.end(JSON.stringify({ success: true, sessionId, message: `Tenant backend stopped for ${sessionId.substring(0, 8)}...` })); } async handleStatus(req, res) { const status = Array.from(this.tenantProcesses.entries()).map(([sessionId, tenant]) => { const lastActivityAgo = Date.now() - tenant.lastActivity; const timeUntilTimeout = this.enableTimeout ? this.defaultTimeout - lastActivityAgo : -1; return { sessionId: sessionId.substring(0, 8) + '...', workspace: tenant.workspace, port: tenant.port, uptime: Date.now() - tenant.startTime, last_activity: lastActivityAgo, time_until_timeout: timeUntilTimeout > 0 ? timeUntilTimeout : 0, timeout_enabled: this.enableTimeout, timeout_minutes: this.defaultTimeout / 1000 / 60, active: true }; }); res.writeHead(200); res.end(JSON.stringify({ service: 'Tenant Backend Service', uptime: process.uptime(), activeTenants: this.tenantProcesses.size, configuration: { timeout_enabled: this.enableTimeout, timeout_minutes: this.defaultTimeout / 1000 / 60, activity_tracking: this.activityTracking }, tenants: status })); } setupActivityTimeout(sessionId) { const tenant = this.tenantProcesses.get(sessionId); if (!tenant || !this.enableTimeout) return; // Clear existing timer if (tenant.timeoutTimer) { clearTimeout(tenant.timeoutTimer); } // Set new timeout timer tenant.timeoutTimer = setTimeout(() => { console.error(`โฐ Session timeout for tenant: ${sessionId.substring(0, 8)}... (${this.defaultTimeout / 1000 / 60} minutes inactivity)`); this.terminateTenantSession(sessionId); }, this.defaultTimeout); } resetActivityTimeout(sessionId) { if (!this.activityTracking || !this.enableTimeout) return; const tenant = this.tenantProcesses.get(sessionId); if (tenant) { console.error(`๐Ÿ”„ Activity detected for tenant: ${sessionId.substring(0, 8)}... - resetting timeout`); this.setupActivityTimeout(sessionId); } } terminateTenantSession(sessionId) { const tenant = this.tenantProcesses.get(sessionId); if (!tenant) return; console.error(`๐Ÿ”š Terminating inactive tenant session: ${sessionId.substring(0, 8)}...`); // Clear timeout timer if (tenant.timeoutTimer) { clearTimeout(tenant.timeoutTimer); } // Terminate bash process gracefully tenant.process.kill('SIGTERM'); this.tenantProcesses.delete(sessionId); console.error(`โœ… Inactive tenant session terminated: ${sessionId.substring(0, 8)}...`); } cleanup() { console.error('๐Ÿงน Cleaning up tenant processes...'); for (const [sessionId, tenant] of this.tenantProcesses) { console.error(`๐Ÿ›‘ Stopping tenant: ${sessionId.substring(0, 8)}...`); // Clear timeout timer if (tenant.timeoutTimer) { clearTimeout(tenant.timeoutTimer); } tenant.process.kill('SIGTERM'); } this.tenantProcesses.clear(); if (this.server) { this.server.close(); } process.exit(0); } } // Auto-start the service const tenantService = new TenantBackendService(); tenantService.initialize().catch(console.error); // Auto-restart on crash process.on('uncaughtException', (error) => { console.error('๐Ÿ’ฅ Uncaught exception, restarting service:', error.message); tenantService.cleanup(); }); process.on('unhandledRejection', (reason) => { console.error('๐Ÿ’ฅ Unhandled rejection, restarting service:', reason); tenantService.cleanup(); });

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/pythondev-pro/egw_writings_mcp_server'

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