Scan

Example scan: GitHub, files and git

This page shows output from a committed CLI run, not a live scanner. The config is built from the official MCP server READMEs. Nothing is pasted or sent from here; to scan your own config, run the CLI on your machine.

run log: docs/live-evidence/public-readmes-*.log your config: node packages/cli/dist/index.js scan --mcp <file>

Scan summary

567
leak paths
2
private resources
3
servers involved
9
grants to revoke

The config that was scanned

input config mcp-public-readmes.json
{
  "mcpServers": {
    "github": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-github"],
      "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "<YOUR_TOKEN>" }
    },
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem",
               "/Users/username/Desktop", "/path/to/other/allowed/dir"]
    },
    "git": {
      "command": "uvx",
      "args": ["mcp-server-git", "--repository", "path/to/git/repo"]
    }
  }
}
CLI log: redacted 1 value before the graph · 40 tools labelled by rule · 0 unlabelled

What the scanner reports

567 leak paths computed, not scored
stranger's words github issue
  ↓ github.get_issue · reads untrusted
  ↓ agent context · now tainted
private local repo · the project itself
  ↓ git.diff · reads private
  ↓ agent context
  ↓ git.push · writes remote
  ↓ public sink public repo · origin/main

one of 567 routes — scan prints the 25 shortest by hops; --json carries the same 25 plus the full count.

THE FIX — CALL algo.maxFlow ⇒ 9 grants

Breaks 567 of 567 paths · revokes 9 write grants (every github write + git.push) · verified 0 remain when the same query re-runs without them. When cuts tie on cost the engine prefers the write side — the agent keeps its read and loses only the publish.

Why this edge?

policy diff

- allow: git.push, github.* writes # 567 paths end in a public sink
+ deny: the nine write grants # re-run without them: 0 remain
label confidence from the rulebook · a wrong label is a bug — tell us
toollabelsourceconf
github.get_issueuntrustedrule0.70
filesystem.write_fileprivaterule0.60
git.pushsinkrule0.70

The queries, verbatim

Cypher executed — leak-path query · read-only · 3 s timeout · LIMIT capped at 100
MATCH (u:Resource {trust:'untrusted'})-[:FLOWS]->(:Tool)-[:FLOWS]->(ctx:Context)
MATCH leak = (p:Resource {sensitivity:'private'})
              -[:FLOWS*1..5]->(ctx)
              -[:FLOWS*1..5]->(s:Resource {visibility:'public'})
RETURN u.kind AS injected_from,
       [n IN nodes(inj) | coalesce(n.kind, n.name, n.id)] AS injection,
       [n IN nodes(leak) | coalesce(n.kind, n.name, n.id)] AS chain,
       [n IN nodes(leak) WHERE n:Tool | n.name] AS tools,
       length(leak) AS hops
ORDER BY hops ASC LIMIT 25
Cypher executed — min-cut · one algo.maxFlow call; both cuts derived from it in code
MATCH (p:Resource {sensitivity:'private'})
WHERE p.visibility IS NULL OR p.visibility <> 'public'
WITH collect(p) AS src
MATCH (s:Resource {visibility:'public'})
WHERE s.sensitivity IS NULL OR s.sensitivity <> 'private'
WITH src, collect(s) AS dst
CALL algo.maxFlow({
  sourceNodes: src,
  targetNodes: dst,
  relationshipTypes: ['FLOWS'],
  capacityProperty: 'capacity'
})
YIELD maxFlow, edges, edgeFlows
RETURN maxFlow, edges, edgeFlows

One call, one relationship type (all 6.0.2 accepts); it returns flows, not the cut, so the residual graph is walked in mincut.ts to produce the edge set above.