ultimate-mcp-server
Version:
The definitive all-in-one Model Context Protocol server for AI-assisted coding across 30+ platforms
290 lines • 10.4 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import crypto from 'crypto';
import { Logger } from '../utils/logger.js';
import { getHomeDir, joinPath, getPlatformInfo } from '../utils/platform-utils.js';
export class SessionStorage {
logger;
sessionDir;
currentSession = null;
sessionFile;
autoSaveInterval = null;
isDirty = false;
constructor(sessionId) {
this.logger = new Logger('SessionStorage');
// Determine session directory (similar to Claude Code's approach)
const baseDir = process.env.MCP_SESSION_DIR ||
joinPath(getHomeDir(), '.ultimate-mcp', 'sessions');
this.sessionDir = baseDir;
// Generate or use provided session ID
const id = sessionId || this.generateSessionId();
this.sessionFile = path.join(this.sessionDir, `${id}.json`);
}
async initialize() {
try {
// Ensure session directory exists
await fs.mkdir(this.sessionDir, { recursive: true });
// Try to load existing session
await this.loadSession();
// Start auto-save interval (every 30 seconds)
this.startAutoSave();
this.logger.info(`Session initialized: ${this.currentSession?.id}`);
}
catch (error) {
this.logger.error('Failed to initialize session storage:', error);
// Create new session if loading fails
await this.createNewSession();
}
}
generateSessionId() {
const timestamp = Date.now();
const random = crypto.randomBytes(8).toString('hex');
return `session-${timestamp}-${random}`;
}
async createNewSession() {
const sessionId = path.basename(this.sessionFile, '.json');
this.currentSession = {
id: sessionId,
created: new Date().toISOString(),
lastAccessed: new Date().toISOString(),
conversations: [],
metadata: {
...getPlatformInfo(),
nodeVersion: process.version,
mcpVersion: '2.0.0',
startTime: Date.now()
},
tools: [],
files: []
};
await this.saveSession();
}
async loadSession() {
try {
const data = await fs.readFile(this.sessionFile, 'utf-8');
this.currentSession = JSON.parse(data);
// Update last accessed time
if (this.currentSession) {
this.currentSession.lastAccessed = new Date().toISOString();
this.isDirty = true;
}
}
catch (error) {
// Session file doesn't exist or is corrupted
await this.createNewSession();
}
}
async saveSession() {
if (!this.currentSession)
return;
try {
const data = JSON.stringify(this.currentSession, null, 2);
await fs.writeFile(this.sessionFile, data, 'utf-8');
this.isDirty = false;
this.logger.debug('Session saved successfully');
}
catch (error) {
this.logger.error('Failed to save session:', error);
}
}
startAutoSave() {
this.autoSaveInterval = setInterval(async () => {
if (this.isDirty) {
await this.saveSession();
}
}, 30000); // Save every 30 seconds if there are changes
}
async addMessage(conversationId, message) {
if (!this.currentSession)
return;
// Find or create conversation
let conversation = this.currentSession.conversations.find(c => c.id === conversationId);
if (!conversation) {
conversation = {
id: conversationId,
created: new Date().toISOString(),
updated: new Date().toISOString(),
messages: [],
context: {},
tokens: 0
};
this.currentSession.conversations.push(conversation);
}
// Add message
conversation.messages.push(message);
conversation.updated = new Date().toISOString();
// Estimate tokens (rough calculation)
const messageTokens = Math.ceil(message.content.length / 4);
conversation.tokens += messageTokens;
// Keep conversation size manageable (max 100 messages)
if (conversation.messages.length > 100) {
// Keep first 10 and last 90 messages for context
conversation.messages = [
...conversation.messages.slice(0, 10),
{
role: 'system',
content: '[... earlier messages truncated for space ...]'
},
...conversation.messages.slice(-89)
];
}
this.isDirty = true;
}
async addToolUsage(tool, input, output, duration) {
if (!this.currentSession)
return;
this.currentSession.tools.push({
tool,
timestamp: new Date().toISOString(),
input,
output,
duration
});
// Keep only last 500 tool usages
if (this.currentSession.tools.length > 500) {
this.currentSession.tools = this.currentSession.tools.slice(-500);
}
this.isDirty = true;
}
async addFileAccess(filePath, operation, content) {
if (!this.currentSession)
return;
this.currentSession.files.push({
path: filePath,
operation,
timestamp: new Date().toISOString(),
content: content ? content.substring(0, 1000) : undefined // Store first 1000 chars
});
// Keep only last 200 file accesses
if (this.currentSession.files.length > 200) {
this.currentSession.files = this.currentSession.files.slice(-200);
}
this.isDirty = true;
}
async getConversationHistory(conversationId, limit = 50) {
if (!this.currentSession)
return [];
const conversation = this.currentSession.conversations.find(c => c.id === conversationId);
if (!conversation)
return [];
return conversation.messages.slice(-limit);
}
async getAllConversations() {
if (!this.currentSession)
return [];
return this.currentSession.conversations;
}
async getRecentTools(limit = 20) {
if (!this.currentSession)
return [];
return this.currentSession.tools.slice(-limit);
}
async getRecentFiles(limit = 20) {
if (!this.currentSession)
return [];
return this.currentSession.files.slice(-limit);
}
async searchMessages(query, limit = 20) {
if (!this.currentSession)
return [];
const results = [];
const lowerQuery = query.toLowerCase();
for (const conversation of this.currentSession.conversations) {
for (const message of conversation.messages) {
if (message.content.toLowerCase().includes(lowerQuery)) {
results.push(message);
if (results.length >= limit)
return results;
}
}
}
return results;
}
async getSessionMetadata() {
if (!this.currentSession)
return {};
return {
...this.currentSession.metadata,
sessionId: this.currentSession.id,
created: this.currentSession.created,
lastAccessed: this.currentSession.lastAccessed,
conversationCount: this.currentSession.conversations.length,
totalMessages: this.currentSession.conversations.reduce((sum, c) => sum + c.messages.length, 0),
totalTokens: this.currentSession.conversations.reduce((sum, c) => sum + c.tokens, 0),
toolUsageCount: this.currentSession.tools.length,
fileAccessCount: this.currentSession.files.length
};
}
async listSessions() {
try {
const files = await fs.readdir(this.sessionDir);
return files
.filter(f => f.endsWith('.json'))
.map(f => path.basename(f, '.json'));
}
catch (error) {
return [];
}
}
async switchSession(sessionId) {
// Save current session
await this.saveSession();
// Switch to new session
this.sessionFile = path.join(this.sessionDir, `${sessionId}.json`);
await this.loadSession();
}
async deleteSession(sessionId) {
const targetId = sessionId || this.currentSession?.id;
if (!targetId)
return;
const targetFile = path.join(this.sessionDir, `${targetId}.json`);
try {
await fs.unlink(targetFile);
// If deleting current session, create a new one
if (targetId === this.currentSession?.id) {
await this.createNewSession();
}
}
catch (error) {
this.logger.error('Failed to delete session:', error);
}
}
async exportSession() {
if (!this.currentSession)
return '{}';
return JSON.stringify(this.currentSession, null, 2);
}
async importSession(data) {
try {
const sessionData = JSON.parse(data);
// Validate session data structure
if (!sessionData.id || !sessionData.conversations) {
throw new Error('Invalid session data format');
}
this.currentSession = sessionData;
await this.saveSession();
}
catch (error) {
this.logger.error('Failed to import session:', error);
throw error;
}
}
async cleanup() {
// Stop auto-save
if (this.autoSaveInterval) {
clearInterval(this.autoSaveInterval);
this.autoSaveInterval = null;
}
// Save final state
await this.saveSession();
}
// Get current session ID
getCurrentSessionId() {
return this.currentSession?.id || null;
}
// Check if session has unsaved changes
hasUnsavedChanges() {
return this.isDirty;
}
}
//# sourceMappingURL=session-storage.js.map