Skip to main content
Glama

Claude Desktop Commander MCP

toolHistory.ts6.86 kB
import { ServerResult } from '../types.js'; import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; export interface ToolCallRecord { timestamp: string; toolName: string; arguments: any; output: ServerResult; duration?: number; } interface FormattedToolCallRecord extends Omit<ToolCallRecord, 'timestamp'> { timestamp: string; // formatted local time string } // Format timestamp in local timezone for display function formatLocalTimestamp(isoTimestamp: string): string { const date = new Date(isoTimestamp); return date.toLocaleString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }); } class ToolHistory { private history: ToolCallRecord[] = []; private readonly MAX_ENTRIES = 1000; private readonly historyFile: string; private writeQueue: ToolCallRecord[] = []; private isWriting = false; private writeInterval?: NodeJS.Timeout; constructor() { // Store history in same directory as config to keep everything together const historyDir = path.join(os.homedir(), '.claude-server-commander'); // Ensure directory exists if (!fs.existsSync(historyDir)) { fs.mkdirSync(historyDir, { recursive: true }); } // Use append-only JSONL format (JSON Lines) this.historyFile = path.join(historyDir, 'tool-history.jsonl'); // Load existing history on startup this.loadFromDisk(); // Start async write processor this.startWriteProcessor(); } /** * Load history from disk (all instances share the same file) */ private loadFromDisk(): void { try { if (!fs.existsSync(this.historyFile)) { console.error('[ToolHistory] No history file found, starting fresh'); return; } const content = fs.readFileSync(this.historyFile, 'utf-8'); const lines = content.trim().split('\n').filter(line => line.trim()); // Parse each line as JSON const records: ToolCallRecord[] = []; for (const line of lines) { try { records.push(JSON.parse(line)); } catch (e) { console.error('[ToolHistory] Failed to parse line:', line); } } // Keep only last 1000 entries this.history = records.slice(-this.MAX_ENTRIES); console.error(`[ToolHistory] Loaded ${this.history.length} entries from disk`); // If file is getting too large, trim it if (lines.length > this.MAX_ENTRIES * 2) { this.trimHistoryFile(); } } catch (error) { console.error('[ToolHistory] Failed to load history:', error); } } /** * Trim history file to prevent it from growing indefinitely */ private trimHistoryFile(): void { try { console.error('[ToolHistory] Trimming history file...'); // Keep last 1000 entries in memory const keepEntries = this.history.slice(-this.MAX_ENTRIES); // Write them back const lines = keepEntries.map(entry => JSON.stringify(entry)).join('\n') + '\n'; fs.writeFileSync(this.historyFile, lines, 'utf-8'); console.error(`[ToolHistory] Trimmed to ${keepEntries.length} entries`); } catch (error) { console.error('[ToolHistory] Failed to trim history file:', error); } } /** * Async write processor - batches writes to avoid blocking */ private startWriteProcessor(): void { this.writeInterval = setInterval(() => { if (this.writeQueue.length > 0 && !this.isWriting) { this.flushToDisk(); } }, 1000); // Flush every second // Prevent interval from keeping process alive during shutdown/tests this.writeInterval.unref(); } /** * Flush queued writes to disk */ private async flushToDisk(): Promise<void> { if (this.isWriting || this.writeQueue.length === 0) return; this.isWriting = true; const toWrite = [...this.writeQueue]; this.writeQueue = []; try { // Append to file (atomic append operation) const lines = toWrite.map(entry => JSON.stringify(entry)).join('\n') + '\n'; fs.appendFileSync(this.historyFile, lines, 'utf-8'); } catch (error) { console.error('[ToolHistory] Failed to write to disk:', error); // Put back in queue on failure this.writeQueue.unshift(...toWrite); } finally { this.isWriting = false; } } /** * Add a tool call to history */ addCall( toolName: string, args: any, output: ServerResult, duration?: number ): void { const record: ToolCallRecord = { timestamp: new Date().toISOString(), toolName, arguments: args, output, duration }; this.history.push(record); // Keep only last 1000 in memory if (this.history.length > this.MAX_ENTRIES) { this.history.shift(); } // Queue for async write this.writeQueue.push(record); } /** * Get recent tool calls with filters */ getRecentCalls(options: { maxResults?: number; toolName?: string; since?: string; }): ToolCallRecord[] { let results = [...this.history]; // Filter by tool name if (options.toolName) { results = results.filter(r => r.toolName === options.toolName); } // Filter by timestamp if (options.since) { const sinceDate = new Date(options.since); results = results.filter(r => new Date(r.timestamp) >= sinceDate); } // Limit results (default 50, max 1000) const limit = Math.min(options.maxResults || 50, 1000); return results.slice(-limit); } /** * Get recent calls formatted with local timezone */ getRecentCallsFormatted(options: { maxResults?: number; toolName?: string; since?: string; }): FormattedToolCallRecord[] { const calls = this.getRecentCalls(options); // Format timestamps to local timezone return calls.map(call => ({ ...call, timestamp: formatLocalTimestamp(call.timestamp) })); } /** * Get current stats */ getStats() { return { totalEntries: this.history.length, oldestEntry: this.history[0]?.timestamp, newestEntry: this.history[this.history.length - 1]?.timestamp, historyFile: this.historyFile, queuedWrites: this.writeQueue.length }; } /** * Cleanup method - clears interval and flushes pending writes * Call this during shutdown or in tests */ async cleanup(): Promise<void> { // Clear the interval if (this.writeInterval) { clearInterval(this.writeInterval); this.writeInterval = undefined; } // Flush any remaining writes if (this.writeQueue.length > 0) { await this.flushToDisk(); } } } export const toolHistory = new ToolHistory();

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/wonderwhy-er/DesktopCommanderMCP'

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