Scan · Map · Detect modules · Analyze
Grafly turns a codebase into a dependency map — a directed graph of artifacts (files, classes, functions) and their dependencies. It parses source files locally with tree-sitter, detects cohesive modules with the Leiden algorithm, and emits an interactive HTML visualization, a JSON map, and a Markdown report — with no API calls required for code files.
Designed to work as a standalone CLI, a Rust library, and an MCP server that any LLM coding assistant can call as a tool.
Grafly uses the architect's vocabulary — what software architects actually call things:
| Term | Meaning |
|---|---|
| Artifact | A unit of code (file, class, function, ...) |
| Package | A buildable unit declared in a manifest (Cargo.toml, pyproject.toml, package.json, go.mod). Sits above File in the containment hierarchy |
| Dependency | A directed relationship between artifacts |
| Dependency Map | The full graph of artifacts and dependencies |
| Module | A cohesive cluster of artifacts (detected by Leiden) |
| Hotspot | An artifact with disproportionately high centrality |
| Coupling | A dependency that crosses module boundaries |
| Visibility | Source-level access of a declared symbol — Public, Crate, Private, Unknown. Drives the public-surface filter on hotspots, couplings, and the artifact HTML |
| Scan | Discovering artifacts and dependencies from source |
| Insight | A finding the analysis surfaces about the architecture |
- Local-first — all code scanning runs with tree-sitter, fully offline
- Fast — parallel file scanning via Rayon; single-pass map construction
- Package layer — discovers buildable units from project manifests (
Cargo.toml,pyproject.toml,package.json,go.mod), links each source file to its declaring package, and flags binary entry points - Visibility-aware — detects
Public/Crate/Privateon each declared symbol (Rustpub, Python underscore, JSexport, Go capitalisation, Java modifiers, TS accessibility) and filters internal helpers out of the architecture view by default - Module detection — Leiden algorithm runs both globally (cross-package modules) and within each package (fine-grained subsystems), so you can see both "where the cross-cuts are" and "what lives inside each crate"
- Architecture insights — hotspots, cross-module couplings, suggested insights
- Interactive path queries — weighted shortest paths that prefer runtime call chains (
Calls=1) over file-level import shortcuts (Imports=5), and BFS subgraphs with a supernode cap to keep neighborhoods focused - Interactive HTML — vis-network map with module colours, click-to-inspect
- MCP server — expose grafly as a tool to Claude Code, Cursor, and any MCP-compatible LLM
| Language | Extensions |
|---|---|
| Python | .py |
| Rust | .rs |
| JavaScript | .js .mjs .cjs |
| TypeScript | .ts .tsx |
| Go | .go |
| Java | .java |
cargo install grafly-cli # CLI
cargo install grafly-mcp # MCP serverOr build from source (requires Rust ≥ 1.85):
git clone https://github.com/gianlucaciocci/grafly
cd grafly
cargo build --release# Analyze the current directory (writes ./grafly-out/)
grafly analyze .
# `grafly .` is shorthand for the same.
# Specific project
grafly analyze ~/projects/myapp
# Tune module resolution (default 1.0 — higher = more, smaller modules)
grafly analyze . --resolution 0.5
# Deterministic run
grafly analyze . --seed 42
# Choose output formats
grafly analyze . --formats json,html| Flag | Default | Description |
|---|---|---|
<PATH> (positional) |
. |
Directory to scan |
-r, --resolution <FLOAT> |
1.0 |
Leiden resolution — higher → more, smaller modules |
-s, --seed <INT> |
— | Random seed for deterministic module detection |
-f, --formats <CSV> |
json,html,html-modules,html-packages,md |
Comma-separated output formats: json, html, html-modules, html-packages, md |
--max-html-nodes <N> |
800 |
Cap on artifacts in the artifact-level HTML (0 = unlimited) |
--max-html-modules <N> |
100 |
Cap on modules in the module-level HTML (0 = unlimited) |
--html-include-ambiguous |
false |
Show Ambiguous-confidence edges in the artifact HTML (always kept in JSON) |
--no-ignore |
false |
Disable all path filtering — scan every file, including hidden dirs, .gitignored paths, node_modules, target, .venv, tests, examples |
--include-tests |
false |
Keep test/example files (tests/, __tests__/, examples/, *_test.go, *.test.ts, *Test.java, etc.). Excluded by default — they're not runtime architecture |
--include-imports |
false |
Keep Imports edges in the output. Used for clustering either way; dropped after clustering by default because they create misleading A → shared_file → B path shortcuts and inflate hotspot degrees |
--no-intra-package-modules |
false |
Skip the intra-package Leiden pass. By default grafly clusters within each Package separately (in addition to the global cross-package modules), surfacing fine-grained subsystems inside each crate/package |
--leiden-thorough |
false |
Use leiden-rs's stock high-quality defaults (max_iter=100, epsilon=1e-10) instead of grafly's fast defaults (max_iter=30, epsilon=1e-8, min_iter=3). Adds time on large codebases for a sub-percent quality gain |
--include-private |
false |
Show Visibility::Private symbols in the artifact HTML, hotspots, and couplings. Hidden by default so the architecture view stays focused on the public surface; always kept in grafly_knowledge.json regardless |
Run grafly analyze --help for the same list straight from the binary.
Output files (always written to ./grafly-out/ — hardcoded so the rules file, MCP server, and /grafly-* skills all agree on where to look):
| File | Description |
|---|---|
README.md |
Index of all output files, written for both humans and LLM agents |
grafly_report.md |
Markdown analysis: packages, modules, hotspots, cross-module couplings, suggested questions — LLM-discoverable |
grafly_knowledge.json |
Full directed dependency map with source_file:line on every edge |
grafly_modules.html |
Interactive module-level overview (Leiden modules as nodes, edges grouped by relationship kind) |
grafly_packages.html |
Interactive package-level overview (Cargo/pyproject/package.json/go.mod packages as nodes, cross-package edges; binaries coloured distinctly from libraries) |
grafly_artifacts.html |
Interactive artifact-level graph (top-N by degree, Ambiguous edges suppressed for clarity) |
SUGGESTED_QUESTIONS.md |
Kickoff list of architectural questions grouped by which grafly file answers them, with a section for an LLM to fill in project-specific versions |
After analyzing, run grafly install to wire grafly into your LLM tool of
choice. One command does everything that target supports — the rules file
(so agents know to consult ./grafly-out/), the MCP server registry (so
they can query the graph live), and (for Claude Code) the /grafly-*
slash commands.
grafly install # default: Claude Code
grafly install --platform cursor # Cursor
grafly install --platform claude-desktop # Claude Desktop (MCP only)
grafly install --platform vscode # VS Code (MCP only)
grafly install --all # every supported target
grafly list # show what's currently installed
grafly uninstall --all # cleanly remove everything everywherePer-target capability matrix:
| Target | Rules file | MCP config | Claude Code skills |
|---|---|---|---|
claude (default) |
CLAUDE.md |
.mcp.json |
/grafly-ask, /grafly-suggest-questions |
claude-desktop |
— | claude_desktop_config.json (global) |
— |
cursor |
.cursor/rules/grafly.mdc |
.cursor/mcp.json |
— |
windsurf |
.windsurfrules |
~/.codeium/windsurf/mcp_config.json |
— |
vscode |
— | .vscode/mcp.json |
— |
agents |
AGENTS.md (Codex/Aider/OpenCode/Factory) |
— | — |
copilot |
.github/copilot-instructions.md |
— | — |
gemini |
GEMINI.md |
— | — |
Each install is all-or-nothing per target. There's no flag to install just the rules or just the MCP server — if a target supports a surface, it gets it.
Rules-file installs append a marker-bracketed section
(<!-- grafly-section-start --> / <!-- grafly-section-end -->); MCP installs
merge a grafly-mcp entry into the existing JSON registry. Other content in
those files is preserved. grafly uninstall reverses each surface cleanly.
grafly-mcp exposes ten tools — analyze, get_artifacts, get_modules,
get_hotspots, get_couplings, get_insights, export, find_path,
get_neighbors, get_dependents. The binary path is auto-detected (prefers
the bare name grafly-mcp on PATH; falls back to the sibling of the current
grafly executable); override with --bin.
Installing the claude target also wires two skill briefs into
~/.claude/skills/ plus a registration in ~/.claude/CLAUDE.md:
| Slash command | What it does |
|---|---|
/grafly-ask |
Ask any architectural / structural question — overview, modules, hotspots, cross-module couplings, "how does X connect to Y", "what depends on Z" — and the skill routes to the right grafly-mcp:* MCP tool. Falls back to ./grafly-out/grafly_report.md if the MCP server isn't connected. |
/grafly-suggest-questions |
Bootstrap a project-specific question list: reads ./grafly-out/SUGGESTED_QUESTIONS.md + grafly_report.md, resolves the placeholders (<ARTIFACT> / <MODULE> / <PACKAGE>) to real names from this codebase, appends a dated section, and surfaces the top 10 as a menu in chat. Useful as a kick-off for onboarding or code review. |
The same suggested-questions workflow is also wired into the install templates
for every other LLM tool, so agents that don't support Claude Code skills still
know to consult SUGGESTED_QUESTIONS.md when the user asks "what can I ask
about this codebase?" or "where do I start?".
Grafly ships a first-class MCP server (grafly-mcp) so LLMs can call it directly as a tool.
Add to your ~/.claude/settings.json (or project .claude/settings.json):
{
"mcpServers": {
"grafly-mcp": {
"command": "grafly-mcp",
"args": []
}
}
}| Tool | Description |
|---|---|
analyze |
Full pipeline — returns summary with artifact/dependency/module counts, quality score, hotspots, and insights |
get_artifacts |
List artifacts, optionally filtered by kind or module_id |
get_modules |
Module breakdown — sizes and representative artifacts |
get_hotspots |
High-centrality artifacts that may be architectural bottlenecks |
get_couplings |
Cross-module couplings — unexpected dependencies between modules |
get_insights |
Suggested insights for architectural review |
export |
Write JSON / HTML / Markdown files to a directory |
find_path |
Weighted shortest path between two artifacts — prefers Calls chains over Imports shortcuts |
get_neighbors |
Depth-limited BFS subgraph around an artifact (default: runtime edges only, supernode cap at degree 200) |
get_dependents |
The artifacts that depend on a given artifact (incoming-direction subgraph) |
{
"tool": "analyze",
"arguments": {
"path": "/home/user/projects/myapp",
"resolution": 1.0,
"seed": 42
}
}Response:
{
"artifacts": 312,
"dependencies": 847,
"modules": 7,
"quality": 0.4821,
"hotspots": [
{ "label": "DatabaseClient", "degree": 34, "source_file": "src/db/client.py" }
],
"insights": [
"`DatabaseClient` (src/db/client.py) is a hotspot with 34 connections — consider splitting it.",
"`AuthMiddleware` (module 2) couples to `ReportGenerator` (module 5) via `Imports` — is this intentional?"
]
}{
"tool": "find_path",
"arguments": {
"path": "/home/user/projects/myapp",
"from": "DataActorCore",
"to": "ExecutionEngine"
}
}By default the path is weighted so Calls edges (weight 1) are strongly preferred
over Imports edges (weight 5) and References/Uses (weight 10). In a message-bus
architecture this routes the answer through the actual mediation chain instead of
taking a file-level import shortcut. Set weighted: false for raw shortest path by
hop count. Each hop in the response carries its DependencyKind, Confidence,
and source_line so callers can distinguish hard-evidence chains from inferred ones.
Add to Cargo.toml:
[dependencies]
grafly = "0.1"use std::path::Path;
// 1. Scan
let scan = grafly::scan::scan_dir(Path::new("./src"))?;
// 2. Build dependency map
let mut builder = grafly::MapBuilder::new();
builder.add_scan(scan);
let mut map = builder.build();
// 3. Detect modules
grafly::cluster::detect_modules(&mut map, 1.0, None)?;
// 4. Analyze
let analysis = grafly::analyze::analyze(&map);
// 5. Query — weighted shortest path between two artifacts
let from = grafly::query::resolve(&map, "DataActorCore")?;
let to = grafly::query::resolve(&map, "ExecutionEngine")?;
if let Some(path) = grafly::query::find_path(&map, from, to, &Default::default()) {
println!("{} hops, weight {}", path.total_hops, path.total_weight);
}
// 6. Export
grafly::export::write_html(&map, Path::new("map.html"))?;grafly/
├── crates/
│ ├── core/ Artifact, Dependency, DependencyMap, MapBuilder — shared types
│ ├── scan/ tree-sitter parsers (Python, Rust, JS, TS, Go, Java) + Rayon walker
│ ├── cluster/ Leiden module detection via leiden-rs (detect_modules)
│ ├── analyze/ Hotspots · couplings · insights
│ ├── query/ find_path · neighbors · ancestors · descendants
│ ├── report/ Markdown report generator
│ ├── export/ JSON + interactive HTML (vis-network)
│ ├── cli/ grafly binary (clap)
│ └── mcp/ grafly-mcp binary (rmcp stdio server)
Pipeline:
scan_dir(path)
→ ScanResult { artifacts, dependencies }
→ MapBuilder → DependencyMap (petgraph DiGraph)
→ detect_modules() → module_id assigned to each artifact
→ analyze() → Analysis { hotspots, couplings, insights }
→ query() → Path / Subgraph — weighted shortest path, BFS
→ export / report
Prerequisites:
- Rust ≥ 1.85
- Build:
git clone https://github.com/gianlucaciocci/grafly
cd grafly
cargo buildAll dependencies (including leiden-rs for module detection) resolve from crates.io.
- Add the
tree-sitter-<lang>crate tocrates/scan/Cargo.tomland the workspaceCargo.toml. - Create
crates/scan/src/<lang>.rsfollowing the pattern inpython.rs. - Register the extension in
crates/scan/src/lib.rs. - Update the supported languages table in this README and in
CLAUDE.md.
MIT — see LICENSE.