A motion sensor for AI agents: MCP server that reports when a screen region changes or goes still. Scores only, never pixels.
pir-mcp demonstrates solid tool design with consistent naming (verb_noun pattern: pir_define_*, pir_pick_*, pir_sample, pir_wait_*), comprehensive parameter descriptions, and well-structured JSON schemas. All 10 tools have explicit descriptions (avg ~150 chars) and typed input schemas with constraints (min/max, enums, array limits). Error handling uses structured envelopes. However, output schemas are not documented, callers cannot predict response structure. Some parameter descriptions are dense and could be more LLM-optimized. Tool composition is clean (single responsibility), but the server lacks tool annotations (readOnlyHint/destructiveHint) to signal intent.
Name a rectangle of the screen to watch. Coordinates are in the global layout the capture tool uses (on a multi-monitor wlroots layout an output left of the primary has negative x). Masks are rects inside the region, in region-local pixels, that are ignored: put a clock or spinner there. Redefining a name replaces it and forgets its last sample.
Watch a window rather than a fixed rectangle: geometry is looked up from the window id before every capture, so the region follows the window when it moves. A resize counts as maximal change. Masks are window-local. The id is whatever your window-geometry command understands (a Hyprland address like 0x5f3a..., an X11 window id).
The regions currently defined, with their geometry and masks.
Ask the person to draw a rectangle inside an existing region that should be ignored (a clock, a spinner, a caret). Forgets the region's last sample, since masked grids are not comparable to unmasked ones.
Ask the person at the screen to draw the rectangle to watch (a selector such as slurp appears; Escape cancels). Prefer this over pir_define_region whenever a human is present: they choose what is sensed, and you get the geometry back. Blocks until they finish or the selector times out.
Output schemas not documented. Callers cannot predict response structure (e.g., what fields does pir_sample return? Is it {ok, result, error} or {changed, score, timestamp}?). LLMs must infer structure from trial, wasting tokens and risking misinterpretation.
Tool annotations missing. No readOnlyHint or destructiveHint declared. pir_define_region, pir_define_window, pir_pick_window, pir_pick_region, pir_pick_mask, pir_remove_region are WRITE operations; pir_sample, pir_wait_for_change, pir_wait_for_stillness are READ_ONLY. Annotations help LLMs reason about side effects and plan safely.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 63 | 2025-06-18+ | v2 |
Ask the person at the screen to click the window to watch (visible windows are offered as boxes; Escape cancels). The region then follows that window. Prefer this over pir_define_window when a human is present.
Forget a region.
Capture the region once and score it against the previous sample of the same region (null on the first call). Cheap and non-blocking: use it to check the capture command works, to take a baseline before doing something, or to ask 'did anything happen there since I last looked?'.
Block until the region looks different from how it looked when this call started, or until timeout_ms. Returns changed=true with the score, or changed=false with the largest score seen. Use it as a doorbell: 'wake me when the build output moves', 'when the dialog closes', 'when the game loads'.
Block until consecutive samples have stayed below threshold for still_for_ms, or until timeout_ms. A short window means 'the page/animation has settled, safe to act'. A long window on something that should keep changing (a clock, a progress bar, a status bar) means it is frozen: still=true is then the alarm.
Parameter descriptions are dense and technical. E.g., pir_wait_for_change threshold description is 180+ chars with embedded metric details. Should split: threshold description (20-50 chars) + metric description (separate param). Reduces cognitive load for LLM selection.
Error handling uses generic envelope {ok, result, error}. No error categorization (retryable vs user-fixable vs fatal). When pir_sample fails, LLM cannot determine if it should retry, ask user, or abort. Add error codes or recovery hints.
pir_list_regions description is minimal (18 chars: 'The regions currently defined, with their geometry and masks.'). Does not explain when to call it or what structure it returns. Should be 50-150 chars with context.