Skip to main content
Glama

mcp-with-ssh

SshUtils.ts11.1 kB
import * as fs from 'fs'; import { exec as execCallback, ExecException } from 'child_process'; import * as util from 'util'; import { logger } from './LogManager.js'; // Type for the exec callback function type ExecCallback = (error: ExecException | null, stdout: string, stderr: string) => void; /** * Utility class for SSH operations * * Provides methods for interacting with remote servers via SSH. */ export class SshUtils { private sshKeyPath: string; private remoteUser: string; private remoteHost: string; private remotePath: string; /** * Creates a new SshUtils instance * * @param sshKeyPath - Path to the SSH private key file * @param remoteUser - Username for the remote server * @param remoteHost - Hostname or IP address of the remote server * @param remotePath - Base path on the remote server for memory bank storage */ constructor(sshKeyPath: string, remoteUser: string, remoteHost: string, remotePath: string) { this.sshKeyPath = sshKeyPath; this.remoteUser = remoteUser; this.remoteHost = remoteHost; this.remotePath = remotePath; } /** * Executes an SSH command * * @param command - Command to execute * @returns Promise that resolves with command output */ private async executeCommand(command: string): Promise<string> { return new Promise((resolve, reject) => { // Build SSH command with options const sshCommand = `ssh -v -i "${this.sshKeyPath}" -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ServerAliveCountMax=3 ${this.remoteUser}@${this.remoteHost} "${command}"`; logger.debug('SshUtils', `Executing SSH command: ${sshCommand}`); // Set a timeout for the command execution (30 seconds) const timeoutMs = 30000; let timeoutId: NodeJS.Timeout | null = null; // Execute SSH command const childProcess = execCallback(sshCommand, (error, stdout, stderr) => { // Clear the timeout if it was set if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } if (error) { logger.error('SshUtils', `SSH command error: ${error.message}`); logger.error('SshUtils', `SSH command stderr: ${stderr}`); logger.error('SshUtils', `SSH command stdout: ${stdout}`); logger.error('SshUtils', `Full SSH command: ${sshCommand}`); logger.error('SshUtils', `Error details: ${JSON.stringify(error)}`); reject(error); return; } if (stderr) { logger.warn('SshUtils', `SSH command stderr: ${stderr}`); } logger.debug('SshUtils', `SSH command stdout: ${stdout}`); resolve(stdout); }); // Set up the timeout timeoutId = setTimeout(() => { if (childProcess) { childProcess.kill(); } logger.error('SshUtils', `SSH command timed out after ${timeoutMs}ms: ${sshCommand}`); reject(new Error(`SSH command timed out after ${timeoutMs}ms`)); }, timeoutMs); }); } /** * Checks if a remote directory exists * * @param dirPath - Path to check * @returns True if the directory exists, false otherwise */ async directoryExists(dirPath: string): Promise<boolean> { try { const remoteDirPath = `${this.remotePath}/${dirPath}`; const command = `[ -d "${remoteDirPath}" ] && echo "EXISTS" || echo "NOT_EXISTS"`; const result = await this.executeCommand(command); return result === 'EXISTS'; } catch (error) { logger.error('SshUtils', `Error checking if remote directory exists: ${error}`); return false; } } /** * Creates a directory on the remote server * * @param dirPath - Path to create * @throws Error if directory creation fails */ async createDirectory(dirPath: string): Promise<void> { try { const remoteDirPath = `${this.remotePath}/${dirPath}`; const command = `mkdir -p "${remoteDirPath}"`; await this.executeCommand(command); } catch (error) { logger.error('SshUtils', `Failed to create remote directory: ${error}`); throw new Error(`Failed to create remote directory: ${error instanceof Error ? error.message : String(error)}`); } } /** * Reads a file from the remote server * * @param filePath - Path to the file * @returns The file contents as a string * @throws Error if file reading fails */ async readFile(filePath: string): Promise<string> { try { const remoteFilePath = `${this.remotePath}/${filePath}`; const command = `cat "${remoteFilePath}"`; return await this.executeCommand(command); } catch (error) { logger.error('SshUtils', `Failed to read remote file: ${error}`); throw new Error(`Failed to read remote file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Writes content to a file on the remote server * * @param filePath - Path to the file * @param content - Content to write * @throws Error if file writing fails */ async writeFile(filePath: string, content: string): Promise<void> { try { const remoteFilePath = `${this.remotePath}/${filePath}`; // Ensure the directory exists first const dirPath = remoteFilePath.substring(0, remoteFilePath.lastIndexOf('/')); const mkdirCommand = `mkdir -p "${dirPath}"`; await this.executeCommand(mkdirCommand); // Use base64 encoding to safely transfer the content // This avoids issues with shell interpretation of special characters const contentBuffer = Buffer.from(content); const base64Content = contentBuffer.toString('base64'); // Create the command that will decode the base64 and write to the file const writeCommand = `echo "${base64Content}" | base64 -d > "${remoteFilePath}"`; await this.executeCommand(writeCommand); // Verify file was written successfully const checkCommand = `[ -f "${remoteFilePath}" ] && echo "FILE_EXISTS" || echo "FILE_NOT_EXISTS"`; const checkResult = await this.executeCommand(checkCommand); if (checkResult.trim() !== "FILE_EXISTS") { throw new Error(`Failed to verify file was created: ${remoteFilePath}`); } } catch (error) { logger.error('SshUtils', `Failed to write to remote file: ${error}`); throw new Error(`Failed to write to remote file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Lists files in a remote directory * * @param dirPath - Path to the directory * @returns Array of file names * @throws Error if directory reading fails */ async listFiles(dirPath: string): Promise<string[]> { try { const remoteDirPath = `${this.remotePath}/${dirPath}`; const command = `ls -A "${remoteDirPath}" | tr '\\n' ' '`; const result = await this.executeCommand(command); return result ? result.split(' ').filter(Boolean) : []; } catch (error) { logger.error('SshUtils', `Failed to list files in remote directory: ${error}`); throw new Error(`Failed to list files in remote directory: ${error instanceof Error ? error.message : String(error)}`); } } /** * Deletes a file on the remote server * * @param filePath - Path to the file * @throws Error if file deletion fails */ async deleteFile(filePath: string): Promise<void> { try { const remoteFilePath = `${this.remotePath}/${filePath}`; const command = `rm "${remoteFilePath}"`; await this.executeCommand(command); } catch (error) { logger.error('SshUtils', `Failed to delete remote file: ${error}`); throw new Error(`Failed to delete remote file: ${error instanceof Error ? error.message : String(error)}`); } } /** * Copies a file or directory on the remote server * * @param sourcePath - Source path * @param destPath - Destination path * @throws Error if copy operation fails */ async copy(sourcePath: string, destPath: string): Promise<void> { try { const remoteSourcePath = `${this.remotePath}/${sourcePath}`; const remoteDestPath = `${this.remotePath}/${destPath}`; const command = `cp -r "${remoteSourcePath}" "${remoteDestPath}"`; await this.executeCommand(command); } catch (error) { logger.error('SshUtils', `Failed to copy from ${sourcePath} to ${destPath}: ${error}`); throw new Error(`Failed to copy from ${sourcePath} to ${destPath}: ${error instanceof Error ? error.message : String(error)}`); } } /** * Tests the SSH connection to the remote server * * @returns True if connection is successful, false otherwise */ async testConnection(): Promise<boolean> { try { // First check if the SSH key file exists try { const fsPromises = require('fs').promises; const stats = await fsPromises.stat(this.sshKeyPath); if (!stats.isFile()) { console.error(`SSH key is not a file: ${this.sshKeyPath}`); return false; } } catch (fileError) { console.error(`SSH key file does not exist or cannot be accessed: ${this.sshKeyPath}`); console.error(`Error details: ${fileError}`); return false; } const command = 'echo "Connection successful"'; console.log(`Testing SSH connection to ${this.remoteUser}@${this.remoteHost} using key ${this.sshKeyPath}`); console.log(`Remote path: ${this.remotePath}`); const sshTestCommand = `ssh -v -i "${this.sshKeyPath}" -o StrictHostKeyChecking=no ${this.remoteUser}@${this.remoteHost} "${command}"`; console.log(`Direct SSH test command: ${sshTestCommand}`); const result = await this.executeCommand(command); console.log(`SSH test result: "${result}"`); // Check if the remote path exists try { const pathCheckCommand = `[ -d "${this.remotePath}" ] && echo "PATH_EXISTS" || echo "PATH_NOT_EXISTS"`; const pathResult = await this.executeCommand(pathCheckCommand); console.log(`Remote path check result: "${pathResult}"`); if (pathResult.trim() !== "PATH_EXISTS") { console.log(`Warning: Remote path ${this.remotePath} does not exist. Attempting to create it...`); const mkdirCommand = `mkdir -p "${this.remotePath}"`; await this.executeCommand(mkdirCommand); console.log(`Created remote path ${this.remotePath}`); } } catch (pathError) { console.error(`Error checking remote path: ${pathError}`); } // The connection test is successful if we received any response return result.includes('Connection successful'); } catch (error) { logger.error('SshUtils', `Failed to connect to remote server: ${error}`); console.error('Connection test error details:', error); if (error instanceof Error) { console.error('Error stack:', error.stack); } return false; } } }

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/aakarsh-sasi/memory-bank-mcp'

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