request-finder
Netscribe
A Chrome (Manifest V3) extension that captures network traffic across your tabs and lets you search it with a query language — without opening DevTools.
Netscribe was formerly Request Finder — they're the same project. The old name is still the internal identifier throughout, and that's deliberate: it's what the MCP server is registered as and where the daemon keeps its state, so renaming it would break existing setups. Wherever you see
request-finder, read Netscribe:
Where
Value
MCP server id
request-finder(inclaude mcp add)Daemon state
~/.request-finder/(token,history.db, logs)npm package / scripts
request-finder,npm run bridge:uninstallExtension name in Chrome
Request Finder
What it captures, across every tab, all the time:
HTTP API calls — XHR/fetch and ordinary page loads: URL, method, status, headers, and timing.
GraphQL — captured like any other HTTP call (typically
POST /graphql). Under Deep Capture its query and variables are searchable body text, sobody:will match an operation name.RPC — JSON-shaped RPC (JSON-RPC, tRPC, Connect in JSON mode) is captured in full, bodies included. Protobuf-shaped RPC (gRPC-Web, Connect in proto mode) is captured too — URL, status, headers, timing — but its body is binary protobuf, so
body:won't match it.WebSockets — the handshake plus the individual frames sent and received (ADR 0006), including binary ones. RPC tunneled over a socket is searchable frame by frame.
Ask Claude about a request. With the bridge daemon running, Netscribe exposes your captured traffic to Claude Code over MCP — so you can ask "why did this call 401?" or "what changed between these two requests?" and Claude can read the real headers and bodies instead of guessing. Each request has a Request Ref you can paste to point Claude at one exact call. See Claude Code integration below.
The search UI lives in a dedicated full-tab page — click the Request Finder toolbar icon to open it.
Privacy note: Netscribe records request/response headers (including
Authorization,Cookie, API keys) and — under Deep Capture — request and response bodies, in plaintext IndexedDB on your machine. It is a personal debugging tool for your own traffic. Use the Discard list (domains never captured — the button right of Purge all), Pause, and Purge all guardrails, and the secret-masking toggle. Purge all clears the extension's history and, if you tick the box, the Claude/MCP daemon's history too (otherwise the daemon keeps its own copy — ADR 0008). Seedocs/adr/0003.
Install & build
A Makefile wraps the npm scripts, so building the extension is one command:
make # fresh build → dist/, ready to load unpackedmake wipes dist/, installs dependencies if a lockfile has moved, then runs
the typecheck and production build. Other targets (make help lists them all):
make dev # Vite + CRXJS watch build
make test # Vitest unit suite (parser, evaluator, index, IDB)
make test-e2e # Playwright smoke (builds first — it needs dist/)
make check # unit suite + a fresh build
make clean # drop build output; `make distclean` also drops node_modulesThe underlying npm run build / npm test scripts still work if you prefer them.
Load the unpacked extension: chrome://extensions → Developer mode → Load
unpacked → select dist/.
Capture model
Path | API | Scope | Captures | Cost |
Metadata Capture |
| all tabs, always on | URL, method, headers, status, timing | silent |
Deep Capture |
| opt-in, per tab | the above + request/response bodies | shows a debugging banner; can't coexist with DevTools |
body: and response: only match Deep-Captured requests and degrade
gracefully on the rest (ADR 0001). Storage is a ring buffer (default 50k,
configurable) — the oldest request is evicted past the cap (ADR 0002).
Query language
method:POST has:authorization # space = AND
status:500 OR status:401 # explicit OR (binds looser than AND)
domain:slack.com -status:200 # '-' negates
(status:500 OR status:401) method:POST # parentheses groupOperator | Meaning |
| header present (request or response) |
| header present (alias of |
| header value contains |
| exact method |
|
|
| substring of the full request URL |
| substring of the page URL the request fired from (live SPA route) |
| host or any subdomain ( |
| cookie present in the request |
| substring of the originating tab's title (as captured) |
| WebSocket frames (any / sent / received) or HTTP records |
| substring of the request body (HTTP, Deep Capture only) or a sent WS frame's payload (always captured) |
| substring of the response body (HTTP, Deep Capture only) or a received WS frame's payload (always captured) |
bare word | substring across URL, headers, bodies, and frame payloads |
WebSocket messages are captured by default — a content script wraps
window.WebSocket (ADR 0006/0007), so frames are recorded with no debugger, no
banner, and even while DevTools is open. Deep Capture is not involved in
WS frames at all (it stays HTTP-only, for response bodies).
The Finder Page has a top-level Requests | WebSockets switch. The
WebSockets view shows frames only — ▲ WS sent / ▼ WS received — and the
search box takes the same operators: response:"chat_message" (received
payload), is:sent, domain:hiver.space, tab:outlook. The handshake itself
appears in the Requests view as a websocket request (recorded at the
101, since a live socket never "completes").
The content script must be in place before the page opens its socket, so reload the page once after installing/reloading the extension. Frames have their own ring-buffer budget (
maxFrames, default 100k) so a chatty socket can't evict HTTP history (maxRequests, default 50k).Heartbeat frames (JSON
{"type":"ping"}/"pong") are dropped at capture by default to cut noise and save the frame budget. The WebSockets toolbar has a Record pings checkbox to keep them.Binary frames are size-only by default. The Log binary checkbox captures their payloads — decoded as text when the bytes are valid UTF-8 (so text-as-Blob stays searchable), else base64 — capped at
maxBodyBytes. When off, the bytes aren't even read/encoded.
Matching is case-insensitive. Use "quotes" for values with spaces.
Architecture
service worker ── webRequest / debugger ──▶ IndexedDB (source of truth, ring buffer)
│ ▲
└── live feed (port) ──▶ Finder Page ── load ┘
│
├─ Web Worker: in-memory index (URL + headers
│ + structured fields), evaluates queries.
│ Body-dependent matches come back as
│ "needs body scan" …
└─ … which the page resolves (Pass 2) by
reading full records from IndexedDB.The query evaluator is one three-valued (Kleene) function run twice: in the
Worker against in-RAM data (body text → MAYBE), then against full records from
IndexedDB to resolve the MAYBEs.
Layout
src/
background/ service worker: webRequest + debugger capture, batched IDB writes
shared/ types, IndexedDB layer, query engine (tokenizer→parser→evaluate)
worker/ in-memory RequestIndex + Worker entry
finder/ React full-tab UI (search, virtualized list, detail panel)
docs/adr/ architecture decision records
CONTEXT.md glossary (ubiquitous language)Claude Code integration (bridge daemon)
A small always-on local daemon (bridge/) lets Claude Code watch traffic
live, query history, and act (toggle Deep Capture, pause, purge) over MCP.
The extension streams captures to it over a localhost WebSocket; the daemon
keeps its own durable SQLite history and exposes an MCP server. See ADR 0004.
extension SW ═══WS═══▶ bridge daemon ◀═══MCP/HTTP═══ Claude Code
◀══WS═══ (actions: deep-capture, pause, purge)Setup
cd bridge && npm install # native better-sqlite3 build
cd .. && npm run bridge # starts the daemon on 127.0.0.1:8787The daemon prints a token on first run (and on every start; it's stable,
stored at ~/.request-finder/token). Then:
In the Finder Page → ⚙ Bridge → paste the token, set the URL (
ws://127.0.0.1:8787/ws), and check Enable bridge.Register with Claude Code (the daemon prints this exact line):
claude mcp add --transport http request-finder http://127.0.0.1:8787/mcp \ --header "Authorization: Bearer <token>"Or use the committed
.mcp.jsonwithexport RF_BRIDGE_TOKEN=<token>.
MCP tools
Tool | What it does |
| search history with the query language (newest-first) |
| full record by Request Ref |
| cursor long-poll for live watching (call in a loop) |
| tabs the extension sees + Deep Capture state |
| counts, time range, connection + Deep Capture status |
| turn Deep Capture on/off for a tab (to see bodies) |
| pause/resume Metadata Capture |
| wipe daemon + extension history ( |
Security (ADR 0004): loopback bind only, a shared token on both the
WebSocket and every MCP call, and an Origin check that rejects web-page
WebSocket connections. The daemon serves captured secrets — keep the token
private; history_purge clears everything.
Always-on (macOS LaunchAgent)
Instead of npm run bridge each time, run the daemon at login and keep it
alive (deploy/com.requestfinder.bridge.plist, launched via
bridge/run-daemon.sh which resolves your nvm Node):
cp deploy/com.requestfinder.bridge.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.requestfinder.bridge.plist # starts now + every login
# stop/uninstall:
launchctl unload ~/Library/LaunchAgents/com.requestfinder.bridge.plistLogs: ~/.request-finder/daemon.log (and daemon.err.log). On another machine,
edit the two absolute paths in the plist.
The watch-requests skill
.claude/skills/watch-requests/SKILL.md teaches Claude Code the common
workflows over these MCP tools — "tail any 5xx on the gmail tab", "find the
auth-bearing calls to api.slack.com and show me one", "deep-capture tab 42 and
inspect the failing response". It activates automatically when you ask for that
kind of thing (the request-finder MCP server must be registered).
Running it day-to-day
Part | When | How |
Extension | always (whenever Chrome runs) | Load unpacked from |
Bridge daemon | only for Claude | LaunchAgent (above) runs it at login; or |
Claude registration | once |
|
Uninstall / cleanup
Two separate stores:
Extension data — cleared automatically by Chrome when you remove the extension (its IndexedDB + settings). Nothing to do.
Bridge daemon — a separate local process + SQLite at
~/.request-finder, independent of the extension; uninstalling the extension does not touch it. The extension can't purge it on uninstall (no code runs then). Clean it one of two ways:Before uninstalling: Purge all → tick Claude/MCP (daemon) history.
Any time:
npm run bridge:uninstall— removes only Netscribe's own artifacts: its LaunchAgent, its running daemon (only if the process on :8787 is Netscribe's), and~/.request-finder(history.db, token, logs). It touches nothing else.