UNPKG

mcp-ssh-tool

Version:

Model Context Protocol (MCP) SSH client server for remote automation

319 lines 11.9 kB
import { createPackageManagerError, createSudoError, createFilesystemError, createPatchError } from './errors.js'; import { logger } from './logging.js'; import { execCommand, execSudo, commandExists } from './process.js'; import { readFile, writeFile, pathExists } from './fs-tools.js'; import { detectOS } from './detect.js'; import { sessionManager } from './session.js'; /** * Ensures a package is installed on the system */ export async function ensurePackage(sessionId, packageName, sudoPassword) { logger.debug('Ensuring package is installed', { sessionId, packageName }); const session = sessionManager.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found or expired`); } try { // Detect OS and package manager const osInfo = await detectOS(session.ssh); const pm = osInfo.packageManager; if (pm === 'unknown') { throw createPackageManagerError('No supported package manager found', 'Supported package managers: apt, dnf, yum, pacman, apk'); } logger.debug('Detected package manager', { sessionId, pm }); // Check if package is already installed const isInstalled = await checkPackageInstalled(sessionId, packageName, pm); if (isInstalled) { logger.info('Package already installed', { sessionId, packageName }); return { ok: true, pm, code: 0, stdout: `Package ${packageName} is already installed`, stderr: '' }; } // Install the package const installCommand = getInstallCommand(pm, packageName); logger.debug('Installing package', { sessionId, packageName, command: installCommand }); const result = await execSudo(sessionId, installCommand, sudoPassword); const packageResult = { ok: result.code === 0, pm, code: result.code, stdout: result.stdout, stderr: result.stderr }; if (result.code === 0) { logger.info('Package installed successfully', { sessionId, packageName }); } else { logger.error('Package installation failed', { sessionId, packageName, code: result.code }); } return packageResult; } catch (error) { logger.error('Failed to ensure package', { sessionId, packageName, error }); throw error; } } /** * Ensures a service is in the desired state */ export async function ensureService(sessionId, serviceName, state, sudoPassword) { logger.debug('Ensuring service state', { sessionId, serviceName, state }); const session = sessionManager.getSession(sessionId); if (!session) { throw new Error(`Session ${sessionId} not found or expired`); } try { // Detect init system const osInfo = await detectOS(session.ssh); const initSystem = osInfo.init; if (initSystem === 'unknown') { throw createSudoError('No supported init system found', 'Supported init systems: systemd, service'); } logger.debug('Detected init system', { sessionId, initSystem }); let command; if (initSystem === 'systemd') { switch (state) { case 'started': command = `systemctl start ${serviceName}`; break; case 'stopped': command = `systemctl stop ${serviceName}`; break; case 'restarted': command = `systemctl restart ${serviceName}`; break; case 'enabled': command = `systemctl enable ${serviceName}`; break; case 'disabled': command = `systemctl disable ${serviceName}`; break; } } else { // Traditional service command switch (state) { case 'started': command = `service ${serviceName} start`; break; case 'stopped': command = `service ${serviceName} stop`; break; case 'restarted': command = `service ${serviceName} restart`; break; case 'enabled': command = `chkconfig ${serviceName} on || update-rc.d ${serviceName} enable`; break; case 'disabled': command = `chkconfig ${serviceName} off || update-rc.d ${serviceName} disable`; break; } } logger.debug('Executing service command', { sessionId, serviceName, command }); const result = await execSudo(sessionId, command, sudoPassword); const serviceResult = { ok: result.code === 0 }; if (result.code === 0) { logger.info('Service state changed successfully', { sessionId, serviceName, state }); } else { logger.error('Service state change failed', { sessionId, serviceName, state, code: result.code, stderr: result.stderr }); } return serviceResult; } catch (error) { logger.error('Failed to ensure service state', { sessionId, serviceName, state, error }); throw error; } } /** * Ensures specific lines exist in a file */ export async function ensureLinesInFile(sessionId, filePath, lines, createIfMissing = true, sudoPassword) { logger.debug('Ensuring lines in file', { sessionId, filePath, lineCount: lines.length }); try { let fileContent = ''; let fileExists = false; // Check if file exists and read its content if (await pathExists(sessionId, filePath)) { fileExists = true; fileContent = await readFile(sessionId, filePath); } else if (!createIfMissing) { throw createFilesystemError(`File ${filePath} does not exist and createIfMissing is false`); } // Check which lines are missing const existingLines = fileContent.split('\n'); const missingLines = []; for (const line of lines) { if (!existingLines.includes(line)) { missingLines.push(line); } } if (missingLines.length === 0) { logger.info('All lines already exist in file', { sessionId, filePath }); return { ok: true, added: 0 }; } // Add missing lines const newContent = fileExists ? fileContent + '\n' + missingLines.join('\n') : missingLines.join('\n'); // Write file (may need sudo) try { await writeFile(sessionId, filePath, newContent); } catch (error) { if (sudoPassword) { // Try with sudo by writing to temp file and moving const tempFile = `/tmp/ssh-mcp-${Date.now()}.tmp`; await writeFile(sessionId, tempFile, newContent); const moveResult = await execSudo(sessionId, `mv ${tempFile} ${filePath}`, sudoPassword); if (moveResult.code !== 0) { throw createFilesystemError(`Failed to move temporary file to ${filePath}`, 'Check file permissions and sudo access'); } } else { throw error; } } logger.info('Lines added to file successfully', { sessionId, filePath, added: missingLines.length }); return { ok: true, added: missingLines.length }; } catch (error) { logger.error('Failed to ensure lines in file', { sessionId, filePath, error }); throw error; } } /** * Applies a patch to a file */ export async function applyPatch(sessionId, filePath, diff, sudoPassword) { logger.debug('Applying patch to file', { sessionId, filePath }); try { // Check if patch command is available const hasPatch = await commandExists(sessionId, 'patch'); if (!hasPatch) { throw createPatchError('patch command not found on remote system', 'Install patch utility or apply changes manually'); } // Write patch to temporary file const tempPatchFile = `/tmp/ssh-mcp-patch-${Date.now()}.patch`; await writeFile(sessionId, tempPatchFile, diff); try { // Test patch first (dry run) const testResult = await execCommand(sessionId, `patch --dry-run -p0 ${filePath} < ${tempPatchFile}`); if (testResult.code !== 0) { throw createPatchError('Patch would fail to apply', `Patch test failed: ${testResult.stderr}`); } // Apply patch const applyCommand = `patch -p0 ${filePath} < ${tempPatchFile}`; let result; if (sudoPassword) { result = await execSudo(sessionId, applyCommand, sudoPassword); } else { result = await execCommand(sessionId, applyCommand); } const patchResult = { ok: result.code === 0, changed: result.code === 0 }; if (result.code === 0) { logger.info('Patch applied successfully', { sessionId, filePath }); } else { logger.error('Patch application failed', { sessionId, filePath, code: result.code, stderr: result.stderr }); } return patchResult; } finally { // Clean up temporary patch file try { await execCommand(sessionId, `rm -f ${tempPatchFile}`); } catch (error) { logger.warn('Failed to clean up temporary patch file', { tempPatchFile, error }); } } } catch (error) { logger.error('Failed to apply patch', { sessionId, filePath, error }); throw error; } } /** * Checks if a package is installed using the appropriate package manager */ async function checkPackageInstalled(sessionId, packageName, pm) { let checkCommand; switch (pm) { case 'apt': checkCommand = `dpkg -l ${packageName} | grep -q '^ii'`; break; case 'dnf': case 'yum': checkCommand = `${pm} list installed ${packageName}`; break; case 'pacman': checkCommand = `pacman -Q ${packageName}`; break; case 'apk': checkCommand = `apk info -e ${packageName}`; break; default: return false; } try { const result = await execCommand(sessionId, checkCommand); return result.code === 0; } catch (error) { return false; } } /** * Gets the install command for the appropriate package manager */ function getInstallCommand(pm, packageName) { switch (pm) { case 'apt': return `apt-get update && apt-get install -y ${packageName}`; case 'dnf': return `dnf install -y ${packageName}`; case 'yum': return `yum install -y ${packageName}`; case 'pacman': return `pacman -S --noconfirm ${packageName}`; case 'apk': return `apk add ${packageName}`; default: throw createPackageManagerError(`Unsupported package manager: ${pm}`); } } //# sourceMappingURL=ensure.js.map