Workspace Event Tracking System for UI Interactions in Desktop Commander MCP
Desktop Commander MCP implements a lightweight, three-layer event tracking pipeline that captures UI interactions via the track_ui_event tool, validating payloads with Zod schemas and forwarding them asynchronously to a telemetry endpoint without blocking the user interface.
The Desktop Commander MCP server provides a sophisticated workspace event tracking system for UI interactions that decouples analytics instrumentation from business logic. This open-source Model Context Protocol implementation records user actions through a client-side tracker that forwards normalized events to a server-side handler, ensuring type safety via Zod schemas while maintaining UI responsiveness through fire-and-forget telemetry delivery.
Three-Layer Architecture Overview
The event tracking system organizes data collection into three distinct layers:
| Layer | Responsibility | Key File |
|---|---|---|
| UI Tracker | Normalizes parameters and dispatches track_ui_event calls |
src/ui/shared/ui-event-tracker.ts |
| Server Handler | Validates payloads against TrackUiEventArgsSchema and routes to telemetry |
src/tools/schemas.ts + src/handlers/history-handlers.ts |
| Telemetry Capture | Posts sanitized data to the telemetry proxy (skipping when inside UI origin calls) | src/utils/capture.ts |
UI-Side Event Tracking with createUiEventTracker
The UI layer provides a factory function called createUiEventTracker in src/ui/shared/ui-event-tracker.ts. This helper constructs tracker instances bound to specific UI components, ensuring consistent event structure before dispatching to the server.
export function createUiEventTracker(
callTool: (name: string, args: Record<string, unknown>) => Promise<unknown>,
options: { component: string; baseParams?: UiEventParams }
) {
const baseParams = options.baseParams ?? {};
return (event: string, params: Record<string, unknown> = {}): void => {
void callTool('track_ui_event', {
event,
component: options.component,
params: { ...baseParams, ...normalizeUiEventParams(params) },
}).catch(() => {
// UI analytics must never block the UI.
});
};
}
The returned function accepts an event name and a parameters object, merges them with baseParams defined at creation, and invokes the generic track_ui_event tool through the supplied callTool function (typically app.callServerTool or bridge.callTool). The void operator and .catch() handler ensure that network failures or latency never freeze the interface.
Parameter Normalization and Safety
The tracker enforces a strict primitive-only policy through normalizeUiEventParams. Valid parameter values are limited to strings, numbers, booleans, and null. This constraint prevents accidental leakage of complex objects or sensitive data into telemetry streams while keeping payload sizes predictable.
Server-Side Validation and Dispatch
When the UI invokes track_ui_event, the server routes the call to handleTrackUiEvent in src/handlers/history-handlers.ts. Before processing, the payload undergoes strict validation against TrackUiEventArgsSchema defined in src/tools/schemas.ts:
export const TrackUiEventArgsSchema = z.object({
event: z.string().min(1).max(80),
component: z.string().optional().default('file_preview'),
params: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()]))
.optional()
.default({}),
});
The schema enforces:
- Event names between 1 and 80 characters
- Component identifiers (defaults to
file_previewif unspecified) - Primitive-only parameters via the
z.record()union type
Upon validation, the handler forwards the sanitized data to the capture layer:
await capture_ui_event(
'mcp_ui_event',
buildTrackUiEventCapturePayload(parsed.event, parsed.component, parsed.params)
);
Asynchronous Telemetry Capture
The capture_ui_event function in src/utils/capture.ts is an alias for the generic capture utility. This layer implements two critical safeguards:
- Double-counting prevention: The function checks
isInsideUiOriginCall()and silently drops telemetry when the call originates from inside a UI widget, preventing duplicate event recording. - Fire-and-forget delivery: Events are sent via HTTP POST to the telemetry proxy asynchronously, with errors suppressed to guarantee UI responsiveness.
Because the capture mechanism is non-blocking, users experience zero latency from analytics instrumentation regardless of network conditions.
Implementation Examples
File Preview Widget
The file preview UI component in src/ui/file-preview/src/app.ts instantiates a tracker to monitor document interactions:
const filePreviewUiEvent = createUiEventTracker(
(name, args) => app.callServerTool({ name, arguments: args }),
{ component: 'file_preview' }
);
trackUiEvent = (event, params = {}) => filePreviewUiEvent(event, {
tool_name: getTelemetryToolName(currentPayload ?? hostPayload),
...params,
});
Configuration Editor
The config editor in src/ui/config-editor/src/app.ts uses baseParams to inject common metadata across all events:
const trackConfigUiEvent = createUiEventTracker(
(name, args) => bridge.callTool(name, args),
{
component: 'config_editor',
baseParams: { origin: 'ui' },
}
);
// Usage
trackConfigUiEvent('expand', { tool_name: 'get_config', expanded: true });
Custom Widget Implementation
Any new UI widget can implement tracking by importing the factory:
const myTracker = createUiEventTracker(
(name, args) => bridge.callTool(name, args),
{ component: 'my_custom_widget' }
);
myTracker('button_clicked', { button_id: 'save', successful: true });
Summary
- Desktop Commander MCP tracks UI interactions through a decoupled three-layer pipeline: UI tracker, server validator, and telemetry capture.
- The
createUiEventTrackerfactory insrc/ui/shared/ui-event-tracker.tsprovides type-safe, non-blocking event dispatch with primitive-only parameter constraints. - Zod validation via
TrackUiEventArgsSchemaenforces 80-character event name limits and sanitized payloads. - The
capture_ui_eventfunction prevents double-counting via origin checks and guarantees asynchronous delivery without UI blocking. - Real-world usage spans the file preview and config editor widgets, demonstrating extensible instrumentation patterns.
Frequently Asked Questions
What is the maximum length for event names in the tracking system?
According to the TrackUiEventArgsSchema in src/tools/schemas.ts, event names must be between 1 and 80 characters (z.string().min(1).max(80)). This limit prevents payload bloat while ensuring descriptive event identifiers.
How does Desktop Commander MCP prevent telemetry calls from blocking the UI?
The UI tracker uses the void operator to discard the promise returned by callTool, chains a .catch() handler to swallow errors, and implements fire-and-forget delivery in the capture layer. These patterns ensure network latency or failures never interrupt user interactions.
Why are only primitive types allowed in event parameters?
The params field in TrackUiEventArgsSchema accepts only z.union([z.string(), z.number(), z.boolean(), z.null()]). This restriction prevents accidental serialization of complex objects that might contain secrets, reduces payload size, and ensures compatibility with downstream telemetry systems that expect flat key-value structures.
How does the system avoid double-counting UI widget interactions?
The capture_ui_event function in src/utils/capture.ts checks isInsideUiOriginCall() before transmitting data. When this check returns true—indicating the call originated from within a UI widget context—the function silently drops the event, preventing duplicate records when the server handles both the initial tool call and the subsequent UI event.
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 →