webscope
# Webscope
<!-- mcp-name: io.github.partprogramming09/webscope -->
A lightweight, token-efficient MCP server written in Go (stdio, one static binary, no runtime to install). It gives coding agents short, surgical answers: grouped search hits, one symbol instead of a whole file, tests that drive the user's open Chrome, and bounded project scripts.
Tool names, parameters, and messages are in English.
**Scope.** The core (`search`, `read`, `run`, `run_write`, `test`) knows no stack. Stack knowledge lives in packs: `packs/laravel/` holds 23 adapters as scripts (Laravel, Vue, Astro, Python) that a manifest declares, and `packs/laravel/stacks.json` tells `--init` which of them a project gets. The `test` tool works on any app in Chrome; it was checked on a plain-ARIA page, a CRUD page with Vuetify look-alike markup and a CRUD page built with the real Vue 3 and Vuetify 3 libraries (`fixtures/`).
**Status.** `v1.0.0` is the Go server: GitHub release with 5 binaries, 5 `.mcpb` bundles, checksums and `server.json`. The container image (`ghcr.io/partprogramming09/webscope:1.0.0`) is not pushed yet and the entry in the MCP registry is not published. The previous release `0.6.0` is the Python server (PyPI `webscope-mcp`; source in the `legacy/python` branch). The Laravel adapters came out of one application; the parity scripts (`docs/parity-scripts/README.md`) were also run live on a Laravel 12 fixture with two tenants on MySQL, PostgreSQL and SQLite (`fixtures/laravel/run-engine-checks.sh`), the other adapters only on that application, and most automated tests use toy file trees. The measurements below were taken with the earlier Python server and the same adapters; the Go server has its own measurements in `docs/rework/`.
---
## Measured Effect
**Why not the agent's own grep?** Grep and read answer «where is X». They cannot compute an answer that crosses layers or reads the database in one step. Measured below: 5 calls against 1 for a column sheet, 6 against 1 for «who reads this column». Where plain search is enough (map of a module), there is no measurable difference: 6 calls against 7.
The table below was measured on the earlier Python server (0.6.0) on 2026-10-20, before the Go rewrite; the Go server has its own, smaller measurements in «Measured on the Go server» after it. Measured 2026-10-20 (second run of the same method): four read-only questions about a real Laravel + Vue monorepo, asked through `claude -p` (Sonnet) in a clean session, five runs each per condition (40 runs), order randomized per pair, one warm-up run per condition discarded, ground truth fixed by hand from sources that are not adapters (a grep of the code, direct SQL counts, a manual check of each file). Hypotheses and thresholds were written before the run: the column sheet and the consumers question cost at most half as much with webscope (met: 25 % and 19 % of the median cost); the module map differs by less than 20 % (met: 14 % lower). The method, the environment and the numbers per run are in [`bench/`](bench/); the table below is printed by `bench/report.py` from `bench/results/runs-v3.json`.
| Question | USD without: median (min–max) | USD with: median (min–max) | Calls without → with (median) | Complete answers without → with |
| :--- | :--- | :--- | :--- | :--- |
| Backend module and tables of a frontend module | 0.050 (0.045–0.058) | 0.043 (0.037–0.050) | 6 → 7 | 5/5 → 5/5 |
| Rows with a value per column of a table | 0.053 (0.035–0.185) | 0.013 (0.013–0.023) | 5 → 1 | 5/5 → 5/5 |
| Type clashes between rules and columns | 0.165 (0.111–0.257) | 0.093 (0.038–0.272) | 11 → 7 | not scored |
| Backend files that read or write a column | 0.088 (0.065–0.245) | 0.017 (0.013–0.017) | 6 → 1 | 4/5 → 5/5 |
Totals over the 40 runs (sums): 2.26 USD and 146 calls without webscope and 0.93 USD and 80 calls with it. The ranges do not overlap for the column sheet and the consumers question; they overlap for the module map and the type audit, where no difference can be claimed. The first run of this method (`bench/results/runs.json`, 2026-10-20 as well) gave the same picture: 2.02 USD against 1.00 USD in total.
Read with care:
- One model, one project, and questions chosen because an adapter covers each. Questions with no adapter are not measured; the module map is the realistic one for them.
- USD includes cached tokens. The first run showed cost rising with each repetition; this run does not (without webscope per repetition: 0.42, 0.38, 0.43, 0.59, 0.44 USD). The expensive repetitions are the ones with the most cache-creation tokens, which makes cache effects the probable cause of the outliers (five points per condition, not tested further); there is no trend.
- «Complete answers» means every item of the hand-fixed truth was found. The one incomplete answer without webscope missed a file.
- An earlier run of 24 pairs scored against adapter output is discarded: that scoring was circular. The type audit is not scored because it has no independent truth; its precision was measured separately (see Checks below).
### Measured on the Go server
Measured 21 and 22 October 2026 with `bench/accept.py` through `claude -p` (Sonnet); a run that errors, times out or leaves the server unconnected never counts as a cheap success. What it shows and what it does not:
- **Filling a form** (`bench/tasks-fill.json`, a synthetic 9-field form, 5 runs each): `test` with `fill` cost 0.091 USD (median, range 0.080 to 0.098) and 14 calls; the generic Playwright MCP cost 0.128 USD (0.107 to 0.165) and 22 calls; both answered 5 of 5 correctly. That is 29 % cheaper, with ranges that do not overlap. The agent used 11 webscope calls where one `fill` would do. One task, one form, not the application above. The N and G rows come from two batches, not interleaved (`bench/results/fill-wide-2026-10-22.json`).
- **A question about where a screen is used** on the application above (`t1`, 5 runs each): with the Go server 0.110 USD and 12 calls, with no server 0.218 USD and 11 calls, with the Python server 0.047 USD and 2 calls. The Go server is cheaper than no server and not cheaper than the Python one; the Python server's instructions send the agent to its scripts first, and whether that explains the gap was not tested. `t2` ran twice per condition and `t3` not at all: no verdict.
- **One-shot questions** (find a file, count matches, a symbol; `bench/tasks-synthetic.json`): the agent never called a webscope tool in 12 of 12 runs, so there is nothing to compare. The server's own instructions say that a one-off search or read costs the same with grep and Read.
- **Not measured:** other models, other agents than Claude Code, Windows, and any claim of savings on a task that is not one of the above.
### Checks
Measured on the same application, 2026-10-20, labels set by hand from the code (not by an adapter). Counts are small: read them as orders of magnitude.
- `audit_types`: before the change, 28 of 36 labelled findings were real (78 %). The false ones came from joining a column by name across tables, from services that transform the value before the column (`true` to `'on'`), from filter-only requests and from rules that already bound the value. It now narrows to the table of the model the request names and marks as `heuristic` any finding it cannot tie to one table (nested keys, no model match, `boolean`, `in`/`exists`/closure rules); only the others set exit code 1. On the sample used to tune it: 14 of 15 real; on 12 fresh findings: 10 of 12; together 24 of 27 (89 %). The last rule change (`in`/`exists`) was not measured on a new sample. Target was 90 %: not reached.
- `unique_indexes`: 9 of 22 findings labelled before the labeller stopped: 8 real, 1 false (`CATCHES?` guessed from the folder).
- `tenant_queries`: 23 of 92 lines labelled: all 23 are real default-connection queries on tenant tables; none is reachable from a job, command or export, so the risk it warns about did not show up in this application.
- Engines: `fixtures/laravel` (Docker; Laravel 12 with MySQL 8, PostgreSQL 16 and SQLite) checks that `tinker --read-only` is refused by the database, that the default run rolls back and that only `--commit` persists, by row counts. Run it with `sh fixtures/laravel/run-engine-checks.sh`.
- CI runs on GitHub (`.github/workflows/go.yml` for the server on Ubuntu and macOS with `-race`, and the Docker image check; `pack.yml` for the adapters and the engine job). Windows is not covered and not validated. One test of the `matrix` delete fails now and then on macOS (see `docs/known-failures.md`).
---
## Quick Start
### 1. Get the binary
Download a binary or `.mcpb` bundle from the `v1.0.0` GitHub release, or build it from source (Go 1.27 or newer):
```sh
go build -o bin/webscope ./cmd/webscope
```
`release/build.sh` builds the five release targets with checksums, `release/mcpb.sh` packs each one as an `.mcpb` bundle, and the `Dockerfile` builds an image. To rebuild them locally:
```sh
VERSION=1.0.0 release/build.sh dist # darwin/arm64, darwin/amd64, linux/amd64, linux/arm64, windows/amd64 + SHA256SUMS
VERSION=1.0.0 release/mcpb.sh dist dist-mcpb # one .mcpb bundle per binary (needs npx)
release/docker_check.sh # builds the image and checks it answers over stdio
```
The binary reports its version in `serverInfo` (`webscope -version`); a plain `go build` reports `1.0.0-dev`, a release build sets it from `VERSION`.
### Command line
| Flag | What it does |
| :--- | :--- |
| `-manifest <path>` | Path of `webscope.json` (default `$WEBSCOPE_MANIFEST`, else the current folder as root with no scripts). |
| `-init [folder or .json]` | Detects the stack and writes `webscope.json`. |
| `-adapters <folder>` | Pack folder used by `-init`. |
| `-agent [folder]` | Exports the `walkthrough` and `screenpass` agents. |
| `-doctor` | Checks the manifest, its scripts and the environment (Docker, PHP, Chrome). |
| `-force` | Lets `-init` and `-agent` overwrite what they would write. |
| `-version` | Prints the version. |
### Environment variables
| Variable | Effect |
| :--- | :--- |
| `WEBSCOPE_MANIFEST` | Manifest path when `-manifest` is not given. |
| `WEBSCOPE_TEST=1` | Offers the `test` tool (also needs a base URL). |
| `WEBSCOPE_BASE_URL` | Base URL of the app under test (overrides `project.base_url`). |
| `WEBSCOPE_CDP` | Chrome debugging address (default `http://127.0.0.1:9222`). |
| `WEBSCOPE_CHROME_PROFILE` | Profile folder of a Chrome with remote debugging switched on in `chrome://inspect`, to read `DevToolsActivePort` from. Without it webscope never reads your everyday Chrome profile. |
| `WEBSCOPE_WRITE` | `1` (asks first) or `no-confirm`: allows `run_write` and scripts that write to the database. |
| `WEBSCOPE_SENSITIVE=1` | Allows scripts the manifest marks as sensitive. |
Scripts receive `WEBSCOPE_ROOT` and `WEBSCOPE_MANIFEST`.
### Repository layout
| Path | Contents |
| :--- | :--- |
| `cmd/webscope/` | The binary: flags, `--init`, `--agent`, `--doctor`. |
| `internal/server/` | MCP stdio server and tool registry. |
| `internal/tools/{search,read,run,test}/` | The four tools. |
| `internal/{cdp,manifest,sandbox,initcmd,textutil,version}/` | Chrome connection, manifest, root and secret sandbox, init and doctor, helpers, version. |
| `web/` | JavaScript page helpers the `test` tool injects (`__f`, `__e2e`). |
| `packs/laravel/` | Stack pack: adapters and `stacks.json`. |
| `agents/`, `skills/` | Exported subagents and skills. |
| `fixtures/` | Test apps (ARIA, form, Vuetify) and the Laravel engine fixture. |
| `bench/` | Benchmark method and results. |
| `release/` | Build, `.mcpb` packing, Docker check, MCP registry file. |
| `docs/` | Guides, `rework/` (Go spec, baseline, conformance), `known-failures.md`, `patterns.md`. |
### 2. Initialize your project
Run in your project root. It detects the stack (Laravel, Vue, Astro or Python get stack-specific scripts from the pack; anything else gets the generic set) and writes `webscope.json`:
```sh
bin/webscope --init --adapters /path/to/webscope/packs/laravel
```
Without `--adapters` it looks for `packs/laravel` beside the binary or in the current folder; with no pack it writes a manifest with no scripts. The manifest gets only the core scripts of the pack; add `--all` for the extra ones (the list is in `docs/PURPOSE.md`; they stay in the pack either way). `--doctor` checks the manifest, that every script file exists, and whether Chrome answers.
### 3. Register with your MCP client
```sh
claude mcp add --scope project webscope -- /absolute/path/to/bin/webscope -manifest "$PWD/webscope.json"
```
Start `claude` and verify with `/mcp`: `webscope` is connected with `search`, `read` and `run` (and `test` when a base URL is set). Without a manifest the server refuses to start from the filesystem root.
---
## Tools
| Tool | Description |
| :--- | :--- |
| `search(text, where?, limit?, format?, context?, regex?)` | Case-insensitive search grouped by file with relevance ranking. `format="concise"` returns top 5 files. Skips secrets (`.env`, `*.key`). |
| `read(file, symbol?, lines?, max_lines?, tail?, contains?, format?)` | Surgical read: extracts a single function, method, class (PHP, TS, JS, Vue, Python), Markdown section, line range, or log tail (`tail`). |
| `run(name?, args?, detail?)` | Runs a declared read-only script from the manifest. Output trimmed to head and tail limits. |
| `run_write(name, args)` | Runs scripts declared in `writes`. Requires `WEBSCOPE_WRITE=1` and interactive user confirmation. |
| `test(action, tab?, args?)` | Drives a tab of the app under test in a running Chrome (see below). Offered only when `WEBSCOPE_TEST=1` is set and `project.base_url` or `WEBSCOPE_BASE_URL` names the app, so a project that does no browser tests does not pay for the tool. |
Every response starts with a one-line actionable guide and is capped at `token_limit` (~4 characters per token).
---
## Browser tests: the `test` tool
`test` reaches the screen in one of two ways. Webscope never starts a browser and never touches your everyday Chrome on its own:
1. **Claude in Chrome (first choice).** `test(action="emit", args=["state"])` returns a short snippet that the agent runs in your open tab with the extension's JavaScript tool. It uses your own Chrome and your login, opens no port and shows no permission prompt (see «Running in your own Chrome through another browser tool» below).
2. **A Chrome you start yourself with a debugging port and its own profile.** Start it as shown under «Native Chrome Connector» (`--remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-webscope"`); webscope uses the one that answers on `http://127.0.0.1:9222` (or the address in `WEBSCOPE_CDP`). If none answers, `tabs` fails with that instruction. If the app needs a login, sign in once in that window; the profile keeps it.
`test` attaches to one tab, runs one action and lets go; it keeps no session, so every call names its tab.
From Chrome 136 the debugging port is ignored on the default profile, so give that Chrome its own `--user-data-dir`. Your normal Chrome also works after you switch on `chrome://inspect/#remote-debugging` **and** set `WEBSCOPE_CHROME_PROFILE` to its profile folder (webscope does not look for it); it asks for permission once per server session.
### Running in your own Chrome through another browser tool
`test(action="emit", args=["state"])` returns a JavaScript snippet (under 500 bytes) instead of running the action. Run it in an open tab with any tool that evaluates JavaScript in the page (for example the Claude in Chrome extension's `javascript_tool`): it loads the page helpers from a small server webscope starts on `127.0.0.1` and calls the action, so no debugging port is needed. `emit` covers page actions only; `tabs`, `newtab`, `newwindow`, `closetab` and `upload` run inside the tool. The page's content security policy must allow `fetch` and `eval`. The result comes back as text with URL query strings cut out (some browser tools refuse results that carry them), `simulate` and `observe` run in the page this way too, and the snippet reinstalls a page helper that comes from an older script.
### Which browser mode
| Mode | Permission prompts | Sessions | How |
|---|---|---|---|
| **extension** (default of the exported `screenpass` agent) | none: the tool never opens a debugging connection | the user's own Chrome, with their logins | `emit` + the Claude in Chrome extension's `javascript_tool`; tabs opened with the extension |
| **cdp, own profile** | none | a separate Chrome; sign in once, the profile keeps it | start Chrome with `--remote-debugging-port=9222 --user-data-dir=$HOME/.chrome-webscope` |
| **cdp, normal Chrome** | one per MCP session (again after reconnecting the server or restarting Chrome) | the user's Chrome | switch on `chrome://inspect/#remote-debugging` and set `WEBSCOPE_CHROME_PROFILE` |
Use extension unless the user asks otherwise; for an isolated run with no sessions use the own-profile Chrome. `upload`, `newtab` and `closetab` work only in the two cdp modes.
Cost is not assumed: measure a pass in each mode with `session_cost.py` before quoting numbers.
### Passing a module with a subagent
`webscope --agent` exports `screenpass`. It passes the list, the forms and every sub-route as bounded tasks, reports what was tested and what was not, and cleans up only
through the screen. Two actions keep a pass short: `tabpass` runs a whole table of rows (add, fill, edit, remove, save, read back) in one call, and `purge` deletes every
listed row that starts with a test prefix, whatever number of confirmations the screen asks. The agent never touches the database: leftovers (logical deletes, child rows) are listed
in its report for the main session.
### Simulating server errors
`test(action="simulate", tab, args=["POST /api/items", 403, "press", "Guardar"])` runs a page action while the requests that match the route (the method is optional) are answered by webscope with that HTTP status, or as a network failure when the status is `0`. The server never receives them; interception ends with the call. It shows how the screen reacts to an error; it does not prove that the backend enforces a permission.
### Observing what the page does
`test(action="observe", tab, args=["press", "Guardar"])` runs a page action and returns its result plus everything the page did meanwhile: each fetch or XHR with its status, the body sent and the reply (cut at 2000 bytes), network failures, console errors and warnings, browser log errors and uncaught exceptions. It needs nothing installed in the page, so it also sees calls made before the helpers load. It watches one call; to cover a whole module in one go, put the steps in a single `steps` call and observe that.
### Icon-only row buttons
`inventory` lists a row button with no name (no label, title, text or `mdi-` class, such as an SVG icon) as `#N`, its position in the row, and `row` accepts `"#N"` to press it.
Ask for the tool with `WEBSCOPE_TEST=1` and set the app's base URL (`project.base_url` in the manifest or `WEBSCOPE_BASE_URL`); the tool drives only tabs whose URL is under it and refuses any other. `WEBSCOPE_CDP` changes Chrome's address (default `http://127.0.0.1:9222`). Then `test(action="tabs")` lists the tabs and the other actions take `tab` and `args` (a JSON array). The actions (`state`, `fill`, `press`, `crud`, `matrix`, `inventory`, `emptySubmit`, `testRequired`, `menu`, `row`, `steps`, `upload`, `payloads`, `network` and more) are listed in the resource `webscope://test/actions`. They install the same page helpers as the pack's connector (`window.__f` and the `window.__e2e` capture), so a form is filled by its visible label and a whole create-edit-delete cycle is one `crud` call. Actions marked in the catalog change data: use test data with a `ZZ_` prefix.
`newtab`, `newwindow` and `closetab` open and close tabs in that same Chrome, so a pass does not borrow your working tab. A tab opened this way shares your cookies and login (the session you already have in the debugging profile); `newtab` with `"isolated"` opens it in a new browser context with no session, to check a logged-out page or to sign in as a test user without touching yours. The URL must be under the base URL, at most 5 such tabs stay open, `closetab` refuses any tab the tool did not open, and a shared tab on a base URL that is not this machine needs `project.allow_remote: true`.
Acceptance (21 Oct 2026): a haiku session with only `mcp__webscope__test` filled and saved the form fixture in 9 turns for 0.011 USD (`bench/acceptance_test_tool.sh`).
## Chrome connector of the Laravel pack (`e2e_chrome.py`)
The pack also ships the earlier Python connector, which runs the same helpers from a script and adds route walks, replay and Playwright export:
### Native Chrome Connector (`--evaluate`)
Runs JavaScript in a tab of a Chrome that you control, through the Chrome DevTools Protocol (CDP), so logins and 2FA survive between runs. It needs a Chrome started with remote debugging **and its own profile**: from Chrome 136 the debugging port is ignored on the default profile, so you sign in once in a dedicated profile that then keeps its cookies. It does not attach to your everyday Chrome window.
```sh
# macOS
open -na "Google Chrome" --args --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-webscope"
# Linux
google-chrome --remote-debugging-port=9222 --user-data-dir="$HOME/.chrome-webscope"
# Windows
chrome.exe --remote-debugging-port=9222 --user-data-dir=%USERPROFILE%\.chrome-webscope
```
A new profile opens Chrome's welcome page, not your app: open the app in a tab yourself and sign in. Set the app's base URL once (`project.base_url` in the manifest or `WEBSCOPE_BASE_URL=http://localhost:5173`); without it nothing runs. Another debugging port: `WEBSCOPE_CDP_PORT`. A raw script takes the tab as its last argument (`--evaluate "<js>" localhost:5173`), the sub-commands take `--url` (`--evaluate --map --url localhost:5173`); neither ever runs in «the active tab». If the port does not answer, the command prints the launch lines above and stops: it never falls back to your normal Chrome (the AppleScript fallback was removed on 22 Oct 2026).
Checked on 2026-10-20 on macOS with Chrome 155 and a new profile (before the AppleScript fallback was removed): the port answered, a raw `--evaluate` and `--evaluate --map --url` ran over CDP against a local app (the signed-out login page), and after `--evaluate --forms <route>` the helper `window.__f.state()` returned the buttons of the login page. `__f` is not there until a sub-command installs it. Not checked: Linux, Windows, a signed-in session through the new profile.
`--tabs` lists the open tabs (title and URL, read-only) and `--open <url>` focuses the tab already at that URL or opens one; the URL must be under `project.base_url`. Without `--url`, the tab is found from `project.base_url`: exactly one tab must match, otherwise the command lists them and stops. `sh docs/chrome-cold-start.sh` repeats the whole path on a throwaway profile (5 steps, checked on 2026-10-20 with Chrome and CDP; not re-run after the AppleScript fallback was removed).
What sets it apart from other browser tools is not the session but what runs in the page:
```sh
# Install the helpers (`__e2e` capture and `__f`) in the tab. Give it one route: without routes it walks every route
# of the app, which can take longer than the 120 s default
python3 packs/laravel/e2e_chrome.py --evaluate --forms /demo/auth/login --url localhost:5173
# Then inspect the current screen; a raw script takes the tab as its last argument
python3 packs/laravel/e2e_chrome.py --evaluate "window.__f.state()" "localhost:5173"
# Run a complete JS test flow from a file
python3 packs/laravel/e2e_chrome.py --evaluate test_flow.js "localhost:5173"
```
### Key Capabilities
- **Screen & Form Helpers (`__f`)**: Finds inputs by visible label, fills values, coordinates clicks with active settle wait (`calma()`), and verifies persistence.
- **Network Capture (`__e2e`)**: Intercepts `/api/` calls, latency (`ms`), and console errors without screenshots.
- **Self-Healing Across Reloads**: Injected helpers tolerate full page reloads (`window.location.reload()`) with automatic re-initialization.
- **Zero-Token Replay (`--replay <name>`)**: Replays recorded user flows in one call with 0 AI tokens.
- **Playwright Export (`--export <capture> <url>`)**: Converts recorded steps into clean `.spec.js` Playwright tests.
### The Essential Testing Contract: Full Lifecycle (CRUD + Sub-modules)
A module is **never considered tested** by merely reading a view or clicking a button. Webscope enforces a strict full-lifecycle standard:
1. **Create (C)**: Fill mandatory and conditional fields, submit, verify HTTP 200/201, and capture the generated record ID.
2. **Edit (U)**: Reopen the record, mutate fields, and verify data persistence in both UI and database.
3. **Exercise Sub-modules & Lifecycle**: Navigate sub-routes (`:id`), exercise child panels/tabs (e.g. characteristics, tariffs, pre-settlements, notes, print), and trigger state transitions (e.g. approve, reopen, duplicate).
4. **Delete & Clean (D)**: Delete the test record, confirm UI removal, and verify database clean state (`ZZ_E2E`).
---
## Configuration (`webscope.json`)
All behavior is project-contained. See [`webscope.example.json`](webscope.example.json).
```json
{
"root": ".",
"trim": { "head": 10, "tail": 70 },
"token_limit": 5000,
"storage": ".agents/webscope",
"test_prefix": "ZZ_E2E",
"search": { "dirs": ["src"], "extensions": ["ts", "vue", "php", "py"] },
"allowed_env": ["DATABASE_URL"],
"scripts": {
"report": {
"cmd": ["python3", "-I", "scripts/report.py"],
"description": "Summarises a database table",
"usage": "<table>",
"rejects": ["--out"],
"writes": ["--fix"],
"timeout": 120
}
}
}
```
| Key | Description |
| :--- | :--- |
| `root` | Project root relative to manifest directory. |
| `extra_roots` | Folders outside `root`, relative to the manifest directory, that `search` and `read` may also read (for example a logs folder). Secret files stay blocked. |
| `token_limit` | Hard limit on response size in approximate tokens. |
| `trim` | Default head (`head`) and tail (`tail`) lines kept from script outputs. |
| `storage` | Internal storage for `module-map.json`, `flows/`, `walkthroughs/`. Autodetects `.agents/webscope/`, `.claude/webscope/`, or `.webscope/`. |
| `test_prefix` | Prefix for test entities (default `ZZ_E2E`). |
| `search.dirs`, `extensions` | Folders and extensions searched by default. |
| `allowed_env` | Environment variables passed to child scripts. |
| `redact` | Optional list of regular expressions; every match in a script's output is replaced by `[redacted]`. |
| `scripts.<name>` | Fixed command list (`cmd`), `rejects` (blocked flags), `writes` (write flags). |
### The `project` section
Unknown keys in `project` (a typo such as `base_urll`) are reported on stderr when the server starts and by `webscope --doctor`, which also checks that the manifest parses, that each script file exists, that `docker` or `php` are installed when the scripts need them, and whether a Chrome answers on the debugging port.
The included adapters read the `project` section (container, boot code, module folders, frontends, audit settings…). Every key is optional and has a default. Each one is documented, with its default, who uses it and a minimal example, in [`docs/project.md`](docs/project.md).
Database adapters run inside a transaction that is rolled back. `sql` also puts the database session in read-only mode, so the database itself refuses writes. No adapter commits: `tinker` always rolls back and `cleanup_test_rows` only lists. Test data is deleted through the app's own screen; a screen that cannot delete it is a defect to report.
---
## Included Adapters (`{adapters}`)
Ready-made scripts of the Laravel pack (`packs/laravel/`). Use `{adapters}` in `cmd`; it resolves to the manifest's `adapters_dir`, by default `packs/laravel` beside the manifest:
Grouped by the stack they need. The core tools (`search`, `read`, `run`) need none.
**Any stack**
| Script | Purpose |
| :--- | :--- |
| `e2e_chrome.py` | Browser walk, form automation (`__f`), replay, and open Chrome connector (`--evaluate`). The route walk needs a Vue 3 router; other frameworks are not covered. |
| `triage.py` | Classifies failing pieces of a run summary as known, CHANGED or NEW (`project.known_failures`). |
| `session_cost.py` | Token and USD cost auditor per model for Claude Code sessions. |
| `agent_usage.py`, `webscope_usage.py`, `session_state.py` | Subagent usage, webscope usage and session bootstrap for Claude Code. |
**Laravel** (a database adapter needs `project.container` or `project.workdir`)
| Script | Purpose |
| :--- | :--- |
| `laravel.py` (`map_back`) | Resolves endpoint → controller → service → model → database table. `--required-static` compares the form code with the Request's required rules. |
| `form_options.py` (`options`) | For each select of a module's forms: store list → endpoint → controller → service → table and owning module; a list it cannot follow is reported as unresolved. `--count` adds the real rows (read-only SQL) and warns when a select will be empty. Laravel + Vue with a `fetchList`-style store. |
| `test_plan.py` (`plan`) | Ordered test plan for a Vue module: its kind (crud, actions, report, wizard) deduced from the code, the test types that apply with the webscope call for each, and which ones write. A heuristic; `--kind` forces it. |
| `tinker.py` | Runs PHP in `artisan tinker` inside a transaction rolled back unless `--commit`; `project.boot` prepares the connection. |
| `sql.py` | Read-only SQL (one SELECT/SHOW/DESCRIBE/EXPLAIN/WITH) through `tinker.py`, in a read-only database session; prints TSV with a header. |
| `fields.py` | Column sheet of a table: type, nullability, rows with a value, backend readers and writers. |
| `consumers.py` | Who reads and who writes a column: `file:line` in the backend (and `project.legacy`). |
| `unique_indexes.py` | Unique indexes on tables with a status column and whether the backend covers each (`project.status_columns`). |
| `tenant_queries.py` | `DB::` queries on tenant tables without an explicit connection (`project.tenant_tables`). |
| `snap.py` | Snapshot and diff of a query: what an on-screen action really wrote. |
| `db_check.php` | SELECT-only database integrity checks by prefix marker from `.env` (MySQL / Postgres). |
| `payload_check.py` | Compares captures against FormRequest rules, Resources, and columns. |
| `cleanup_test_rows.py` | Lists (read-only) the test rows matching the test prefix. |
**Laravel + Vue**
| Script | Purpose |
| :--- | :--- |
| `module_graph.py` (`graph`) | Screen → frontend module → endpoints → backend module → tables (`project.frontends`, `project.stores`). |
| `audit_types.py` | Crosses columns with FormRequest rules, Resource casts and frontend numeric conversions, with real data. |
| `simulate.py` | Lists what could break in a module from request, form and schema; `--out` writes the walk spec. |
| `changed.py` | Specs affected by uncommitted changes, resolved to screens (`project.repos`, `project.spec_files`). |
**Vue 3 + TS**: `vue_ts.py` (`map`) maps routes, subroutes, clickable actions, API calls and E2E coverage.
**Astro**: `astro.py` (`map_web`) maps file-based routes, components, form fields and client scripts.
---
## Subagents Integration
Webscope keeps subagents light. Two separate numbers:
- **The server itself** costs about 1.6k tokens per session: `tools/list` plus `instructions`, measured 2026-10-20 with a 23-script manifest (the adapters it declares plus the project's own scripts) and with the 25-script `webscope.example.json`. It does not grow with the number of included adapters: they are not in `tools/list`; only the scripts your manifest declares add a short line to `instructions`.
- **A subagent** that declares `tools:` in its frontmatter loads about 9.3k tokens at start, against 28.8k when it loads every tool and skill name. That number depends on how many tools the agent declares, not on webscope's adapters.
### Exporting the `walkthrough` and `screenpass` Agents
Export the ready-made agents without overwriting existing ones. `walkthrough` walks a module with the pack's scripts; `screenpass` passes one screen with the `test` tool only (inventory, validation, create and edit, the API calls of each save, simulated server errors) and keeps the agent inside the project instead of your global agents:
```sh
# Autodetects .agents/ or .claude/agents/
bin/webscope --agent
# Or targeting a specific folder
bin/webscope --agent [directory]
```
- **Non-destructive**: Never overwrites existing agent files unless `--force` is passed.
- **Isolation**: Leaves all other user-defined agents untouched.
### Declaring Tools in Agents
In your `.agents/<name>.md` or `.claude/agents/<name>.md`:
```yaml
---
name: browser
tools: Read, Grep, Glob, mcp__webscope__search, mcp__webscope__read, mcp__webscope__run
model: sonnet
effort: high
---
```
---
## Configuration in Other Clients
### Claude Desktop (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"webscope": {
"command": "/absolute/path/to/bin/webscope",
"args": ["-manifest", "/absolute/path/to/webscope.json"]
}
}
}
```
### Cursor / VS Code / Cline (`.cursor/mcp.json` or `.vscode/mcp.json`)
```json
{
"mcpServers": {
"webscope": {
"command": "/absolute/path/to/bin/webscope",
"args": ["-manifest", "${workspaceFolder}/webscope.json"]
}
}
}
```
### Zed (`settings.json`)
```json
{
"context_servers": {
"webscope": {
"command": {
"path": "/absolute/path/to/bin/webscope",
"args": ["-manifest", "/absolute/path/to/webscope.json"]
}
}
}
}
```
---
## Updating
Rebuild (`go build -o bin/webscope ./cmd/webscope`) or replace the binary, then restart the client.
---
## Security Model
- **Root Sandbox**: `search` and `read` resolve strictly within `root` and reject outside paths.
- **Invisible Secrets**: Automatically hides `.env*`, `*.pem`, `*.key`, `*.p12`, `id_rsa*`, `id_ed25519*`, `auth.json`, and credentials.
- **No Shell Injection**: Scripts execute as fixed argument lists without intermediate shell.
- **Read-Only Default**: Destructive arguments (`writes`) require `run_write`, `WEBSCOPE_WRITE=1`, and explicit approval.
- **SQL guard (`sql.py`)**: a regex accepts one statement starting with SELECT, SHOW, DESCRIBE, DESC, EXPLAIN or WITH and rejects `;`. It is a convenience filter, not a parser: `WITH ... DELETE` passes it. The real guard is that the database session is read-only (`SET SESSION TRANSACTION READ ONLY` on MySQL/MariaDB, `default_transaction_read_only` on PostgreSQL, `query_only` on SQLite) inside a rolled-back transaction. Verified on MySQL: a `WITH ... DELETE` fails with «Cannot execute statement in a READ ONLY transaction». Not covered by rollback: file output such as `SELECT ... INTO OUTFILE`, locks and sleeps, PostgreSQL sequences and non-transactional engines. For a hard guarantee, connect with a database user that only has SELECT.
- **`allowed_env` is a deliberate escape.** Scripts get only `PATH`, `HOME`, `LANG`, `LC_ALL`, `TMPDIR` and the variables you list in `allowed_env` (for example `DATABASE_URL`). Those values are real, and the output of a script goes back to the agent. Webscope does **not** redact script output: «Invisible Secrets» applies to `search` and `read` only. A script that dumps its environment, or a stack trace that prints a DSN, reaches the agent. Declare only what a script needs. Only `cleanup_test_rows.py` masks, on its own, the values of columns that look like secrets (`pass`, `token`, `secret`, `clave`).
- **`tinker.py`** only runs PHP files from the system temp folder or the project's storage folder, never arbitrary project code.
### Recommended `.gitignore`
```gitignore
# Temporary Webscope artifacts
captures/
.webscope/captures/
```
Commit `webscope.json`, `.claude/webscope/walkthroughs/` (or `.agents/webscope/`), and `module-map.json`.
---
## Tests
```sh
go vet ./... && go test -race ./... # server, browser tests need Chrome (WEBSCOPE_REQUIRE_CHROME=1 fails instead of skipping)
python3 -I packs/laravel/test_pack.py # the pack's adapters
sh fixtures/laravel/run-engine-checks.sh # Docker: MySQL, PostgreSQL and SQLite
release/docker_check.sh # Docker: the image answers over stdio
```
## License
[MIT](LICENSE)
TDQS
Scored across 3 tools
search, read, and run have clearly distinct roles—locating text, reading file content, and executing read-only scripts—and the descriptions explicitly steer agents away from overlap (e.g., 'To see the code use read', 'use it instead of grep'). No tool pair does the same job.
All tools use a single lowercase verb (search, read, run), a consistent and predictable convention. There is no mixed casing or noun/verb style inconsistency.
Three tools is well-scoped for a focused read-only code exploration server; each tool is distinct and earns its place. The count is not thin because each tool packs several related capabilities (read covers symbols, ranges, sections, logs, and index).
The surface covers text search, file/symbol/range reading, and read-only script execution for codebase exploration. A minor gap is the lack of a dedicated filename/glob search, though read's file index and search partially cover that need.