webscope
# Webscope
<!-- mcp-name: io.github.partprogramming09/webscope -->
A small MCP server (stdio, JSON-RPC 2.0, Python standard library only) that gives a coding agent short answers about a
project: grouped search hits, one symbol instead of a whole file, and the output of the project's own read-only scripts,
trimmed to head and tail. The goal is fewer tokens per question.
Tool names, parameters and messages are in Spanish.
## Requirements
- Python 3.9 or newer (no third-party packages)
- Optional: [uv](https://docs.astral.sh/uv/), to run it with `uvx` without cloning
- `grep` on `PATH` (used by `buscar`)
## Installation
1. In your project's root, create its manifest and list the folders and scripts it may use
(see [Configuration](#configuration-webscopejson) and [`webscope.example.json`](webscope.example.json)).
2. Register the server. With Claude Code, from the project's root, pick one:
```sh
# with uv, no clone needed (like npx)
claude mcp add --scope project webscope -- uvx --from git+https://github.com/partprogramming09/webscope webscope-mcp --manifest "$PWD/webscope.json"
# with a clone and plain Python
git clone https://github.com/partprogramming09/webscope.git ~/webscope
claude mcp add --scope project webscope -- python3 -I ~/webscope/webscope.py --manifest "$PWD/webscope.json"
```
`--scope project` writes `.mcp.json` so the whole team gets it; use `--scope user` for all your projects. Add
`-e WEBSCOPE_ESCRITURA=1` before `--` only if a script declares `escribe` and you want to allow it.
3. Start `claude` in the project, approve the server, and check it with `/mcp`: `webscope` should show as connected
with the tools `buscar`, `leer` and `correr`.
Other MCP clients: add the same command to their configuration, for example a `.mcp.json`:
```json
{
"mcpServers": {
"webscope": {
"command": "python3",
"args": ["-I", "/path/to/webscope/webscope.py", "--manifest", "/path/to/your/project/webscope.json"]
}
}
}
```
Instead of `--manifest` you can set the environment variable `WEBSCOPE_MANIFEST`. Without either, the server uses
`./webscope.json` if it exists; otherwise it serves the current folder with `buscar` and `leer` and no scripts.
### Docker
```sh
docker build -t webscope .
docker run -i --rm -v "$PWD:/project:ro" -e WEBSCOPE_MANIFEST=/project/webscope.json webscope
```
Without `WEBSCOPE_MANIFEST` the image serves the bundled `webscope.example.json`.
## Tools
| Tool | What it does |
| :--- | :--- |
| `buscar(texto, donde?, tope?, formato?, contexto?, regex?)` | Case-insensitive search, literal unless `regex=true` (extended regex). Hits grouped by file, most-referenced files first, capped at `tope` lines. `donde` limits it to a folder or file; `contexto` (0-5) adds lines around each hit. `formato="conciso"` returns only the top 5 files with counts. |
| `leer(archivo, simbolo?, lineas?, max_lineas?, cola?, contiene?, formato?)` | Returns only one function, method or class (PHP, TS, JS, Vue, Python) — or several, if `simbolo` is a list —, one Markdown section by heading, a line range (`lineas="120-180"`), or for `.log`/`.txt`/`.out` files the last `cola` lines and/or the lines containing `contiene`. Without `simbolo` or `lineas` it returns the file's index: headings or definitions with their line numbers. |
| `correr(nombre?, args?, detalle?)` | Runs a script declared in the manifest and returns `exit N` plus trimmed output. Without `nombre` it lists the scripts; `detalle="esquema"` shows one script's usage. Read-only. |
| `correr_escritura(nombre, args)` | Only listed when a script declares `escribe`. Runs a write argument; requires `WEBSCOPE_ESCRITURA=1` in the server environment and is marked `destructiveHint` and `anthropic/requiresUserInteraction`. |
Every answer starts with one short line saying what it is and what to do next, and is capped at `tope_tokens`
(about 4 characters per token).
A malformed request, a non-UTF-8 script output or an unexpected error comes back as an error message; the server keeps
running. `buscar` skips files that are not valid UTF-8 when the locale is UTF-8 (grep treats them as binary).
## Security model
- **Project root only.** `buscar` and `leer` resolve paths against the manifest's `root` and reject anything outside it.
- **Secret files are invisible.** `buscar` and `leer` skip `.env`, `.env.*`, `*.pem`, `*.key`, `*.p12`, `id_rsa*`,
`id_ed25519*`, `auth.json` and `credentials*` (templates ending in `.example` stay visible).
- **Fixed commands, no shell.** Scripts run as a fixed argument list from the manifest. Arguments that are absolute paths
or contain `..` are rejected (unless the script sets `acepta_barra`, which allows URL-like `/a/b` but still not `..`).
Flags listed in `rechaza` are rejected.
- **Read-only by default.** `correr` refuses any argument listed in a script's `escribe`. Those go through
`correr_escritura`, which only exists when declared and only runs when the user sets `WEBSCOPE_ESCRITURA=1`.
- **Minimal environment.** Scripts do not inherit the server's environment. They receive only `PATH`, `HOME`, `LANG`,
`LC_ALL`, `TMPDIR`, the variables named in the manifest's `env_permitidas`, plus `WEBSCOPE_RAIZ` (project root) and
`WEBSCOPE_MANIFIESTO` (manifest path).
- **Bounded output.** Script output is trimmed to head and tail lines, and each response to `tope_tokens`.
The scripts themselves are trusted code: Webscope limits how they are called, not what they do. Only list scripts you
would run yourself.
## Configuration (`webscope.json`)
See [`webscope.example.json`](webscope.example.json).
```json
{
"root": ".",
"recorte": { "cabeza": 10, "cola": 70 },
"tope_tokens": 5000,
"buscar": { "dirs": ["src"], "extensiones": ["py", "ts"] },
"env_permitidas": ["DATABASE_URL"],
"scripts": {
"report": {
"cmd": ["python3", "-I", "scripts/report.py"],
"descripcion": "Summarises a table",
"uso": "<table>",
"rechaza": ["--out"],
"escribe": ["--fix"],
"timeout": 120
}
}
}
```
| Key | Meaning |
| :--- | :--- |
| `root` | Project root, relative to the manifest's folder. |
| `recorte` | Default head (`cabeza`) and tail (`cola`) lines kept from script output. |
| `tope_tokens` | Maximum size of any response, in approximate tokens. |
| `buscar.dirs`, `buscar.extensiones` | Folders and file extensions searched by default. |
| `env_permitidas` | Extra environment variables passed to scripts. |
| `scripts.<name>.cmd` | Fixed command as a list; `python3` is replaced by the server's interpreter. |
| `descripcion`, `uso` | Shown by `correr()` listings. |
| `rechaza` | Flags that would write files; always refused. |
| `escribe` | Arguments that write data; only via `correr_escritura` with `WEBSCOPE_ESCRITURA=1`. |
| `acepta_barra` | Allow arguments starting with `/` (URL paths). |
| `timeout`, `cabeza`, `cola` | Per-script overrides. |
## Included scripts
The package ships ready-made scripts in `webscope_adaptadores/`. A manifest names them with `{adaptadores}` in `cmd`
(see [`webscope.example.json`](webscope.example.json)); copy the entries you need and adjust the folders.
| Script | What it answers | Stack |
| :--- | :--- | :--- |
| `vue_ts.py` (`mapa`) | Routes, views, API calls, composables and stores of a frontend module; `--recorrido` lists what can be clicked (actions with their `v-if` and permission, links, tabs with their API, form fields); `--cobertura` says which calls an e2e capture exercised | Vue 3 + TS |
| `laravel.py` (`mapa_back`) | Endpoint, controller, service, model and table behind a frontend module or API path | Laravel |
| `astro.py` (`mapa_web`) | File-based routes and endpoints with what they call; for one page, its components, data, client scripts, API calls and form fields with their visible (translated) label; `--cobertura <capture>` counts the page's `/api/` calls a capture exercised | Astro |
| `e2e_chrome.py` | JavaScript for **one** `javascript_tool` call of Claude in Chrome: walks routes and returns per route time, rows, failed requests, console errors and UI problems, without screenshots. `--llamar` reuses the walk already installed in the page; `--captura`/`--detalle` record what the page sends and receives; `--form` installs `__f` to fill a form by label, press Save and read it back, plus screen helpers that replace screenshots and coordinate clicks: `estado()` (the screen as short text), `clic(text)` (click by visible text, then wait until the page settles), `esperar`, `ir(route)` (router navigation without reload) and `pasos([...])` (several steps in one call). With a local receiver: `--servir [port folder]` starts it, `--cargar [port]` installs capture and `__f` from it, `--guardar <name> [port]` sends the whole capture to `<folder>/<name>.txt` (with one `ms <ms> <method> <status> <path>` line per API call; the receiver answers with the 5 slowest) and the steps `__f` recorded to `.claude/webscope/recorridos/<name>.json`, and `--repetir <name>` replays those steps in one call (with a fresh stamp on `ZZ_E2E` values) while the frontend does not change; `--exportar <capture> <url>` writes a Playwright test with the same steps | Any SPA with a router |
| `payload_check.py` | Checks a capture against the FormRequest rules, the Resource and the table columns; flags keys without a rule, values outside `in:`, `''` sent to a nullable column | Laravel |
| `limpiar_zz.py` | Lists, and with `--borrar` deletes after a backup, test rows whose code starts with `ZZ_` | Laravel |
| `costo_sesion.py` | Cost of the latest (or a given) Claude Code session in the project: USD per model at list prices (subagents and advisor included), calls and re-read context per tool, large outputs, repeated calls, and which tool loops would have been cheaper in a subagent | Claude Code |
| `uso_agentes.py` | Subagent usage: turns, context, models, agents over the turn limit | Claude Code |
| `uso_webscope.py` | Share of file reads and searches that went through `leer` and `buscar` in sessions where Webscope was available, and the `Read`/`sed`/`grep` calls used instead | Claude Code |
Project-specific settings go in the manifest's `proyecto` section:
| Key | Used by | Meaning |
| :--- | :--- | :--- |
| `modulos` | `payload_check` | Backend modules folder (default `app/Modules`) |
| `prefijo_rutas` | `e2e_chrome` | Prefix the app lives under, e.g. `/demo`, so routes work with or without it |
| `contenedor` | `mapa_back` | Docker container where `php artisan route:list --json` runs; empty reads the route files |
| `sql` | `payload_check`, `limpiar_zz` | Script that runs a read-only SQL query and prints TSV with a header |
| `tinker` | `limpiar_zz` | Script that runs a PHP file against the database (`<script> <file.php> --commit`) |
| `php_db` | `limpiar_zz` | PHP expression for the connection (default `DB::connection()`) |
| `capturas` | MCP resources | Captures folder (the one given to `--servir`); its `.txt` files are listed as `webscope://capturas/<file>` next to the saved walks `webscope://recorridos/<file>` |
The Vue index cache and `limpiar_zz` backups go to `~/.cache/webscope/`. The `e2e_chrome` receiver writes captures to the
folder given to `--servir` (default `capturas`, relative to where it starts). `--servir` does not return: start it from a
shell in the background (the example manifest lists it in `rechaza`, so `correr` refuses it).
### Using the scripts from a subagent
A subagent sees Webscope only if its definition lists the tools, for example in Claude Code's `.claude/agents/<name>.md`:
```yaml
tools: Read, Grep, Glob, Bash, mcp__webscope__buscar, mcp__webscope__leer, mcp__webscope__correr
model: sonnet
```
Add `mcp__webscope__correr_escritura` only if the agent must run write arguments, plus the browser tools it needs.
[`agents/recorrido.example.md`](agents/recorrido.example.md) is a ready-made agent that walks one frontend module in
Chrome with `mapa`, `e2e_chrome` and `limpiar_zz` and returns a summary of at most 25 lines: copy it to
`.claude/agents/recorrido.md` and replace `<webscope>` (the clone's folder; the receiver runs from the clone, not from
`uvx`) and the captures folder.
One measurement (a browser walk of one module, one project, one run per condition) found:
- With the walk in the main session, Webscope did not change the cost (+7 % and −5 % in two runs). It changed accuracy:
the order of steps (`mapa --recorrido`), the fields that were not saved (`__f.verificar`) and the measured coverage.
- The same walk in a sonnet subagent that returns a summary of at most 25 lines cost 1.05 USD instead of 6.40, with the
same coverage. About 2× came from isolating the context; the rest from the sonnet price.
- Page reads and screenshots were about 80 % of the re-read context, so the agent reads state with `javascript_tool` and
takes screenshots only to understand an error.
- Subagents that ran for more than about 40 turns grew their context like a main session. Give one module or one bounded
task per agent, and tell it to stop at about 40 tool calls and return what is done and what is pending.
`costo_sesion.py --raiz <project>` and `uso_agentes.py --raiz <project>` measure the same in another project. The USD
come from the price table at the top of `costo_sesion.py` (list prices, October 2026), which only lists models checked
against the cost reported by `claude -p` (Opus 5.5, Sonnet 5.5, Fable 5.1: within 0.01 USD on three runs); any other
model is shown as `sin precio` until it is added.
## Hook: suggest Webscope
Agents often keep using `Read`, `cat`, `sed -n` and `grep` even when Webscope is available. `hooks/sugerir_webscope.py`
is a Claude Code `PostToolUse` hook that, after such a call, adds one line to the model's context with the equivalent
Webscope call (for example `leer(archivo, lineas="120-180")`). It never blocks and speaks at most 3 times per session.
Add it to the project's `.claude/settings.json` or `.claude/settings.local.json`:
```json
{"hooks": {"PostToolUse": [{"matcher": "Read|Bash", "hooks": [{"type": "command",
"command": "python3 -I /path/to/webscope/hooks/sugerir_webscope.py"}]}]}}
```
`uso_webscope.py` measures whether it works: the share of reads and searches that go through Webscope.
## Browser UI check
`webscope_adaptadores/web/chequeo-ui.js` is a standalone snippet for a browser automation tool (`page.evaluate` in Playwright, or a
JavaScript tool in a browser extension). It returns only the problems on the current page: horizontal overflow, empty
chips or badges, untranslated i18n keys, raw values (`undefined`, `NaN`…), overlapping text and unnamed buttons.
```sh
PLAYWRIGHT_DIR=<folder containing node_modules/playwright> node webscope_adaptadores/web/test_chequeo_ui.mjs
PLAYWRIGHT_DIR=<folder containing node_modules/playwright> PW_CHANNEL=chrome node webscope_adaptadores/web/test_form.mjs
```
`PW_CHANNEL=chrome` uses the installed Chrome when Playwright's own browser is missing.
## Tests
```sh
python3 -I test_webscope.py
```
## Out of scope
Webscope does not replace a language server or a browser automation server, does not run code written by the agent, and
is not a test framework.
## License
[MIT](LICENSE)
TDQS
Scored across 3 tools
Cada herramienta tiene un rol claramente distinto: buscar localiza texto, leer extrae fragmentos de archivo y correr ejecuta scripts de solo lectura. La descripción de buscar redirige explícitamente a leer para ver código, eliminando cualquier solapamiento práctico.
Los tres nombres son verbos en infinitivo en español (buscar, correr, leer), con una convención uniforme y predecible en todo el conjunto.
Tres herramientas cubren las operaciones esenciales de exploración de código de solo lectura (buscar, leer, ejecutar scripts) sin redundancia; el conjunto está bien acotado para su propósito.
El conjunto cubre búsqueda textual, lectura por fragmentos y ejecución de scripts de solo lectura, pero no ofrece una operación directa para listar archivos/directorios ni búsqueda por nombre o regex. Además, se menciona correr_escritura sin incluirla, aunque el diseño parece intencionalmente read-only; son lagunas menores que un agente podría sortear vía scripts del manifiesto o búsqueda textual.