eyes-mcp
# eyes-mcp
Temporary visual eyes for agents on Hyprland/Wayland. Lets Claude (or any MCP client) capture monitors, windows, or arbitrary regions and **see** the result as a PNG — closing the QA loop without relying on Playwright or screen-share heuristics.
Captures live in a per-process tempdir (`/tmp/eyes-mcp-{pid}/`) and self-destruct after a TTL (default 60s). Each capture tool returns metadata with a `path`; read that file to view the image. Capturing and looking are separate steps on purpose — you can take several captures and only spend tokens on the ones worth inspecting — but the TTL is running, so read what you need promptly.
## Why
Agents writing UI fixes, desktop scripts, theming tweaks, notebook plots, or anything that produces a visual artifact normally have no way to verify the result. They commit blind, declare success, and the human catches the regression. `eyes-mcp` gives them a quick, scoped peek so they can self-correct before claiming "done".
## Requirements
- Hyprland (Wayland) — uses `hyprctl` for window/monitor enumeration
- `grim` — Wayland screenshot backend
- Python 3.11+
- Pillow (image resize)
- `uv` — required by the plugin install path (see below)
```bash
# Arch
sudo pacman -S grim hyprland
```
## Install
```bash
cd ~/Projects/mcp/eyes-mcp
pip install -e .
```
## Install as plugin
```bash
claude plugin install eyes --marketplace Rixmerz/claude-plugins
```
Pulls the whole repo via `source: github`, launches the server with `uv run
--project ${CLAUDE_PLUGIN_ROOT} eyes-mcp` — resolves deps from
`pyproject.toml` on first run, no `pip install -e .` needed.
**First-launch timeout risk:** on a cold `uv` cache, resolving and installing
all deps (fastmcp, pillow, pydantic, transitively ~70 packages) has measured
up to ~90s before the server reports ready. Claude Code's default MCP startup
timeout is 30s, so the very first launch after `claude plugin install eyes`
can time out. Work around it by warming the cache once before registering the
plugin:
```bash
uv sync --project ~/.claude/plugins/cache/rixmerz/eyes/0.1.1
```
The cache path uses the **plugin** name (`eyes`), not the repo name — adjust
the version to match what you installed. Or run `uv sync --project <path>`
against a manual checkout. Subsequent launches use the warmed `uv` cache and
start well under the timeout.
## Register with Claude Code
```bash
claude mcp add eyes -- python -m eyes_mcp
```
Or in `~/.config/claude/mcp.json` (or equivalent):
```json
{
"mcpServers": {
"eyes": {
"command": "python",
"args": ["-m", "eyes_mcp"],
"env": {
"EYES_TTL_SECONDS": "60",
"EYES_MAX_SIDE_PX": "1920"
}
}
}
}
```
## Tools
| Tool | Purpose |
| --- | --- |
| `capture_monitor(name?)` | Full monitor screenshot. Defaults to focused monitor. |
| `capture_active_window()` | Currently focused window. |
| `capture_window(query)` | Window matching class/title substring (largest match wins). |
| `capture_region(x, y, w, h)` | Arbitrary rectangle in global compositor coords. |
| `list_monitors()` | Enumerate monitors (name, geometry, focused). |
| `list_windows()` | Enumerate open windows (class, title, geometry). |
| `list_captures()` | Captures still alive in current session. |
| `cleanup_captures()` | Wipe all session captures now. |
Each capture tool returns a JSON metadata blob (id, path, dims, bytes, ttl). Read the file at `path` to view the PNG.
`capture_window` fails rather than guessing when the target window is on a workspace that is not currently displayed: `grim` reads the composited output, so capturing a hidden window would silently return the visible workspace's pixels under the requested window's name. It also cannot see through an occluding window stacked on top of the target — what you get is what is on screen.
## Config
| Env var | Default | Meaning |
| --- | --- | --- |
| `EYES_TTL_SECONDS` | `60` | Seconds before a capture is swept from disk. |
| `EYES_MAX_SIDE_PX` | `1920` | Longest side after downscale (token-cost guard). |
## Lifecycle
- Tempdir created on startup: `/tmp/eyes-mcp-{pid}/`
- Background sweeper deletes expired files every `TTL/4` seconds
- `atexit` wipes the whole tempdir on clean shutdown
- All captures are PNG; downscaled in-place via Pillow LANCZOS
## Limitations
- Wayland-only (relies on `grim` + `hyprctl`). No X11, macOS, Windows.
- No webcam capture in v1 (intentional scope cut).
- Region selection is non-interactive — agents can't drive `slurp`. Use `capture_window` or pass coords directly.
- Multi-monitor coords are global (Hyprland's compositor space).
## License
MIT
TDQS
Scored across 8 tools
The four capture tools target distinct scopes (monitor, active window, matched window, arbitrary region), and the list tools cover distinct resources (windows, monitors, captures). capture_active_window vs capture_window could be briefly confused since both capture a window, but descriptions clarify the difference (focused vs class/title match).
All names follow a clean verb_noun snake_case pattern: capture_* for screenshots, list_* for enumeration, cleanup_* for deletion. The convention is applied uniformly with no deviations.
Eight tools is well-scoped for a screen-capture utility, with each tool earning its place across capture targets, discovery, and session cleanup. No redundant or filler tools.
The surface covers capture across monitor/window/region plus discovery (list_windows, list_monitors) and lifecycle management (list_captures, cleanup_captures), which is strong. Minor gaps like delayed/timed capture or annotation are absent but not essential to the core workflow.