Run it for real

Run this on your config

The walkthrough below is an illustrative example. To get a real, config-scoped cross-server toxic-flow analysis for the servers you actually run together, paste a config (or a launch command) below — the form also takes a live URL or a GitHub/npm source, though the cross-server flow attaches only when the input resolves to a multi-server config. In that case we request the runtime-observed cross-server flow (observe_cross_server_flow) so the report can include the static reader→sink join, the multi-step attack-graph synthesis, and — in a sandbox — a genuinely observed flow. You land on /scan/:id, which shows the result as it completes.

Paste your MCP client config. Remote entries are connected to; stdio entries are resolved to their published package and scanned.

Both kinds of entry are scanned. A url entry is connected to over MCP; a command entry is resolved to the npm or PyPI package it names and the published artifact is scanned. Any entry that cannot be resolved is listed in the report with the reason. env values are never read.

Rule scope

Full registry scan — all 183 active rules

Example of what a run produces

The composer and report below are a hand-authored design preview (the fixture is flagged illustrative: true) — a fixed three-server scenario, not a live scan. It shows what a real run looks like; the picks and the one “observed” edge are illustrative. Run the real thing above.

Cross-Server Toxic Flow · exampleIllustrative preview
Toxic flow found · config-scoped — not a server score
Three individually-clean MCP servers, wired into one agent, compose an exfiltration path: an untrusted source feeds a private reader whose output can reach an external sink. No single server is misconfigured — the risk lives in the composition, mediated by the agent.
Config-scoped · agent.mcp.json (illustrative)

Let MCP Sentinel choose. The scoped flow-driver agent (ADR-012) selects the participants to surface the highest-risk route. In this preview the agent’s picks are illustrative.

3 selected3 on route2 edges
Flow graphServer lanes · role-coloured tools · agent-mediated edges
Illustrative
github-mcp tool get_issue. Role SOURCE. returns issue title + body as text. attacker-controllable issue body. on the highlighted toxic route.SOURCETOOLget_issuereturns issue title + body as textattacker-controllable issue bodygithub-mcp tool list_repos. Role SOURCE. lists repositories (metadata only). not selected.SOURCETOOLlist_reposlists repositories (metadata only)filesystem-mcp tool read_file. Role READER. reads a file by path. ~/.ssh/id_rsa. on the highlighted toxic route.READERTOOLread_filereads a file by path~/.ssh/id_rsafilesystem-mcp tool list_dir. Role READER. lists directory entries. not selected.READERTOOLlist_dirlists directory entriesslack-mcp tool post_message. Role SINK. posts a message to a channel. external channel — leaves the trust boundary. on the highlighted toxic route.SINKTOOLpost_messageposts a message to a channelexternal channel — leaves the tru…slack-mcp tool list_channels. Role SINK. lists channels (metadata only). not selected.SINKTOOLlist_channelslists channels (metadata only)github-mcp resource issue_comments. Role SOURCE. exposes issue comment threads (untrusted text). another untrusted-ingress surface. not selected.SOURCERESissue_commentsexposes issue comment threads (untr…another untrusted-ingress surfacefilesystem-mcp resource workspace_root. Role READER. exposes the configured workspace root. read scope for the server. not selected.READERRESworkspace_rootexposes the configured workspace ro…read scope for the serverslack-mcp resource channel_directory. Role SINK. lists channels the token can reach. egress targets. not selected.SINKRESchannel_directorylists channels the token can reachegress targets
  1. github-mcp.get_issue (SOURCE) filesystem-mcp.read_file (READER). Provenance: Observed, agent-mediated. DESIGN PREVIEW of what an "observed" edge renders like — NOT a record of a real execution of THIS flow. An observed edge is now reachable from a real producer: the Wave 3 executed-behavior producer (engine.ts `ExecutedBehavior`) emits `provenance: "observed"` for a tool it actually ran in the ADR-007 T3 sandbox, and contract.ts `fromSandboxExecutedBehavior` maps that to an observed edge — a single tool observed reading private data and egressing off-box in one invocation, measured from the host side of the jail. This particular source→reader chained leg is instead hand-authored to preview the observed state; the static-analyzer cross-server join emits only `inferred_binding`. Read this edge as a design preview, never as a claim that this specific data movement was witnessed by a live scan.
  2. filesystem-mcp.read_file (READER) slack-mcp.post_message (SINK). Provenance: Inferred binding, agent-mediated. post_message targets a live third-party Slack endpoint. Under ADR-007 T1 we never invoke tools/call against a third party's live server, so this leg was WITHHELD. read_file's output is schema-compatible with post_message's `text` parameter and the agent has both tools in scope, so the binding is INFERRED from capability, not observed.
Source Reader Sink
Observed (solid)Inferred binding (dashed)
critical · config-scoped
Cross-server exfiltration path: GitHub issue → SSH key → Slack
SOURCE
Untrusted Source
github-mcp.get_issue
returns issue title + body as text
attacker-controllable issue body
READER
Observedissue body → file path
Private Reader
filesystem-mcp.read_file
reads a file by path
~/.ssh/id_rsa
SINK
Inferred bindingfile contents → message text
Egress Sink
slack-mcp.post_message
posts a message to a channel
external channel — leaves the trust boundary
Mapped toMCP04ASI07AML.T0057AML.T0054.001
ExploitabilityModerate — requires preconditions. Requires the agent to hold all three tools in one session and to act on the injected instruction. The first leg is shown in the observed state as a design preview (hand-authored, not from a live scan — see the edge note); the sink leg is inferred, so full end-to-end exploitation is not proven — only its precondition binding.
An attacker files a GitHub issue whose body carries an indirect prompt-injection instruction. When the agent reads that issue via get_issue (untrusted source), the injected text steers it to call read_file on ~/.ssh/id_rsa (private reader) and then post_message the contents to an external Slack channel (external sink). No single server is misconfigured — get_issue is a normal reader, read_file honours its root, post_message posts where it is told. The exfiltration lives in the COMPOSITION, mediated by the agent.
Mitigationbreaks chain
quarantine untrusted-source output before it can reach a private reader on github-mcp. Route get_issue output through a content-firewall / data-labelling boundary so external text cannot flow into a filesystem-read argument in the same agent turn. Breaking the source→reader leg collapses the whole chain, since the sink leg has nothing private to carry.
Per-server intrinsic scoresEach server, scored on its own — clean. Two units, never mixed.

The toxic flow is a property of the configuration, not of any one server. These are the servers’ own intrinsic scores, exactly as the registry shows them in isolation — the config-level finding does not deduct from any of them.

88
GitHub MCP · Good
Clean in isolation. get_issue returning external text is expected behaviour; the risk only exists once a private reader and an external sink share its agent.
SOURCE leg
74
Filesystem MCP · Moderate
Clean in isolation. read_file honours its configured root; reading a path the agent supplies is its job. It becomes the reader leg only when fed untrusted source text.
READER leg
91
Slack MCP · Good
Clean in isolation. post_message posting to a channel is normal. It is the external sink leg only when the message body originates from a private reader.
SINK leg

Read the two units apart: a per-server score answers “is this server safe to install?”; the config verdict answers “is this combination safe to wire into one agent?” A clean server can still be one leg of a toxic flow.

Coverage ledgerWhat we actually executed vs. what we withheld.
Executed
get_issue → read_file
Executed in a measured egress-denied sandbox (ADR-007 T3). Flow directly observed.
Withheld (safety)
read_file → post_message
Not executed — the binding is inferred (external sink withheld under ADR-007 T1, or the pairing is outside the executed trace).
1
Executed
1
Withheld
0
Skip-gap
Upper bound — certainty, not severity
This assessment is an upper bound on certainty, not on severity. At least one leg was inferred rather than executed (an external sink is withheld for safety under ADR-007 T1). Consented dynamic testing (ADR-007 T2) against the operator's own endpoint could confirm or refute it; nothing here can raise certainty without that.

Provenance summary: Illustrative: the source → reader leg is shown in the observed state as a design preview (hand-authored, not from a live scan — see the edge note). The sink leg is an inferred binding — withheld for safety, not confirmed. Final leg: Inferred binding.

Observed vs. inferred — what the labels mean

ObservedObserved

An observed edge is drawn solid (with a subtle flow animation). It means we executed the step and watched the data move: the artifact ran inside a measured, egress-denied sandbox (ADR-007 T3), the upstream tool’s output reached the downstream tool’s argument, and the call fired against our own copy in our own container.

It is evidence of a real flow, not a guess. Only executed steps earn this label.

Inferred bindingInferred binding

An inferred binding edge is drawn dashed. The downstream tool is a live third-party sink, so we withheld it: ADR-007 T1 forbids tools/call against a stranger’s server. The binding is reasoned from schema compatibility and shared agent scope — it is not observed.

It marks a plausible, unconfirmed leg. We label it as inference precisely so a withheld step is never quietly presented as a proven one.

Two things we never do. A safety-withheld sink leg is never redrawn as “observed” to make a cleaner story. And registry-wide co-existence of capable servers is never drawn as an attack path — this view is config-scoped: it only reasons about servers that share one client configuration, never “these two servers both exist, therefore risk.”