{"id":"3afd6573-cacd-4959-94ca-82961896c6ac","slug":"cheap-eyes","kind":"mcp","name":"cheap-eyes","summary":"Cheap eyes for an expensive agent: an MCP server that delegates bulk reading to cheap OpenRouter models and mechanically checks every quote and line reference against the source.","owner":"st412m","repoUrl":"https://github.com/st412m/cheap-eyes","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-09-26T12:43:52.000Z","addedAt":"2026-10-14T06:05:12.362Z","logoUrl":"https://avatars.githubusercontent.com/u/78678011?u=c2bc87337823530fbde2b7f457c9029a1da79cef&v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["files-documents","cloud-infra","memory-knowledge"],"tags":["claude","llm","openrouter","ai-agents","document-extraction","home-assistant-addon","pdf"],"runtime":"node","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":true,"reviewed":false,"syncedAt":"2026-10-17T19:01:50.652Z","sample":false,"url":"https://conduits.dev/tools/cheap-eyes","language":"JavaScript","transports":["stdio"],"install":{"npm":"cheap-eyes"},"requirements":{"env":[{"name":"CHEAP_EYES_OPENROUTER_KEY","secret":true,"required":true,"description":"OpenRouter API key (give it its own daily limit on openrouter.ai)"},{"name":"CHEAP_EYES_CONFIG","secret":false,"required":false,"description":"Path of the JSON config file (read roots, model aliases); optional"}],"upstreamMayCharge":false},"tools":null,"readme":"# cheap-eyes\n\n**Cheap eyes for an expensive agent.**\n\nAn AI agent's context is expensive. When it reads a big log, a long doc, a web page or a\nwhole code tree, it burns that context on lines it mostly doesn't need. cheap-eyes is\nan MCP server that hands the reading to a cheap model. It then checks every line of that\nmodel's answer against the source, so the agent gets back a short answer it can trust.\n\n## What it looks like\n\nA real header from the server (trimmed), with a stubbed model answer. The agent asked\nfor every ERROR line after 08:10 in an app log, plus the traceback that follows it:\n\n```text\neyes_run result: 2026-09-26_152246-extract-889090\nmodel: alias fast -> qwen/qwen3.6-35b-a3b (used: qwen/qwen3.6-35b-a3b)\ntokens in 412 / out 96; cost $0.000080; 2 ms\ncheck (extract): refs ok 4 / bad 1 — ok 4, ok~ 0, partial 0, not-in-input 0\n  L9 [bad] text differs from the source line\n--- untrusted model output ---\nL3| 2026-09-20 08:14:09.031 ERROR [sync] upload failed: timeout after 30 s\nL4| 2026-09-20 08:14:09.032 Traceback (most recent call last):\nL6| TimeoutError: timed out\nL8| 2026-09-20 08:15:31.550 ERROR [sync] upload failed: HTTP 503 from api.example.com\nL9| 2026-09-20 08:16:00.000 ERROR [sync] retry 2/3 failed\n--- end ---\n```\n\nThe model quoted five lines. In the real log, line 9 says `INFO [sync] retry 2/3 ok`,\nso the model made up an error. The check compared each quote with the source line and\nflagged L9 as `bad`. The agent sees that before it relies on the answer.\n\n## Why you can trust the answer\n\n- **The model has no tools.** It can't open files, browse or call anything. It sees\n  only the numbered lines cheap-eyes chose to send.\n- **Secrets are masked before anything leaves your machine.** Passwords, tokens and\n  keys become `[token]`, `[key]` and so on, in the files and in the task.\n- **Only Zero Data Retention providers.** Those are providers that don't store your\n  prompts. cheap-eyes talks only to OpenRouter and asks for them on every call.\n- **Every quote and line reference is checked mechanically** against the lines that\n  were sent. Wrong values and uncited claims are flagged.\n- **What the check does not prove:** that the answer is complete or relevant. It\n  proves what was quoted is real, not that nothing was missed.\n\n## Requirements\n\n| | |\n|---|---|\n| Runtime | Node.js ≥ 22.17, or Docker, or Home Assistant OS / Supervised |\n| Architecture (Home Assistant app) | amd64, aarch64 |\n| Network | outbound HTTPS to `openrouter.ai`, and to the web pages you name in a job; for claude.ai, a TLS reverse proxy reachable from the internet |\n| Disk | about 56 MB installed, 33 MB of it `pdfjs-dist` with its CMaps and fonts; the optional `@napi-rs/canvas` adds a native binary of about 37 MB that text extraction does not need (`npm install --omit=optional` skips it) |\n| OpenRouter | an API key with its own daily limit |\n\n## Installation\n\n### Claude Code / Claude Desktop (npx)\n\n1. Write a config file at `~/.config/cheap-eyes/config.json`, or\n   `%APPDATA%\\cheap-eyes\\config.json` on Windows (see [Configuration](#configuration)).\n2. Check it (no network, no secrets printed): `npx -y cheap-eyes check-config`\n3. Add the server to Claude Code:\n\n   ```sh\n   claude mcp add --env CHEAP_EYES_OPENROUTER_KEY=<OPENROUTER_KEY> --transport stdio cheap-eyes -- npx -y cheap-eyes\n   ```\n\n4. Try it: *\"Use eyes_run to list every TODO in /home/me/notes/\\*\\*/\\*.md, with the\n   line it's on.\"*\n\nClaude Desktop, `.mcp.json` and a global install:\n[installation](https://github.com/st412m/cheap-eyes/blob/main/docs/installation.md#npx--stdio).\n\n### Home Assistant app\n\nHome Assistant renamed *add-ons* to *apps* in 2026.2; on older versions the same menus\nsay *Add-ons* and *Add-on Store*.\n\n1. Add this repository:\n\n   [![Open your Home Assistant instance and show the add app repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2Fst412m%2Fcheap-eyes)\n\n   Or by hand: **Settings → Apps → App Store → ⋮ → Repositories → + Add**, paste\n   `https://github.com/st412m/cheap-eyes`, select **Add**.\n2. Find **cheap-eyes** in the store and select **Install**.\n3. In **Configuration**, set `openrouter_key`, an `http_token` of at least 32\n   characters (`openssl rand -hex 32` gives one) and your `read_roots`.\n4. Select **Start**. The app serves MCP on port 3400.\n\nOptions and the connector URL:\n[app documentation](https://github.com/st412m/cheap-eyes/blob/main/ha-addon/cheap_eyes/DOCS.md).\n\n### Docker / HTTP\n\n`cheap-eyes serve --http` runs the same server over Streamable HTTP with a token; the\nrepository's `Dockerfile` and `compose.example.yaml` wrap it. See\n[installation](https://github.com/st412m/cheap-eyes/blob/main/docs/installation.md#http-mode).\n\n## Configuration\n\nA JSON config file. The fields to set first:\n\n| Field | What it is |\n|---|---|\n| `read_roots` | The folders (or single files) the server may read. Absolute paths. |\n| `models` | Aliases: an alias like `fast` is a short name for a list of model ids. |\n| `defaults` | The alias each mode uses when a call names none. |\n| `daily_budget_usd` | Local spending cap per UTC day, default `1.0`. |\n\n```json\n{\n  \"read_roots\": [\"/home/me/notes\"],\n  \"models\": { \"fast\": { \"ids\": [\"qwen/qwen3.6-35b-a3b\"], \"params\": { \"reasoning\": { \"effort\": \"none\" } } } },\n  \"defaults\": { \"extract\": \"fast\", \"draft\": \"fast\", \"edits\": \"fast\" }\n}\n```\n\nThe key goes in the env var `CHEAP_EYES_OPENROUTER_KEY`, never in the file. Every\nfield, env var and file location:\n[configuration](https://github.com/st412m/cheap-eyes/blob/main/docs/configuration.md).\n\n## Connecting to claude.ai\n\nRun the HTTP server (or the Home Assistant app) and publish it through a reverse proxy\nthat terminates TLS. The endpoint is:\n\n```text\nhttps://<your-host>/private_<your-token>/mcp\n```\n\nAdd it in **claude.ai → Settings → Connectors → Add custom connector**. A wrong path\ngets a bare 404, and there is no OAuth by design. See\n[security](https://github.com/st412m/cheap-eyes/blob/main/docs/security.md#http-mode).\n\n## Using it\n\nYou talk to your agent as usual. It calls cheap-eyes when a job fits. Pick the mode by\nwhat you want back:\n\n- **extract** when you need exact lines: errors in a log, a config value, every\n  mention of something. Invented lines are flagged.\n- **draft** when you want notes or a summary. Every line must cite its source lines.\n  Values that aren't in those lines, and lines with no citation, are flagged.\n- **edits** when you want fixes proposed. You get JSON edits whose old text is checked\n  against the file. cheap-eyes never applies them.\n- **schema** when you want fields filled: a JSON schema in, the same JSON out with a\n  line ref and a verbatim quote for every value. Conflicting values are reported, not\n  resolved.\n- **grep** when you know the exact pattern: matching windows, no model, $0.\n\nFiles can be text, HTML, PDF (with a text layer), Word (DOCX, DOC), RTF, PowerPoint\n(PPTX, PPT), OpenDocument text and presentations (ODT, ODP), EPUB, FB2 and mail (EML,\nMSG, attachments included), or an `http(s)` URL that the server fetches itself.\nSpreadsheets and scans are refused with the reason. Refs name the page, slide, chapter\nor attachment of a line: `L40 (slide 3)`, `L12 (attachment: offer.pdf, p.2)`\n([formats](https://github.com/st412m/cheap-eyes/blob/main/docs/formats.md)).\n\nPrompts you might type (the files must lie inside your `read_roots`):\n\n- *\"Use eyes_run to pull every ERROR from /var/log/app/app.log since 08:00 today, with\n  the tracebacks.\"*\n- *\"Have cheap-eyes draft a summary of /home/me/notes/deploy.md: versions, ports,\n  backup times.\"*\n- *\"Use eyes_run in edits mode to fix the dead links in /home/me/notes/\\*.md.\"*\n- *\"Use eyes_run in schema mode on /home/me/docs/tender.pdf: customer, maximum price,\n  deadline, and the list of items with quantities.\"*\n\nWhat each check catches and misses:\n[checks](https://github.com/st412m/cheap-eyes/blob/main/docs/checks.md).\n\n## When to use cheap-eyes and when grep\n\ncheap-eyes pays off for **reading by meaning over large text**: logs, archives, long\ndocuments, web pages, \"everything about X\", chronologies. For **exact lookups** — a key,\nwhich blocks contain X, a list by a field, a value in a table — and for files under\nabout 30 KB, grep, a ranged read or code is cheaper and more reliable. That is why\nspreadsheets are not read at all.\n\nOn a benchmark of 2026-09-30 with independent ground truth:\n\n- An 887 KB log archive, task \"everything about power\": recall 96 %, precision 100 %,\n  $0.08, about 12k tokens of the agent's context instead of about 227k for reading it\n  whole.\n- A structural task over Home Assistant automations (\"who sends to Telegram\"):\n  precision 41 % with a green check. The check catches invented lines, not wrongly\n  chosen ones.\n\nA snippet for `CLAUDE.md` or your Claude settings:\n\n```\nReading by meaning over large text (logs, archives, long documents, web pages, \"everything about X\") → cheap-eyes eyes_run (extract/draft; fields → schema). Exact values, structure, small files (< ~30 KB) → grep / ranged read (no model: eyes_run mode grep). Verify any value from cheap-eyes against the source (eyes_result source) before relying on it.\n```\n\nTo compare models on your own documents, run\n[`cheap-eyes bench`](https://github.com/st412m/cheap-eyes/blob/main/docs/bench.md).\n\n## Tools\n\n- `eyes_run` — does the job and returns a short, checked header.\n- `eyes_result` — reads a stored result: pages, grep, the check report, the source text\n  the model was given; cancels a running job.\n- `eyes_stats` — spend today and all time, your key's status; on request, the model\n  table.\n\nInputs and outputs: [tools](https://github.com/st412m/cheap-eyes/blob/main/docs/tools.md).\n\n## Models and cost\n\nNo model is pinned in code. You name models in your config under aliases (`fast`,\n`long`, …), and switching means editing the config and restarting. Each alias lists\nmodel ids in priority order; if one can't be used, the next is tried. The example ids\nwere valid on 2026-09-26 and will age. To pick new ones:\n\n```sh\ncheap-eyes models --candidates\n```\n\nThis lists cheap Zero Data Retention models, and shows how well each of your aliases\npassed its checks on your own jobs. If your OpenRouter account's Guardrails don't allow\na model, cheap-eyes skips it and marks it `blocked by account`. Allow it at\nopenrouter.ai → Guardrails if you want it used.\n\nWith cheap models a job costs fractions of a cent. Your key's own daily limit on\nOpenRouter is the real ceiling. On top of that, `daily_budget_usd` (default $1 a day)\nstops calls before they would go over. More:\n[models and spending](https://github.com/st412m/cheap-eyes/blob/main/docs/models.md).\n\n## Limitations\n\n- **The check proves quotes are real, not that the answer is complete.** A relevant\n  line the model skipped is not flagged.\n- **No OCR.** Scanned PDFs are refused with the reason; layout and images are lost in\n  extraction.\n- **No spreadsheets.** Excel and OpenDocument spreadsheets are refused: questions about\n  a table are exact lookups, which grep or code answers and a model reading the table\n  as text gets wrong.\n- **Size caps.** A text file (or the text extracted from a document) over 2 MB is\n  refused, never truncated; a document over 50 MB and a URL over 10 MB likewise; a job\n  is capped at 5 MB after filtering and 200 files.\n- **No reduce pass for `draft`.** With several chunks, each chunk's notes come back\n  under their own header.\n- **The hook covers Claude Code's `Read` tool only.** Shell commands are not\n  intercepted.\n\n## Troubleshooting\n\n- **OpenRouter answers 403 \"Access denied by security policy\".** OpenRouter refuses\n  some regions; route the calls through a proxy.\n- **A model is `blocked by account`.** Allow it at openrouter.ai → Guardrails, or put\n  another id in the alias.\n- **The server won't start and names an alias.** A value in `defaults` is not defined\n  in `models`.\n- **The client shows old tools after an update.** Refresh the connector's tool list\n  and start a new chat.\n\nMore: [troubleshooting](https://github.com/st412m/cheap-eyes/blob/main/docs/troubleshooting.md).\n\n## Security\n\n- The model gets no tools; everything it returns is framed as untrusted output.\n- Only files inside `read_roots` are read, after resolving symlinks; `.env*`, keys,\n  `secrets.yaml`, databases and `.git` are always refused.\n- Secrets are masked before anything leaves the machine; model calls go only to\n  `openrouter.ai`, with Zero Data Retention required.\n- URLs in a job are fetched by the server with private and local addresses refused,\n  also against DNS rebinding.\n- The server writes nowhere outside its own state folder unless you opt into export.\n- In HTTP mode the token works as a password. It needs at least 32 characters and is\n  never logged; put TLS in front.\n\nMore: [security](https://github.com/st412m/cheap-eyes/blob/main/docs/security.md).\n\n## Links\n\n- [Documentation](https://github.com/st412m/cheap-eyes/tree/main/docs) —\n  [installation](https://github.com/st412m/cheap-eyes/blob/main/docs/installation.md),\n  [configuration](https://github.com/st412m/cheap-eyes/blob/main/docs/configuration.md),\n  [tools](https://github.com/st412m/cheap-eyes/blob/main/docs/tools.md),\n  [checks](https://github.com/st412m/cheap-eyes/blob/main/docs/checks.md),\n  [models and spending](https://github.com/st412m/cheap-eyes/blob/main/docs/models.md),\n  [results](https://github.com/st412m/cheap-eyes/blob/main/docs/results.md),\n  [security](https://github.com/st412m/cheap-eyes/blob/main/docs/security.md),\n  [hook](https://github.com/st412m/cheap-eyes/blob/main/docs/hook.md),\n  [formats](https://github.com/st412m/cheap-eyes/blob/main/docs/formats.md),\n  [bench](https://github.com/st412m/cheap-eyes/blob/main/docs/bench.md),\n  [troubleshooting](https://github.com/st412m/cheap-eyes/blob/main/docs/troubleshooting.md),\n  [internals](https://github.com/st412m/cheap-eyes/blob/main/docs/internals.md)\n- [Home Assistant app](https://github.com/st412m/cheap-eyes/blob/main/ha-addon/cheap_eyes/DOCS.md)\n- [Changelog](https://github.com/st412m/cheap-eyes/blob/main/CHANGELOG.md)\n\n## Prior art\n\nBoth MIT-licensed; cheap-eyes is written from scratch (see `NOTICE`).\n\n- [BardyLaw/claude-worker-delegation](https://github.com/BardyLaw/claude-worker-delegation)\n  — the server reads files by path, short preview, usage log, daily budget, PreToolUse\n  hook. cheap-eyes adds what it lacked: a `realpath` path guard, refusal instead of\n  silent truncation, masking, line numbers and checking the output.\n- [aashahin/claude-opencode-delegate](https://github.com/aashahin/claude-opencode-delegate)\n  — async task registry, compact status header.\n\n## License\n\n[MIT](https://github.com/st412m/cheap-eyes/blob/main/LICENSE) — by st412m.\n"}