The MCP (Model Context Protocol) Heap Explorer provides an interactive way to analyze LoliProfiler heap data files using Claude Code. It supports two file formats:
- Diff files from
LoliProfilerCLI --compare— two-profile comparison showing memory growth/shrinkage - Snapshot files from
LoliProfilerCLI --dump— single-profile export showing absolute memory distribution
Instead of dumping the entire file content (often 12MB+ for diffs, or 100MB+ for snapshots) into a single prompt, it loads the data into an in-memory tree and exposes query tools that Claude can call interactively.
This approach reduces context usage from megabytes to ~10-20KB of focused, on-demand responses.
data file (diff.txt or snapshot.txt)
|
v
MCP Server (heap_explorer_server.py)
| Auto-detects format (diff vs snapshot)
| Loads into indexed in-memory tree
| Exposes 7 query tools via stdio
v
Claude Code
| Calls load_file() -> loads data (if not pre-loaded)
| Calls get_summary() -> understands situation
| Calls get_top_allocations() -> finds hotspots
| Calls get_children() / get_call_path() -> drills down
| Greps/Reads source code -> understands implementation
v
Analysis Report (markdown/HTML)
Install the MCP Python SDK:
pip install "mcp[cli]>=1.0.0"Or use the included requirements file:
pip install -r requirements.txtThe .mcp.json in the project root starts the MCP server with no file pre-loaded:
{
"mcpServers": {
"loli-heap": {
"command": "python",
"args": ["mcp_server/heap_explorer_server.py"],
"env": {}
}
}
}Start a Claude Code session in the project directory. The MCP tools are available automatically. Just tell Claude which file to analyze:
> Analyze heap_7682.txt for the top memory hotspots.
Claude will call load_file("heap_7682.txt") to load the data, then proceed with analysis using the other tools.
You can also switch files mid-session — calling load_file again replaces the previous data.
Pre-loading a file is still supported if you prefer. Add --file to the args in .mcp.json:
"args": ["mcp_server/heap_explorer_server.py", "--file", "path/to/data.txt"]For CI/CD or scripted analysis, use analyze_heap.py which launches Claude Code as a subprocess with the MCP server pre-configured:
# Analyze a diff (same codebase for both profiles)
python analyze_heap.py diff.txt --repo /path/to/game/source -o report.md
# Analyze a heap snapshot
python analyze_heap.py snapshot.txt --repo /path/to/game/source -o report.md
# HTML output
python analyze_heap.py diff.txt --repo /path/to/game/source -o report.html
# Different repos for baseline vs comparison (diff only)
python analyze_heap.py diff.txt --base-repo /path/to/v1 --target-repo /path/to/v2
# Custom minimum size threshold
python analyze_heap.py diff.txt --repo /path/to/source --min-size 1.0The server exposes 7 tools. Each returns text with [node_id] references for follow-up queries. All tools work identically for both diff and snapshot files.
Load a heap data file into the explorer. Replaces any previously loaded data. Accepts absolute paths or paths relative to the working directory.
Parameters:
file_path(str, required) - Path to.txtfile fromLoliProfilerCLI --compareor--dump.
Loaded 588,473 nodes (230 roots) from heap_7682.txt [mode: snapshot]
When to use: At the start of a session when no file was pre-loaded via --file, or to switch to a different file mid-session.
Returns overview statistics and tree metadata. Output adapts to the file format.
Diff file output:
=== Heap Diff Summary ===
Baseline allocations: 2,353,608
Comparison allocations: 2,369,493
Baseline total size: 534.44 MB
Comparison total size: 512.44 MB
Size delta: -22.00 MB
Changed allocations: 45,413
New allocations: 3,494
=== Tree Metadata ===
Total nodes: 45,413
Root nodes: 53
Unique function names: 4,144
=== Top Root Nodes ===
[0] -[IOSAppDelegate MainAppThread:], +132.19 MB, count=778693
[29585] FRunnableThreadPThread::Run(), +55.84 MB, count=174639
Snapshot file output:
=== Heap Snapshot Summary ===
Total allocations: 2,655,709
Total size: 634.21 MB
=== Tree Metadata ===
Total nodes: 588,473
Root nodes: 230
Unique function names: 12,741
=== Top Root Nodes ===
[0] 0x5e6c000219df5a10, 306.68 MB, count=1280098
[276566] 0xb605800219df5a10, 213.58 MB, count=1015818
When to use: First call in any analysis session. Understand the scale and identify major roots.
Find the N largest allocation nodes across all depths. For diffs this finds the largest growth points; for snapshots it finds the largest absolute allocations.
Parameters:
n(int, default 20) - Maximum results to return.min_size_mb(float, default 0.0) - Minimum absolute size in MB.
Top 5 allocations (min 2.0 MB):
[0] +132.19 MB, count=778693, depth=0 | -[IOSAppDelegate MainAppThread:]
[1] +132.17 MB, count=778551, depth=1 | FAppEntry::Tick()
[31162] +12.72 MB, count=57442, depth=11 | FPrimitiveSceneInfo::CacheMeshDrawCommands(...)
[32281] +21.75 MB, count=111751, depth=0 | CAkThreadedBankMgr::ExecuteCommand()
When to use: Second call. Identifies hotspots at any depth, not just root nodes. Skip the generic wrappers (depth 0-3) and focus on functional-level functions deeper in the tree.
List direct children of a node, sorted by absolute size descending.
Parameters:
node_id(int, required) - The integer ID of the parent node.
Children of [3] UGameEngine::Tick(float, bool) (5 children):
[4] +37.40 MB, count=87655 | UWorld::Tick(ELevelTick, float)
[7241] +28.56 MB, count=194132 | FTickTaskManager::StartFrame(...)
[26689] +8.01 MB, count=57473 | UGameEngine::RedrawViewports()
When to use: Drill into a specific branch. Follow the largest children to find where memory actually goes.
Trace the full call path from root to a specific node.
Parameters:
node_id(int, required) - The integer ID of the target node.
Call path to [31162] (12 frames):
[0] -[IOSAppDelegate MainAppThread:], +132.19 MB, count=778693
[1] FAppEntry::Tick(), +132.17 MB, count=778551
[2] FEngineLoop::Tick(), +132.17 MB, count=778551
...
[31162] FPrimitiveSceneInfo::CacheMeshDrawCommands(...), +12.72 MB, count=57442
When to use: Understand the calling context that leads to an allocation. Useful for cross-referencing with source code.
Regex search across all function names in the tree.
Parameters:
pattern(str, required) - Regular expression to match.max_results(int, default 30) - Maximum results to return.
Found 5 matches (showing top 5 of 4305 total) for 'FMemory':
[5947] +10.00 MB, count=160, depth=25 | FMemory::Malloc(unsigned long, unsigned int)
[31171] +7.57 MB, count=19376, depth=20 | FMemory::Realloc(void*, unsigned long, unsigned int)
Results are sorted by absolute size descending, so top results are the most significant.
When to use: Find specific modules, classes, or patterns. Examples: CAkBankMgr, Texture2D, NavMesh, Lua.
Show the indented tree structure below a node.
Parameters:
node_id(int, required) - The root of the subtree.max_depth(int, default 4) - Maximum depth to render.
[32281] CAkThreadedBankMgr::ExecuteCommand(), +21.75 MB, count=111751
[32282] CAkBankMgr::LoadBank(...), +21.75 MB, count=111751
[32283] CAkBankMgr::ProcessDataChunk(...), +21.75 MB, count=111751
[32284] CAkBankMgr::LoadMedia(...), +11.56 MB, count=55624
[32310] CAkBankMgr::LoadHircChunk(...), +10.19 MB, count=56127
When to use: See the full branch structure at a glance. Good for understanding how memory distributes across sub-branches.
-
Load a file: Call
load_file("data.txt")if no file was pre-loaded via--file. -
Get overview: Call
get_summary()to understand the overall memory situation and tree scale. -
Find hotspots: Call
get_top_allocations(20, 2.0)to find the 20 largest nodes above 2MB. Skip generic wrappers (the first few results at depth 0-3 are usually thread entry points). -
Drill down each hotspot:
get_call_path(node_id)- See how we got hereget_children(node_id)- See where memory branches belowget_subtree(node_id, 5)- See the full branch at a glance
-
Cross-reference source code: Use Grep/Read to find the function implementation and understand what data structures are being allocated.
-
Search for patterns: Use
search_function("LoadBank|LoadMedia")to find all instances of specific allocation patterns.
python analyze_heap.py <data_file> [options]
| Argument | Description |
|---|---|
data_file |
Path to LoliProfilerCLI output file (diff from --compare, or snapshot from --dump) |
| Option | Description |
|---|---|
--repo <path> |
Source code repo (used for both baseline and comparison) |
--base-repo <path> |
Baseline version source code repo (diff only) |
--target-repo <path> |
Comparison version source code repo (diff only) |
| Option | Default | Description |
|---|---|---|
-o, --output <path> |
auto-generated .md | Output report file (.md or .html) |
--min-size <MB> |
2.0 | Minimum allocation size threshold in MB |
-t, --timeout <seconds> |
1800 | Analysis timeout in seconds |
analyze_heap.py
|
|-- Detects file mode (diff vs snapshot)
|-- Writes temp .mcp.json config
|-- Launches: claude -p --mcp-config <config>
| |
| |-- Claude starts MCP server as child process:
| | heap_explorer_server.py --file data.txt
| | |
| | |-- tree_model.py auto-detects format and loads data
| | |-- Exposes 7 tools via FastMCP (stdio)
| |
| |-- Claude calls MCP tools interactively
| |-- Claude searches source code (Grep/Read)
| |-- Claude writes report to output file
|
|-- Optionally converts .md to .html
mcp_server/
__init__.py # Package marker
tree_model.py # Data model + query engine (~400 lines)
heap_explorer_server.py # MCP server with 7 tools (~170 lines)
analyze_heap.py # Batch automation script (~400 lines)
.mcp.json # Project-scoped MCP config (no --file needed)
requirements.txt # mcp[cli]>=1.0.0
TreeNode: Each node in the call stack tree.
node_id(int) - Unique identifier for follow-up queriesfunction_name(str) - C++ function signaturesize_bytes(int) - Size in bytes (signed for diffs, unsigned for snapshots)size_display(str) - Original string like "+10.44 MB" (diff) or "10.44 MB" (snapshot)count(int) - Allocation count (signed delta for diffs, absolute for snapshots)level(int) - Indentation depth (0 = root)parent/children- Tree links
FileSummary: Header statistics parsed from the report header. Contains a mode field ("diff" or "snapshot") that is auto-detected, plus shared fields (total_allocations, total_size) and diff-only fields (comparison_allocations, comparison_total_size, size_delta, changed_allocations, new_allocations).
CallTreeDatabase: The main class that holds the indexed tree.
- Parses diff or snapshot files in a single pass (format auto-detected)
- Builds parent-child relationships from indentation
- Maintains a name index for search
- All query methods return formatted text strings
# Step 1: Generate the diff
LoliProfilerCLI --compare baseline.loli comparison.loli --out diff.txt
# Step 2: Analyze with MCP
python analyze_heap.py diff.txt --repo /path/to/game/source -o report.md# Step 1: Dump a single profile to text
LoliProfilerCLI --dump profile.loli --out snapshot.txt
# Step 2: Analyze with MCP
python analyze_heap.py snapshot.txt --repo /path/to/game/source -o report.mdThe report is written in Chinese markdown with function names in English, including call stacks, source code analysis, and optimization suggestions. For diff files, the report focuses on memory growth reasons; for snapshot files, it focuses on memory distribution and the largest allocation hotspots.
Install the MCP SDK:
pip install "mcp[cli]>=1.0.0"Ensure Claude CLI (claude or claude-internal) is installed and in your PATH.
Verify the server can load your data file:
python mcp_server/tree_model.py diff.txt
python mcp_server/tree_model.py snapshot.txtExpected output: Roots: <N>, Total nodes: <M> followed by summary stats. If this fails, check that the file is a valid LoliProfilerCLI --compare or --dump output.
The MCP server loads the entire file into memory. A 45K-node diff uses ~50MB of RAM; a 588K-node snapshot uses ~600MB. For very large files (100K+ nodes), ensure sufficient memory is available.
