How DesktopCommanderMCP Tool History Tracking Preserves Recent Tool Calls for Debugging
DesktopCommanderMCP captures every tool invocation in a bounded in-memory buffer and asynchronously persists records to a JSON Lines file, ensuring recent tool calls remain available for real-time debugging and post-mortem analysis without blocking the main thread.
DesktopCommanderMCP is a Model Context Protocol server that exposes desktop automation tools to AI assistants. When troubleshooting tool failures or analyzing execution patterns, developers rely on the tool history tracking system implemented in src/utils/toolHistory.ts. This subsystem balances immediate accessibility with data durability by maintaining a volatile ring buffer alongside append-only disk storage with automatic size management.
In-Memory Ring Buffer for Fast Access
The ToolHistory class maintains a private history array that serves as the primary data structure for recent invocations. As implemented in src/utils/toolHistory.ts, this buffer enforces a hard limit of 1,000 entries defined by the MAX_ENTRIES constant. When the buffer reaches capacity, older records shift out automatically, preventing unbounded memory growth while preserving the most recent activity.
Each entry follows the ToolCallRecord interface, capturing the tool name, arguments, result, timestamp, and optional duration. This design ensures that debugging queries against recent activity execute entirely in memory without filesystem I/O, providing microsecond-level access to the last thousand operations.
Persistent JSON Lines Storage
Beyond volatile memory, the constructor initializes persistent storage inside ~/.claude-server-commander/tool-history.jsonl. Upon instantiation, the class loads existing records from this append-only file into the in-memory buffer, ensuring continuity across process restarts.
The storage location is determined at construction and defaults to the user's home directory under the .claude-server-commander folder. While this file grows continuously during operation, the system includes safeguards against unbounded expansion that could consume excessive disk space.
Asynchronous Write Processing
The addCall() method handles new invocations by simultaneously pushing records to the in-memory buffer and queuing them in writeQueue for disk persistence. To prevent blocking the main thread during tool execution, startWriteProcessor() initiates a setInterval timer that triggers flushToDisk() every second.
This batched approach amortizes I/O costs across multiple tool calls. The flush operation writes pending entries to tool-history.jsonl in chronological order before clearing the queue, ensuring durability without synchronous file system penalties that could slow down interactive tools.
Automatic File Size Protection
Before each flush cycle, trimHistoryFileIfTooLarge() checks the on-disk file size against a 5 MiB cap. If the log exceeds this limit, the system rewrites the file retaining only the most recent tail (approximately 4 MiB). This rotation mechanism guarantees that log files never consume excessive disk space while preserving sufficient history for debugging purposes.
This protection runs automatically during the flush operation, requiring no manual intervention from developers or users.
Query APIs for Debugging
The class exposes two primary methods for retrieving tool history without parsing files manually:
-
getRecentCalls(): Returns the last N records (default 50, maximum 1,000) directly from the in-memory buffer. It supports optional filtering bytoolNameor start timestamp, enabling targeted debugging of specific tools or time ranges. -
getRecentCallsFormatted(): Builds on the above method, converting ISO timestamps to human-readable local time strings viaformatLocalTimestamp, improving readability in debug consoles and UI panels.
Additional utilities include getStats(), which exposes buffer size, oldest/newest timestamps, file path, and pending write queue length, and cleanup(), which gracefully flushes pending writes and stops the interval processor during shutdown.
Implementation Example
Tool implementations throughout the codebase integrate history tracking by importing the singleton instance and invoking addCall():
import { toolHistory } from '@/utils/toolHistory';
// Record a tool invocation
toolHistory.addCall(
'search',
{ query: 'error handling' },
{ success: true, result: [] },
123 // duration in milliseconds
);
// Retrieve recent calls for debugging
const recent = toolHistory.getRecentCalls({ maxResults: 10 });
console.log(recent);
// Get human-readable formatted output
const formatted = toolHistory.getRecentCallsFormatted({ maxResults: 5 });
formatted.forEach(c => console.log(`${c.timestamp} – ${c.toolName}`));
// Check system health
console.log(toolHistory.getStats());
// Graceful shutdown
await toolHistory.cleanup();
Summary
- Bounded memory usage: The 1,000-entry ring buffer prevents memory leaks while retaining sufficient recent history.
- Durable persistence: JSON Lines storage in
~/.claude-server-commander/tool-history.jsonlsurvives process restarts. - Non-blocking I/O: Asynchronous flush operations every second ensure tool execution remains responsive.
- Automatic maintenance: 5 MiB file size cap with tail preservation prevents disk space exhaustion.
- Flexible querying: Filter by tool name or timestamp, with optional human-readable formatting.
Frequently Asked Questions
How does the system prevent the tool history from consuming unlimited memory?
The ToolHistory class enforces a strict MAX_ENTRIES limit of 1,000 records in the private history array. When new calls arrive at capacity, the array shifts out the oldest entries, maintaining constant memory usage regardless of total session duration or total tool invocations.
Where are tool call logs stored, and how long do they persist?
Records persist to tool-history.jsonl inside the ~/.claude-server-commander directory. This file survives application restarts and is loaded into memory on class instantiation, though it is subject to automatic trimming when exceeding 5 MiB to prevent unbounded growth.
Can I filter tool history by specific tool names or time ranges?
Yes. The getRecentCalls() method accepts optional toolName and startTimestamp parameters. These filters apply to the in-memory buffer, allowing developers to isolate specific tool invocations or investigate activity within defined time windows without scanning the entire log file.
What happens to queued writes if the process shuts down unexpectedly?
The cleanup() method should be called during graceful shutdowns to flush the writeQueue to disk. If the process terminates abruptly, any unflushed records remaining in the queue are lost, though entries already written to the JSON Lines file persist. Critical tool implementations may wish to await flushToDisk() explicitly after important calls if immediate durability is required.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →