mcp-ssh-tool
Version:
Model Context Protocol (MCP) SSH client server for remote automation
269 lines • 9.52 kB
JavaScript
import { wrapError } from './errors.js';
import { logger } from './logging.js';
import { sessionManager } from './session.js';
import { ErrorCode } from './types.js';
/**
* Reads a file from the remote system
*/
export async function readFile(sessionId, path, encoding = 'utf8') {
logger.debug('Reading file', { sessionId, path, encoding });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
const data = await session.sftp.get(path);
const result = Buffer.isBuffer(data) ? data.toString(encoding) : String(data);
logger.debug('File read successfully', { sessionId, path, size: result.length });
return result;
}
catch (error) {
logger.error('Failed to read file', { sessionId, path, error });
throw wrapError(error, ErrorCode.EFS, `Failed to read file ${path}. Check if the file exists and is readable.`);
}
}
/**
* Writes data to a file on the remote system (atomic operation using temp file)
*/
export async function writeFile(sessionId, path, data, mode) {
logger.debug('Writing file', { sessionId, path, size: data.length, mode });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
// Use atomic write: write to temp file, then rename
const tempPath = `${path}.tmp.${Date.now()}`;
try {
// Write to temporary file
await session.sftp.put(Buffer.from(data, 'utf8'), tempPath);
// Set permissions if specified
if (mode !== undefined) {
await session.sftp.chmod(tempPath, mode);
}
// Atomic rename
await session.sftp.rename(tempPath, path);
logger.debug('File written successfully', { sessionId, path });
return true;
}
catch (writeError) {
// Clean up temp file on failure
try {
await session.sftp.delete(tempPath);
logger.debug('Cleaned up temp file after error', { tempPath });
}
catch (cleanupError) {
logger.warn('Failed to clean up temp file', { tempPath, cleanupError });
}
throw writeError;
}
}
catch (error) {
logger.error('Failed to write file', { sessionId, path, error });
throw wrapError(error, ErrorCode.EFS, `Failed to write file ${path}. Check directory permissions and disk space.`);
}
}
/**
* Gets file/directory statistics
*/
export async function statFile(sessionId, path) {
logger.debug('Getting file stats', { sessionId, path });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
const stats = await session.sftp.stat(path);
let type = 'other';
if (stats.isFile && stats.isFile()) {
type = 'file';
}
else if (stats.isDirectory && stats.isDirectory()) {
type = 'directory';
}
else if (stats.isSymbolicLink && stats.isSymbolicLink()) {
type = 'symlink';
}
const statInfo = {
size: stats.size,
mtime: new Date(stats.mtime ? stats.mtime * 1000 : Date.now()),
mode: stats.mode,
type
};
logger.debug('File stats retrieved', { sessionId, path, type, size: stats.size });
return statInfo;
}
catch (error) {
logger.error('Failed to get file stats', { sessionId, path, error });
throw wrapError(error, ErrorCode.EFS, `Failed to get stats for ${path}. Check if the path exists.`);
}
}
/**
* Lists directory contents with pagination
*/
export async function listDirectory(sessionId, path, page, limit = 100) {
logger.debug('Listing directory', { sessionId, path, page, limit });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
const fileList = await session.sftp.list(path);
// Convert to our DirEntry format
const entries = fileList.map((item) => {
let type = 'other';
if (item.type === 'd') {
type = 'directory';
}
else if (item.type === '-') {
type = 'file';
}
else if (item.type === 'l') {
type = 'symlink';
}
return {
name: item.name,
type,
size: item.size,
mtime: new Date(item.modifyTime),
mode: item.rights ? parseInt(item.rights.toString(), 8) : undefined
};
});
// Apply pagination if requested
if (page !== undefined) {
const startIndex = page * limit;
const endIndex = startIndex + limit;
const paginatedEntries = entries.slice(startIndex, endIndex);
const hasMore = endIndex < entries.length;
const nextToken = hasMore ? String(page + 1) : undefined;
logger.debug('Directory listed with pagination', {
sessionId,
path,
total: entries.length,
page,
returned: paginatedEntries.length,
hasMore
});
return {
entries: paginatedEntries,
nextToken
};
}
logger.debug('Directory listed', { sessionId, path, count: entries.length });
return { entries };
}
catch (error) {
logger.error('Failed to list directory', { sessionId, path, error });
throw wrapError(error, ErrorCode.EFS, `Failed to list directory ${path}. Check if the directory exists and is readable.`);
}
}
/**
* Creates directories recursively (mkdir -p equivalent)
*/
export async function makeDirectories(sessionId, path) {
logger.debug('Creating directories', { sessionId, path });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
await session.sftp.mkdir(path, true); // recursive = true
logger.debug('Directories created successfully', { sessionId, path });
return true;
}
catch (error) {
logger.error('Failed to create directories', { sessionId, path, error });
throw wrapError(error, ErrorCode.EFS, `Failed to create directories ${path}. Check parent directory permissions.`);
}
}
/**
* Removes files or directories recursively (rm -rf equivalent)
*/
export async function removeRecursive(sessionId, path) {
logger.debug('Removing path recursively', { sessionId, path });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
// Check if path exists and get its type
const stats = await session.sftp.stat(path);
if (stats.isDirectory && stats.isDirectory()) {
// Remove directory recursively
await session.sftp.rmdir(path, true); // recursive = true
}
else {
// Remove file
await session.sftp.delete(path);
}
logger.debug('Path removed successfully', { sessionId, path });
return true;
}
catch (error) {
logger.error('Failed to remove path', { sessionId, path, error });
throw wrapError(error, ErrorCode.EFS, `Failed to remove ${path}. Check if the path exists and you have write permissions.`);
}
}
/**
* Renames/moves a file or directory
*/
export async function renameFile(sessionId, from, to) {
logger.debug('Renaming file', { sessionId, from, to });
const session = sessionManager.getSession(sessionId);
if (!session) {
throw new Error(`Session ${sessionId} not found or expired`);
}
try {
await session.sftp.rename(from, to);
logger.debug('File renamed successfully', { sessionId, from, to });
return true;
}
catch (error) {
logger.error('Failed to rename file', { sessionId, from, to, error });
throw wrapError(error, ErrorCode.EFS, `Failed to rename ${from} to ${to}. Check if the source exists and destination is writable.`);
}
}
/**
* Checks if a path exists on the remote system
*/
export async function pathExists(sessionId, path) {
try {
await statFile(sessionId, path);
return true;
}
catch (error) {
return false;
}
}
/**
* Gets the size of a file
*/
export async function getFileSize(sessionId, path) {
const stats = await statFile(sessionId, path);
return stats.size;
}
/**
* Checks if a path is a directory
*/
export async function isDirectory(sessionId, path) {
try {
const stats = await statFile(sessionId, path);
return stats.type === 'directory';
}
catch (error) {
return false;
}
}
/**
* Checks if a path is a file
*/
export async function isFile(sessionId, path) {
try {
const stats = await statFile(sessionId, path);
return stats.type === 'file';
}
catch (error) {
return false;
}
}
//# sourceMappingURL=fs-tools.js.map