Skip to content

Architecture

How passmcp works, for people changing it.

The shape of it

passmcp 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

Packages fall into layers. go list ./... and each package's doc comment are the authoritative list; this section is the map.

surfaces        cmd/ (cobra commands, flags, config precedence),
                cmd/passmcp (main), internal/tui (terminal views),
                internal/web (passmcp serve: browser app and --public mode)
orchestration   internal/engine: the one place a run is configured
                (RunSpec), validated and started, for every surface
multi-target    internal/fleet (scheduled sets), internal/watch (drift),
                internal/discover (find and prove MCP endpoints)
diagnostic      internal/probe (the phases and their Session), with
                internal/egress, canary, witness, supply, clientconf and
                diagnostics/ (policy, generated arguments, validation)
results         internal/report (model, score, renderers), attest,
                baseline, policy (gates), enrich, ocsf, otlp, a2a
support         internal/telemetry (recorder, redaction, HAR/NDJSON),
                internal/creds, config, diag, termsafe
public library  passmcp (root client), auth/, transport/, diagnostics/,
                trace/, attestation/, spec/controls; none imports
                internal/ or cmd/, and only attestation/ (which forwards
                to passmcp-reporting) goes beyond the standard library

Dependencies run one way:

cmd ──► web, fleet, watch, discover ──► engine
engine ──► probe ──► passmcp ──► auth, transport
   │         ├──► creds ──► auth, telemetry
   │         ├──► telemetry ──► trace
   │         └──► diagnostics ──► passmcp
   └──► report, attest, baseline, policy, ocsf, otlp ──► probe (Finding)

The result packages import probe only for the Finding model. discover reaches engine to validate what it found, which is the one edge that points from a source package to the orchestrator.

The run

Every surface builds an engine.RunSpec and calls engine.Run, which validates it and hands probe its options. 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 (telemetry.SensitiveHeader: Authorization, Proxy-Authorization, cookies, anything containing key, token, secret, password or auth). 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 passmcp 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.