@venly/wallet-mcp
Version:
Production-ready MCP server enabling AI agents to perform Web3 wallet operations through Venly's blockchain infrastructure. Supports 14+ networks including Ethereum, Polygon, Arbitrum, and stablecoin operations.
223 lines • 8.71 kB
JavaScript
/**
* Persistent Workflow Storage
* Stores workflow templates and executions to survive container restarts
*/
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import winston from 'winston';
export class WorkflowStorage {
storageDir;
templatesFile;
executionsFile;
logger;
constructor(storageDir = '/app/data') {
this.storageDir = storageDir;
this.templatesFile = path.join(storageDir, 'workflow-templates.json');
this.executionsFile = path.join(storageDir, 'workflow-executions.json');
this.logger = winston.createLogger({
level: 'info',
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
defaultMeta: { service: 'workflow-storage' },
transports: [
new winston.transports.Console({
format: winston.format.simple()
})
]
});
// Initialize storage synchronously
this.initializeStorageSync();
}
initializeStorageSync() {
try {
this.logger.info('Initializing workflow storage', {
storageDir: this.storageDir,
templatesFile: this.templatesFile,
executionsFile: this.executionsFile,
processUid: process.getuid ? process.getuid() : 'unknown',
processGid: process.getgid ? process.getgid() : 'unknown'
});
// Create storage directory if it doesn't exist
if (!fsSync.existsSync(this.storageDir)) {
this.logger.info('Creating storage directory', { storageDir: this.storageDir });
fsSync.mkdirSync(this.storageDir, { recursive: true });
}
else {
this.logger.info('Storage directory already exists', { storageDir: this.storageDir });
}
// Check directory permissions
try {
fsSync.accessSync(this.storageDir, fsSync.constants.R_OK | fsSync.constants.W_OK);
this.logger.info('Storage directory is readable and writable');
}
catch (permError) {
this.logger.error('Storage directory permission issue', {
storageDir: this.storageDir,
error: permError
});
}
// Initialize templates file if it doesn't exist
if (!fsSync.existsSync(this.templatesFile)) {
this.logger.info('Creating templates file', { templatesFile: this.templatesFile });
fsSync.writeFileSync(this.templatesFile, JSON.stringify({}));
}
else {
this.logger.info('Templates file already exists', { templatesFile: this.templatesFile });
}
// Initialize executions file if it doesn't exist
if (!fsSync.existsSync(this.executionsFile)) {
this.logger.info('Creating executions file', { executionsFile: this.executionsFile });
fsSync.writeFileSync(this.executionsFile, JSON.stringify({}));
}
else {
this.logger.info('Executions file already exists', { executionsFile: this.executionsFile });
}
this.logger.info('Workflow storage initialized successfully', {
storageDir: this.storageDir,
templatesFile: this.templatesFile,
executionsFile: this.executionsFile
});
}
catch (error) {
this.logger.error('Failed to initialize workflow storage', {
error: error instanceof Error ? {
message: error.message,
stack: error.stack,
code: error.code,
errno: error.errno,
path: error.path
} : error,
storageDir: this.storageDir,
templatesFile: this.templatesFile,
executionsFile: this.executionsFile
});
// Don't throw the error, just log it and continue with in-memory storage
this.logger.warn('Continuing with in-memory storage only due to initialization failure');
}
}
// Template storage methods
async storeTemplate(template) {
try {
const templates = await this.loadTemplates();
templates[template.templateId] = template;
await fs.writeFile(this.templatesFile, JSON.stringify(templates, null, 2));
this.logger.info('Template stored successfully', {
templateId: template.templateId,
name: template.name
});
}
catch (error) {
this.logger.error('Failed to store template', {
templateId: template.templateId,
error
});
throw error;
}
}
async getTemplate(templateId) {
try {
const templates = await this.loadTemplates();
return templates[templateId];
}
catch (error) {
this.logger.error('Failed to get template', { templateId, error });
return undefined;
}
}
async getAllTemplates() {
try {
return await this.loadTemplates();
}
catch (error) {
this.logger.error('Failed to get all templates', { error });
return {};
}
}
async deleteTemplate(templateId) {
try {
const templates = await this.loadTemplates();
delete templates[templateId];
await fs.writeFile(this.templatesFile, JSON.stringify(templates, null, 2));
this.logger.info('Template deleted successfully', { templateId });
}
catch (error) {
this.logger.error('Failed to delete template', { templateId, error });
throw error;
}
}
async loadTemplates() {
try {
const data = await fs.readFile(this.templatesFile, 'utf-8');
return JSON.parse(data);
}
catch (error) {
this.logger.warn('Failed to load templates, returning empty object', { error });
return {};
}
}
// Execution storage methods
async storeExecution(execution) {
try {
const executions = await this.loadExecutions();
executions[execution.executionId] = execution;
await fs.writeFile(this.executionsFile, JSON.stringify(executions, null, 2));
this.logger.debug('Execution stored successfully', {
executionId: execution.executionId,
status: execution.status
});
}
catch (error) {
this.logger.error('Failed to store execution', {
executionId: execution.executionId,
error
});
throw error;
}
}
async getExecution(executionId) {
try {
const executions = await this.loadExecutions();
return executions[executionId];
}
catch (error) {
this.logger.error('Failed to get execution', { executionId, error });
return undefined;
}
}
async updateExecution(_executionId, execution) {
await this.storeExecution(execution);
}
async loadExecutions() {
try {
const data = await fs.readFile(this.executionsFile, 'utf-8');
return JSON.parse(data);
}
catch (error) {
this.logger.warn('Failed to load executions, returning empty object', { error });
return {};
}
}
// Cleanup old executions (optional)
async cleanupOldExecutions(maxAge = 7 * 24 * 60 * 60 * 1000) {
try {
const executions = await this.loadExecutions();
const cutoffTime = Date.now() - maxAge;
let cleaned = 0;
for (const [executionId, execution] of Object.entries(executions)) {
const executionTime = new Date(execution.startedAt).getTime();
if (executionTime < cutoffTime) {
delete executions[executionId];
cleaned++;
}
}
if (cleaned > 0) {
await fs.writeFile(this.executionsFile, JSON.stringify(executions, null, 2));
this.logger.info('Cleaned up old executions', { cleaned });
}
}
catch (error) {
this.logger.error('Failed to cleanup old executions', { error });
}
}
}
//# sourceMappingURL=WorkflowStorage.js.map