@xcud/remote-commander
Version:
MCP server for remote file operations via REST API
208 lines (207 loc) • 8.16 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { existsSync } from 'fs';
import { mkdir } from 'fs/promises';
import os from 'os';
import dotenv from 'dotenv';
import { VERSION } from './version.js';
import { CONFIG_FILE } from './config.js';
/**
* Singleton config manager for the server
*/
class ConfigManager {
constructor() {
this.config = {};
this.initialized = false;
// Get user's home directory
// Define config directory and file paths
this.configPath = CONFIG_FILE;
}
/**
* Initialize configuration - load from disk or create default
*/
async init() {
if (this.initialized)
return;
// Load environment variables from .env file with absolute path
// Calculate path to .env file relative to this source file location
const currentDir = path.dirname(new URL(import.meta.url).pathname);
const projectRoot = path.resolve(currentDir, '..');
const envPath = path.join(projectRoot, '.env');
dotenv.config({ path: envPath });
try {
// Ensure config directory exists
const configDir = path.dirname(this.configPath);
if (!existsSync(configDir)) {
await mkdir(configDir, { recursive: true });
}
// Check if config file exists
try {
await fs.access(this.configPath);
// Load existing config
const configData = await fs.readFile(this.configPath, 'utf8');
this.config = JSON.parse(configData);
}
catch (error) {
// Config file doesn't exist, create default
this.config = this.getDefaultConfig();
await this.saveConfig();
}
// Override with environment variables if present
if (process.env.REMOTE_COMMANDER_SERVER_URL) {
this.config.serverUrl = process.env.REMOTE_COMMANDER_SERVER_URL;
}
if (process.env.REMOTE_COMMANDER_AUTH_URL) {
this.config.authUrl = process.env.REMOTE_COMMANDER_AUTH_URL;
}
if (process.env.REMOTE_COMMANDER_USERNAME && process.env.REMOTE_COMMANDER_PASSWORD) {
// We'll handle auth token generation in the HTTP client
this.config.username = process.env.REMOTE_COMMANDER_USERNAME;
this.config.password = process.env.REMOTE_COMMANDER_PASSWORD;
}
if (process.env.REMOTE_COMMANDER_BASE_PATH) {
this.config.basePath = process.env.REMOTE_COMMANDER_BASE_PATH;
}
this.config['version'] = VERSION;
this.initialized = true;
}
catch (error) {
console.error('Failed to initialize config:', error);
// Fall back to default config in memory
this.config = this.getDefaultConfig();
this.initialized = true;
}
}
/**
* Alias for init() to maintain backward compatibility
*/
async loadConfig() {
return this.init();
}
/**
* Create default configuration
*/
getDefaultConfig() {
return {
blockedCommands: [
// Disk and partition management
"mkfs", // Create a filesystem on a device
"format", // Format a storage device (cross-platform)
"mount", // Mount a filesystem
"umount", // Unmount a filesystem
"fdisk", // Manipulate disk partition tables
"dd", // Convert and copy files, can write directly to disks
"parted", // Disk partition manipulator
"diskpart", // Windows disk partitioning utility
// System administration and user management
"sudo", // Execute command as superuser
"su", // Substitute user identity
"passwd", // Change user password
"adduser", // Add a user to the system
"useradd", // Create a new user
"usermod", // Modify user account
"groupadd", // Create a new group
"chsh", // Change login shell
"visudo", // Edit the sudoers file
// System control
"shutdown", // Shutdown the system
"reboot", // Restart the system
"halt", // Stop the system
"poweroff", // Power off the system
"init", // Change system runlevel
// Network and security
"iptables", // Linux firewall administration
"firewall", // Generic firewall command
"netsh", // Windows network configuration
// Windows system commands
"sfc", // System File Checker
"bcdedit", // Boot Configuration Data editor
"reg", // Windows registry editor
"net", // Network/user/service management
"sc", // Service Control manager
"runas", // Execute command as another user
"cipher", // Encrypt/decrypt files or wipe data
"takeown" // Take ownership of files
],
defaultShell: os.platform() === 'win32' ? 'powershell.exe' : '/bin/sh',
allowedDirectories: [],
telemetryEnabled: true, // Default to opt-out approach (telemetry on by default)
fileWriteLineLimit: 50, // Default line limit for file write operations (changed from 100)
fileReadLineLimit: 1000, // Default line limit for file read operations (changed from character-based)
// Remote commander defaults
serverUrl: "http://localhost:5000",
authToken: "", // Must be configured by user
basePath: "/data"
};
}
/**
* Save config to disk
*/
async saveConfig() {
try {
await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2), 'utf8');
}
catch (error) {
console.error('Failed to save config:', error);
throw error;
}
}
/**
* Get the entire config
*/
async getConfig() {
await this.init();
return { ...this.config };
}
/**
* Get a specific configuration value
*/
async getValue(key) {
await this.init();
return this.config[key];
}
/**
* Set a specific configuration value
*/
async setValue(key, value) {
await this.init();
// Special handling for telemetry opt-out
if (key === 'telemetryEnabled' && value === false) {
// Get the current value before changing it
const currentValue = this.config[key];
// Only capture the opt-out event if telemetry was previously enabled
if (currentValue !== false) {
// Import the capture function dynamically to avoid circular dependencies
const { capture } = await import('./utils/capture.js');
// Send a final telemetry event noting that the user has opted out
// This helps us track opt-out rates while respecting the user's choice
await capture('server_telemetry_opt_out', {
reason: 'user_disabled',
prev_value: currentValue
});
}
}
// Update the value
this.config[key] = value;
await this.saveConfig();
}
/**
* Update multiple configuration values at once
*/
async updateConfig(updates) {
await this.init();
this.config = { ...this.config, ...updates };
await this.saveConfig();
return { ...this.config };
}
/**
* Reset configuration to defaults
*/
async resetConfig() {
this.config = this.getDefaultConfig();
await this.saveConfig();
return { ...this.config };
}
}
// Export singleton instance
export const configManager = new ConfigManager();