Architecture¶
How scout works, for people changing it.
The shape of it¶
scout does one thing: it connects to a remote MCP server the way an agent would and writes down, step by step, what actually happened. Every phase makes real requests through one recorder, every finding cites the requests that produced it, and the score is derived from the findings rather than asserted separately.
Package layout¶
cmd/scout main(); nothing but a call into cmd
cmd/ cobra commands, shared flag groups, config precedence
internal/probe the nine phases and the Session they share
internal/report Report model, scoring, text/Markdown renderers
internal/telemetry httptrace recorder, redaction, HAR/NDJSON export
internal/creds credential model, env resolution, token store
internal/config flag-keyed config file with defaults and profiles
internal/diag levelled diagnostics, always to stderr
internal/tui Bubble Tea run view (phase list, report viewport), tool selector, braille logo
scout (root) public client: Connect state machine, catalog, calls
auth/ RFC 9110/9728/8414/7591/8707 + PKCE, token sources
transport/ Streamable HTTP: JSON-RPC over POST, SSE, sessions
diagnostics/ policy, schema-driven arguments, validator, limiter
trace/ per-run trace id in context and X-MCP-Trace-ID
Dependencies run one way:
cmd ──► probe ──► scout ──► auth
│ │ └────► transport
│ ├──► creds ──► auth
│ ├──► telemetry ──► trace
│ └──► diagnostics ──► scout
├──► report ──► probe
└──► config
The run¶
probe.Run builds one http.Client whose transport is wrapped by the
telemetry recorder, then wraps that again for the client: trace header,
fixed headers (API keys, basic auth), and the bearer transport. A second,
bare transport to the same endpoint carries nothing but the trace header;
the discovery and auth phases use it for the requests that must arrive
unauthenticated (first contact, the garbage-token probe). Without it, the
bearer transport would silently add the real token and a server that
accepts anything would look open.
Phases run in order. Each returns findings; a phase that discovers the
server cannot be reached, or that credentials are missing for a protected
server, sets Session.blocked and every later phase is recorded as
skipped with that reason. The report therefore never shows a category as
assessed when its phase did not run.
Generated artefacts¶
Manpages and shell completions are generated from the cobra command tree
by scripts/gen_docs.go at build time and never committed, so they cannot
drift from --help. tools.go pins cobra/doc in go.mod so the
generator resolves offline.
Findings¶
A finding is {id, title, status, severity, detail, evidence, advice}.
status is one of pass/warn/fail/skip/info. Evidence is the range of
recorder sequence numbers made while the check ran (req#12-14), so the
JSON report and the HAR file can be cross-referenced by seq.
The rule for status: pass needs a request that showed the property; info records an observation with no judgement; warn is a deviation an agent can live with; fail is one it cannot. Severity on fail drives the score: critical zeroes the category, major costs 40, minor 15; every warn costs 5.
Redaction¶
The recorder owns a Redactor. Operator-supplied secrets are registered
before the first request. Header values are masked by name policy
(Authorization, Cookie, anything containing key/token/secret). Query
parameters and form fields are masked by name. JSON bodies are masked
structurally: access_token, refresh_token, client_secret, code
and friends are replaced and their values registered, so a token issued
in the middle of a run is masked in every later event. Content types are
not trusted; a body that starts with { is treated as JSON.
Safety¶
diagnostics.Policy decides what may be invoked. By default only tools
that declare readOnlyHint: true run. The MCP specification's default for
a tool without annotations is destructive, and scout honours that: such
tools are skipped and the catalog phase warns about them.
--allow-mutations unlocks non-destructive mutations; --allow-destructive
unlocks everything and is documented as dangerous. Requests are throttled
to --rps (default 2) unless --allow-load is given for the burst.