# Workspace Event Tracking System for UI Interactions in Desktop Commander MCP

> Learn how Desktop Commander MCP tracks UI interactions with its event tracking system. Discover its lightweight pipeline, Zod validation, and asynchronous telemetry for a seamless user experience.

- Repository: [Eduard Ruzga/DesktopCommanderMCP](https://github.com/wonderwhy-er/DesktopCommanderMCP)
- Tags: internals
- Published: 2026-08-06

---

**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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/ui/shared/ui-event-tracker.ts) |
| **Server Handler** | Validates payloads against `TrackUiEventArgsSchema` and routes to telemetry | [`src/tools/schemas.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/schemas.ts) + [`src/handlers/history-handlers.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/handlers/history-handlers.ts) |
| **Telemetry Capture** | Posts sanitized data to the telemetry proxy (skipping when inside UI origin calls) | [`src/utils/capture.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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.

```typescript
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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/handlers/history-handlers.ts). Before processing, the payload undergoes strict validation against `TrackUiEventArgsSchema` defined in [`src/tools/schemas.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/tools/schemas.ts):

```typescript
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_preview` if unspecified)
- **Primitive-only parameters** via the `z.record()` union type

Upon validation, the handler forwards the sanitized data to the capture layer:

```typescript
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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/utils/capture.ts) is an alias for the generic `capture` utility. This layer implements two critical safeguards:

1. **Double-counting prevention**: The function checks `isInsideUiOriginCall()` and silently drops telemetry when the call originates from inside a UI widget, preventing duplicate event recording.
2. **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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/ui/file-preview/src/app.ts) instantiates a tracker to monitor document interactions:

```typescript
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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/ui/config-editor/src/app.ts) uses `baseParams` to inject common metadata across all events:

```typescript
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:

```typescript
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 **`createUiEventTracker`** factory in [`src/ui/shared/ui-event-tracker.ts`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/src/ui/shared/ui-event-tracker.ts) provides type-safe, non-blocking event dispatch with primitive-only parameter constraints.
- **Zod validation** via `TrackUiEventArgsSchema` enforces 80-character event name limits and sanitized payloads.
- The **`capture_ui_event`** function 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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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`](https://github.com/wonderwhy-er/DesktopCommanderMCP/blob/main/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.