Reading the evidence¶
Every finding scout reports cites the request that produced it. This page is about following that citation — opening the wire log, finding the exchange, and reading what actually crossed the network.
It matters more than it sounds. An independent audit in 2026 put false positives from pattern-matching MCP scanners at roughly four in five. The answer to "is this finding real?" should not be "the tool said so".
What a run writes¶
Point --report-dir at a directory and a run leaves seven files:
scout check https://mcp.example.com/mcp --report-dir ./scout-report
| File | What it is |
|---|---|
report.json |
The verdict, machine-readable. Findings, scores, phases, counts. |
report.txt |
The same thing for a terminal. |
report.md |
The same thing for a pull request comment or a ticket. |
report.html |
The document you print to PDF and hand to somebody. |
index.html |
The report again, as the directory's landing page. |
telemetry.ndjson |
One JSON object per request, in order. |
telemetry.har |
The same exchanges in HAR 1.2, for a tool that speaks it. |
The two telemetry files are the evidence. The reports are the argument.
Following a finding to its request¶
A finding in report.json carries the sequence numbers of the requests
behind it:
{
"id": "protocol.unknown_tool",
"status": "fail",
"title": "Unknown tool is reported",
"detail": "calling a non-existent tool returned success",
"requests": [31]
}
31 is the seq field in the NDJSON stream. One line, one request:
jq 'select(.seq == 31)' scout-report/telemetry.ndjson
{
"seq": 31,
"phase": "protocol",
"label": "unknown tool",
"method": "POST",
"url": "https://mcp.example.com/mcp",
"status": 200,
"rpc": { "method": "tools/call", "id": 31 },
"timings": { "dns": 0, "connect": 1200000, "tls": 8100000, "ttfb": 21400000, "total": 21900000 },
"request_headers": { "Authorization": "Bearer ***", "Content-Type": "application/json" },
"response_headers": { "Content-Type": "application/json" },
"request_bytes": 118,
"response_bytes": 64,
"trace_id": "fdc8741d05c3ed6a703ac9c5a124d086"
}
That is the whole basis of the finding: a tools/call for a tool that does
not exist, answered 200 with a result rather than -32602. You can
disagree with the verdict, but not with the exchange.
Timings are nanoseconds in NDJSON¶
timings values are time.Duration, so nanoseconds. The _ms fields in
report.json are milliseconds. They are different units on purpose: the
stream is machine-facing, the report is not.
Reading the HAR¶
telemetry.har is HAR 1.2, which means Chrome DevTools, Firefox, Charles,
Insomnia and Postman will all open it. Drag it onto the Network panel with
"Preserve log" on.
The structure is the standard one:
log
├── version "1.2"
├── creator { name: "scout", version: … }
└── entries[]
├── startedDateTime
├── time total, milliseconds
├── request method, url, headers, postData
├── response status, headers, content
├── serverIPAddress
├── timings blocked, dns, connect, ssl, send, wait, receive
└── comment the phase and label, so an entry says why it happened
The comment field is worth knowing about: HAR has nowhere to record why
a request was made, so scout puts the phase and the check's label there. In
DevTools it shows on the entry; with jq it is one field:
jq -r '.log.entries[] | "\(.comment)\t\(.response.status)\t\(.time)ms"' \
scout-report/telemetry.har
What the timings mean¶
| Field | What it measures |
|---|---|
blocked |
Time in the connection queue. |
dns |
Name resolution. 0 on a reused connection or an IP literal. |
connect |
TCP. 0 on a reused connection. |
ssl |
TLS handshake. Included inside connect, per the HAR spec. |
send |
Writing the request. |
wait |
Time to first byte — usually the server thinking. |
receive |
Reading the body. |
A wait that dwarfs everything else is the server being slow. A connect
that does is the network. Distinguishing them is the reason both are there.
What is not in the evidence¶
Secrets. They are removed structurally at the recorder, not by a pattern match on the way out:
- Headers whose names are credential-bearing —
Authorization,Cookie,Set-Cookie,X-Api-Keyand the rest — are masked by name. - Values the operator supplied are masked wherever they appear, including in a body.
- Tokens the server issued during the run are registered as they arrive and masked from that point on, so a refresh token minted at request 12 is masked at request 13.
This is why the files are safe to attach to a ticket. It is also why a
finding about a credential shows Bearer *** rather than nothing at all:
the shape is preserved so you can see a credential was sent.
See the security model for the threat model behind that choice.
Bounded, and honest about it¶
A server can answer with a gigabyte. Response bodies are capped, the recorder keeps a ring buffer, and the summary counts what it dropped rather than pretending the cap did not happen. If a run was truncated, the report says so.