@z-test/memory-bank-mcp
Version:
MCP Server for managing Memory Bank
240 lines • 8.38 kB
JavaScript
import SftpClient from 'ssh2-sftp-client';
import { logger } from '../../utils/LogManager.js';
import { join } from 'path';
export class SftpStorageProvider {
constructor(config) {
this.connected = false;
this.client = new SftpClient();
this.config = config;
}
async initialize(_config) {
try {
// Initialize SFTP client
this.client = new SftpClient();
// Connect to SFTP server
await this.client.connect({
host: this.config.host,
port: this.config.port,
username: this.config.username,
password: this.config.password,
privateKey: this.config.privateKey
});
logger.info('SftpStorageProvider', 'Successfully connected to SFTP server');
}
catch (error) {
logger.error('SftpStorageProvider', `Error initializing SFTP client: ${error}`);
throw error;
}
}
async connect() {
try {
await this.client.connect({
host: this.config.host,
port: this.config.port,
username: this.config.username,
...(this.config.password ? { password: this.config.password } : {}),
...(this.config.privateKey ? { privateKey: this.config.privateKey } : {})
});
this.connected = true;
logger.debug('SftpStorageProvider', 'Connected to SFTP server');
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to connect to SFTP server: ${error}`);
throw error;
}
}
async ensureConnected() {
if (!this.connected) {
await this.connect();
}
}
async exists(path) {
await this.ensureConnected();
try {
const fullPath = this.getFullPath(path);
const stats = await this.client.stat(fullPath);
return stats !== null;
}
catch (error) {
return false;
}
}
async createDirectory(path) {
await this.ensureConnected();
try {
const fullPath = this.getFullPath(path);
await this.client.mkdir(fullPath, true);
logger.debug('SftpStorageProvider', `Created directory: ${fullPath}`);
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to create directory: ${error}`);
throw error;
}
}
async readFile(path) {
await this.ensureConnected();
try {
const fullPath = this.getFullPath(path);
const buffer = await this.client.get(fullPath);
return buffer.toString('utf-8');
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to read file: ${error}`);
throw error;
}
}
async writeFile(path, content) {
await this.ensureConnected();
try {
const fullPath = this.getFullPath(path);
await this.client.put(Buffer.from(content, 'utf-8'), fullPath);
logger.debug('SftpStorageProvider', `Wrote file: ${fullPath}`);
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to write file: ${error}`);
throw error;
}
}
async listFiles(path) {
try {
const list = await this.client.list(path);
return list.map((item) => item.name);
}
catch (error) {
console.error(`Failed to list files in: ${path}`, error);
throw error;
}
}
async getFileStats(path) {
try {
const stats = await this.client.stat(path);
return { mtimeMs: stats.modifyTime * 1000 };
}
catch (error) {
console.error(`Failed to get file stats for: ${path}`, error);
throw error;
}
}
async getStatus(path) {
await this.ensureConnected();
try {
const fullPath = this.getFullPath(path);
const files = await this.listFiles(fullPath);
const coreFiles = [
'product-context.md',
'active-context.md',
'progress.md',
'decision-log.md',
'system-patterns.md'
];
const missingCoreFiles = coreFiles.filter(file => !files.includes(file));
const coreFilesPresent = coreFiles.filter(file => files.includes(file));
let lastUpdated;
if (files.length > 0) {
const stats = await Promise.all(files.map(file => this.getFileStats(path + '/' + file)));
const latestMtime = Math.max(...stats.map(stat => stat.mtimeMs));
lastUpdated = new Date(latestMtime);
}
return {
path: fullPath,
files,
coreFilesPresent,
missingCoreFiles,
isComplete: missingCoreFiles.length === 0,
language: 'en',
lastUpdated
};
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to get status: ${error}`);
throw error;
}
}
async createBackup(sourcePath, backupPath) {
await this.ensureConnected();
try {
const fullSourcePath = this.getFullPath(sourcePath);
const fullBackupPath = this.getFullPath(backupPath);
// Create backup directory
await this.createDirectory(fullBackupPath);
// Copy all files from source to backup
const files = await this.listFiles(fullSourcePath);
for (const file of files) {
const sourceFile = `${fullSourcePath}/${file}`;
const backupFile = `${fullBackupPath}/${file}`;
await this.client.fastGet(sourceFile, backupFile);
}
logger.debug('SftpStorageProvider', `Created backup at: ${fullBackupPath}`);
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to create backup: ${error}`);
throw error;
}
}
getFullPath(path) {
return `${this.config.basePath}/${path}`.replace(/\/+/g, '/');
}
async disconnect() {
if (this.connected) {
try {
await this.client.end();
this.connected = false;
logger.debug('SftpStorageProvider', 'Disconnected from SFTP server');
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to disconnect from SFTP server: ${error}`);
throw error;
}
}
}
async ensureDirectoryExists(path) {
try {
const exists = await this.client.exists(path);
if (!exists) {
await this.client.mkdir(path, true);
}
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to ensure directory exists: ${error}`);
throw error;
}
}
async deleteFile(path) {
try {
await this.client.delete(path);
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to delete file: ${error}`);
throw error;
}
}
async initializeMemoryBank(config) {
const basePath = this.config.basePath;
await this.ensureDirectoryExists(basePath);
const files = [
'product-context.md',
'active-context.md',
'progress.md',
'decision-log.md',
'system-patterns.md'
];
for (const file of files) {
const filePath = join(basePath, file);
const exists = await this.client.exists(filePath);
if (!exists) {
await this.writeFile(filePath, '');
}
}
}
async findMemoryBank(path) {
try {
const exists = await this.client.exists(path);
return exists ? path : null;
}
catch (error) {
logger.error('SftpStorageProvider', `Failed to find memory bank at: ${error}`);
return null;
}
}
}
//# sourceMappingURL=SftpStorageProvider.js.map