Skip to main content
Glama
iabdullah215

pathhound-mcp

by iabdullah215

pathhound-mcp

An MCP (Model Context Protocol) server that lets an LLM reason over an Active Directory attack graph already collected by BloodHound — shortest paths to Tier 0, blast radius from a principal, the highest-leverage choke points to remediate, and defender-facing remediation text — with every action scoped to an authorized engagement and logged.

Authorized penetration-testing use only. This server never touches a live domain, never collects data, and never runs attacks. Collection is done separately (SharpHound / bloodhound-python); this tool only reads and reasons over the resulting graph in Neo4j. See SECURITY.md for the full intended-use boundary.

What it does

  • Turns a collected graph into explainable answers. Ask "what are the shortest paths to Domain Admins," "if I own this account what becomes reachable," or "which edges should we fix first" and get structured results.

  • Read-only and scoped by design. It refuses to start without an engagement scope, refuses to query domains outside that scope, rejects write/destructive Cypher, and appends every call to an audit log.

  • Defender-facing output. explain_edge and remediation_for_path produce report-ready remediation, not exploit commands.

Tools

Tool

What it returns

list_domains()

Domains/forests in the graph, with node counts and an in-scope flag.

run_cypher(query, params)

Rows from a read-only Cypher query. Write clauses are rejected unless allow_writes is set; destructive/external ops are always refused. Results are row-capped with a truncation flag.

shortest_paths_to_tier0(start, max_paths)

Shortest attack paths to Tier 0 / high-value targets — from start (an in-scope principal) or from every owned principal. Each path is an ordered list of start —[edge]→ end hops.

reachable_from(principal, max_hops)

Blast radius from a principal, summarized by node type, highlighting any Tier 0 reachability.

find_choke_points(top_n)

The edges appearing in the most distinct shortest paths to Tier 0, each with the number of paths removing it would break.

explain_edge(edge_type)

Plain-language reason a BloodHound edge (e.g. GenericAll, AddKeyCredentialLink, DCSync) is abusable, the technique name, a defender remediation, and reference pointers.

remediation_for_path(path_id_or_cypher)

For a path (given as a read-only Cypher query returning a path p), the specific ACE / delegation / membership change that breaks each hop, phrased for a report.

engagement_summary()

Headline counts for reporting: principals, owned, high-value, distinct paths to Tier 0, and the top choke points.

Resources

Resource

Contents

pathhound://engagement-scope

The active scope (engagement id, authorization reference, allowed domains, write flag) and the Neo4j target with the password masked.

pathhound://cypher-library

A curated, versioned set of named read-only Cypher queries (Kerberoastable, unconstrained delegation, AS-REP roastable, paths to Domain Admins, ACL abuse from a principal, …), each validated read-only on load.

Optional write tools (off by default)

Two tools — mark_owned(principal, confirm) and set_high_value(principal, confirm) — set BloodHound's owned / Tier 0 markers (and nothing else) so the path tools treat those nodes accordingly. They only exist when allow_writes: true is set in scope.yaml; each call also requires an explicit confirm=true, an in-scope principal, and is audited. They never delete or drop. Leave allow_writes: false (the default) for a strictly read-only deployment.

Related MCP server: EMBA-MCP

Requirements

  • Python 3.11+

  • A BloodHound CE Neo4j instance reachable over Bolt (the graph is collected and ingested separately — see the quickstart below)

Install

# With uv (preferred):
uv venv && uv pip install -e ".[dev]"

# Or with pip:
python -m venv .venv && . .venv/bin/activate
pip install -r requirements.txt && pip install -e .

Configure

Two pieces of configuration: the engagement scope (a file) and the Neo4j connection (environment variables — credentials are never hardcoded).

1. Engagement scope — copy the example and edit it:

cp scope.example.yaml scope.yaml
# scope.yaml
engagement_id: "ENG-0001"                      # names the audit log file
authorization_ref: "SoW-2026-ACME-001 / JIRA-1234"
allowed_domains:                               # only these may be queried
  - "CORP.LOCAL"
  - "CHILD.CORP.LOCAL"
allow_writes: false                            # keep false unless you need the write-flag tools
max_rows: 500                                  # run_cypher row cap

The server refuses to start if scope.yaml is missing or allowed_domains is empty — this is intentional; there are no unscoped queries. Domain names are matched case-insensitively against BloodHound's uppercase FQDNs.

2. Neo4j connection — via environment variables (or a .env; see .env.example):

export NEO4J_URI="bolt://127.0.0.1:7687"
export NEO4J_USER="neo4j"
export NEO4J_PASSWORD="…"
export NEO4J_DATABASE="neo4j"        # optional, defaults to neo4j
# export PATHHOUND_SCOPE="/abs/path/scope.yaml"   # optional; defaults to ./scope.yaml

Run

As a standalone stdio MCP server:

pathhound-mcp          # or:  python -m pathhound_mcp.server

Register with an MCP client

Point your client at the console script (use the absolute path from your venv, e.g. /path/to/pathhound-mcp/.venv/bin/pathhound-mcp) and pass the connection + scope through its environment. Example (Claude Desktop claude_desktop_config.json):

{
  "mcpServers": {
    "pathhound": {
      "command": "/path/to/pathhound-mcp/.venv/bin/pathhound-mcp",
      "env": {
        "NEO4J_URI": "bolt://127.0.0.1:7687",
        "NEO4J_USER": "neo4j",
        "NEO4J_PASSWORD": "…",
        "PATHHOUND_SCOPE": "/path/to/pathhound-mcp/scope.yaml"
      }
    }
  }
}

Example questions to ask the LLM

Once connected, an operator can ask things like:

  • "List the domains in the graph and how big each is."list_domains

  • "Which accounts are Kerberoastable?"run_cypher with the kerberoastable_users library query

  • "Show the shortest attack paths to Tier 0."shortest_paths_to_tier0

  • "If I compromise alice@corp.local, what becomes reachable?"reachable_from

  • "What are the top 10 choke points we should fix first?"find_choke_points

  • "Why is AddKeyCredentialLink dangerous, and how do we fix it?"explain_edge

  • "Give me remediation steps for this attack path."remediation_for_path

  • "Summarize the engagement for the report."engagement_summary

Quickstart: collect into BloodHound, then point this at Neo4j

This server only reads an already-collected graph; collection happens separately and outside this tool.

  1. Collect the domain with SharpHound or bloodhound-python during your authorized engagement. You get a set of JSON files (or a zip) — from a lab like GOAD (GOAD-Light ~16 GB, full GOAD ~32 GB) or a real engagement.

  2. Stand up BloodHound CE and ingest the data. The official compose stack runs Neo4j (Bolt on 127.0.0.1:7687), Postgres, and the BloodHound UI on :8080:

    docker compose -f /path/to/docker-compose.yml up -d
    # open http://localhost:8080, log in, and upload the collection zip
    # (Administration → File Ingest). Wait for ingest + post-processing.

    If the Neo4j volume is already ingested, you can start just the graph: docker compose -f /path/to/docker-compose.yml up -d graph-db.

  3. Point pathhound-mcp at that Neo4j (see Configure) and run it. Your allowed_domains in scope.yaml should match the domains in the collection (e.g. CORP.LOCAL).

BloodHound schema compatibility

Newer BloodHound CE tags Tier 0 with the Tag_Tier_Zero node label and owned principals with Tag_Owned, while older versions used the highvalue / owned properties (and CE also sets system_tags: admin_tier_0). pathhound recognizes all of these, so Tier 0 / owned reasoning works across versions.

Tests

pytest        # unit tests — guardrails, scope, audit, Cypher guard, tool logic

The suite is fully database-free: the read-only guardrail, scope enforcement, audit logging, row-capping, and path/reporting logic are all exercised against stubs, so pytest runs in a fraction of a second with no Neo4j. (The tools have also been validated end-to-end against a real BloodHound CE graph.)

Project layout

src/pathhound_mcp/
├── server.py          # FastMCP app: tool/resource registration + startup guard
├── config.py          # loads/validates scope.yaml (pydantic); Neo4j from env
├── guardrails.py      # read-only Cypher guard + scope enforcement
├── audit.py           # append-only JSONL audit logger (@audited decorator)
├── graph.py           # Neo4j connection + read helpers (the only DB access)
├── schema.py          # attack-edge set + Tier 0 / owned predicates
├── models.py          # pydantic result models
├── cypher_library.json / edge_library.json   # curated read-only queries + edge KB
└── tools/             # inventory, query, paths, reporting, edges

audit/, scope.yaml, and .env are gitignored — treat the audit log and the collected graph as engagement-sensitive.

Safety model

See SECURITY.md for the intended-use boundary and the guarantees: scope-at-startup, read-only by default, scope enforcement, append-only audit log, and no live-domain capability.

License

MIT — see LICENSE.

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/iabdullah215/pathhound-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server