dotnet-native-cli
0.6.0
dotnet tool install --global dotnet-native-cli --version 0.6.0
dotnet new tool-manifest
dotnet tool install --local dotnet-native-cli --version 0.6.0
#tool dotnet:?package=dotnet-native-cli&version=0.6.0
nuke :add-package dotnet-native-cli --version 0.6.0
dotnet-native-mcp
Status: V1 in progress. Thirteen MCP tools are live:
load_native_binary,import_native_manifest,list_native_symbols,list_native_imports,resolve_symbols,extract_strings,get_size_breakdown,explain_retention,compare_native_binaries,disassemble,find_native_callers,get_r2r_header,list_r2r_runtime_functions. Standalone CLI verbs are also live:version,r2r,disasm,resolve,callers,symbols,imports,size,size-diff,strings,retention.
dotnet-native-mcp is tooling for navigating native .NET binaries —
NativeAOT, R2R-only, single-file native — when ECMA-335 metadata is stripped
or absent. You can use it either as an MCP server (dotnet-native-mcp) or as
a standalone local CLI (dotnet-native-cli). It is designed as the third leg
of a tooling triad with
dotnet-assembly-mcp
(managed metadata) and
dotnet-diagnostics-mcp
(live process events).
Why this exists
dotnet-diagnostics-mcp already attaches to NativeAOT processes and emits
hotspot frames whose symbols are mangled ILC names (S_P_____...) and whose
addresses point at native code. dotnet-assembly-mcp cannot answer queries on
those frames — its load_assembly rejects NativeAOT binaries with
module_load_failed: not a managed PE. Today, the LLM receives a hex address
and a mangled symbol and has nowhere to take them.
This server closes the gap. It accepts the NativeFrame handoff (binary +
symbol + address), demangles ILC symbols back to managed-looking names,
disassembles the native code with Iced, and
will read the sidecar artifacts ILC emits (.mstat, .map, DGML) when they
are available.
Where it does not belong
- Managed metadata, IL, decompile-to-C#. That's
dotnet-assembly-mcp. - Live process attach, EventPipe / ETW collection. That's
dotnet-diagnostics-mcp. - Generic reverse engineering (full Ghidra-class decompilation, full dynamic instrumentation, kernel-mode debuggers). Out of scope by design.
Tool surface (13 tools, V1)
| Tool | Purpose |
|---|---|
load_native_binary |
Open a PE/ELF/Mach-O, verify it's a managed-flavored native build, return a handle. Accepts path + optional buildId. |
import_native_manifest |
Bulk handshake from a producer (typically dotnet-diagnostics-mcp): register a list of native binaries in one call. Accepts entries: [{path, name?, buildId?}] + mode (lazy|eager). Per-entry failures reported inline without failing the batch. |
list_native_symbols |
Paginated symbol table. Source priority: .map sidecar → ELF .symtab/.dynsym → PE export table. Includes raw + demangled names. |
list_native_imports |
Paginated import/dependency walk. kind="functions" lists ELF undefined .dynsym imports or PE Import Directory entries; kind="libraries" lists ELF DT_NEEDED or PE imported DLL names. |
resolve_symbols |
Batch address ↔ symbol lookup with ILC demangling. Accepts up to 200 hex (0x-prefixed or bare) or decimal address strings against a single image. An optional loadBase rebases absolute runtime VAs (rva = address - loadBase) so ASLR'd position-independent (PIE) NativeAOT frames resolve. Per-address failures are reported inline without failing the whole batch. Replaces the former single-address resolve_symbol and multi-frame symbolicate_stack tools. |
extract_strings |
Paginated printable ASCII / UTF-16LE scan over .rodata / .rdata / .data.rel.ro / __const (with .data fallback). Returns section + offset for forensics. |
get_size_breakdown |
Read the .mstat sidecar emitted by NativeAOT and aggregate native bytes by assembly, namespace, type, method, or category. Counts method bodies (code + GC + EH info), EETypes/types, and the Blobs catch-all (dehydrated data, runtime metadata, frozen-object regions, RVA static fields, manifest resources) for complete, non-overlapping accounting, and reports the mstat format version, a per-category summary, and the deduplicated-method count. |
explain_retention |
Read the DGML reachability sidecar emitted by NativeAOT and return root → target retention paths — with the ILC retention reason on each edge (e.g. call, Reflectable type, Virtual method), each edge classified into a kind (Reflection, Generics, VirtualDispatch, DirectCode, Structural). Every path gets a verdict — reflection-driven (potentially trimmable) when any edge is reflection/metadata-driven, otherwise structural. With includeSizeCost (default on) each node is priced with the native bytes the .mstat sidecar attributes to it and each path reports the total bytes it keeps alive. maxPaths>1 returns the shortest chain from each distinct root (the independent reasons a type/method stays in the binary); ambiguous queries surface under Candidates. |
compare_native_binaries |
Diff two loaded images: build-id, format, arch, file/section size deltas, added/removed/size-changed symbols. When both images have a sibling .mstat sidecar, also emits a managed "what grew between two builds?" size diff (build-level total delta plus top grown/shrunk buckets) at a chosen grouping (assembly/namespace/type/method/category). |
disassemble |
Iced x86/x64 and AsmArm64 ARM64 disassembly with CALL/JMP/BL cross-ref hints. Default 64 instructions, capped at 2048. resolveSource=false (default) skips per-instruction DWARF lookup; set true to annotate each instruction with file:line. Two modes: registered-handle mode (imageHandle + address/symbolName) for images loaded via load_native_binary; or raw-bytes mode (imagePath + rva + size) for any PE/ELF/Mach-O — including managed PEs with R2R bodies — without a prior load_native_binary call. |
find_native_callers |
Lazily-built xref index: scan all executable sections of a loaded x86-64 or ARM64 image and return every CALL/JMP/BL instruction that targets a given symbol name or hex address. The index is cached in-process (L1) and persisted to disk under ~/.cache/dotnet-native-mcp/<build-id>.xref (L2) so large NativeAOT binaries pay the scan cost only once across sessions. crossImage=true resolves call sites that branch into PLT/import/stub thunks of other loaded images (ELF PLT entries account for the architecture-specific PLT0 header — 16 bytes on x86-64, 32 on AArch64). resolveSource=true (default) annotates each call site with file:line; set false to skip PDB I/O for large binaries. |
get_r2r_header |
Read the ReadyToRun header of a managed PE: version, decoded READYTORUN_FLAG_* names, full sections table, and the CompilerIdentifier (type 100) / OwnerCompositeExecutable (type 116) strings. includeImportSections=true decodes the ImportSections (type 101) fixup-region metadata; includeCompositeInfo=true decodes the composite-image ComponentAssemblies (type 115) entries and ManifestAssemblyMvids (type 118) GUIDs; includeMethodEntryPoints=true decodes the MethodDefEntryPoints (type 103) NativeFormat array into a MethodDef RID → RUNTIME_FUNCTION index mapping (with a has-fixups flag), capped by methodEntryPointsLimit; includeAvailableTypes=true decodes the AvailableTypes (type 108) NativeFormat hashtable into TypeDef (0x02) / ExportedType (0x27) metadata tokens for handoff to dotnet-assembly-mcp, capped by availableTypesLimit; includeInfoMaps=true decodes the V9 RID-indexed info maps — EnclosingTypeMap (type 122, nested→enclosing type tokens), MethodIsGenericMap (type 121, generic-method tokens) and TypeGenericInfoMap (type 123, per-type generic arity/variance/constraints) — capped by infoMapsLimit; includeManifestMetadata=true surfaces the ManifestMetadata (type 112) embedded ECMA-335 blob as a handoff descriptor (file offset, RVA, size, version string and stream directory) without decoding the managed metadata itself; includeHotColdMap=true decodes the HotColdMap (type 120) section into (cold, hot) RUNTIME_FUNCTION index pairs (capped by infoMapsLimit). Returns r2r_not_present for pure-managed or NativeAOT binaries. |
list_r2r_runtime_functions |
Paginated RUNTIME_FUNCTION entries from the RuntimeFunctions section (type 102), or — with rva — a binary-search lookup of the single covering entry. x64 + ARM64; other arches return r2r_arch_unsupported. |
For crash logs or sampled stacks where dotnet-diagnostics-mcp is not in the loop, use load_native_binary once and then call resolve_symbols with a list of raw hex addresses. When you already have NativeFrame handoffs with mangled addresses, pass those address strings directly to resolve_symbols.
ReadyToRun coverage
get_r2r_header + list_r2r_runtime_functions cover every R2R section whose layout
is structurally tractable — decodable from raw PE bytes alone — plus the
MethodDefEntryPoints array, which a safe span-based port of the runtime's
NativeFormat reader now decodes:
| Section (type) | Surface |
|---|---|
| Header, version, decoded flags | get_r2r_header |
| Sections table | get_r2r_header |
RuntimeFunctions (102), x64 + ARM64 |
list_r2r_runtime_functions (list + RVA lookup) |
ImportSections (101) |
get_r2r_header includeImportSections=true |
CompilerIdentifier (100) |
get_r2r_header (eager) |
OwnerCompositeExecutable (116) |
get_r2r_header (eager) |
ComponentAssemblies (115) |
get_r2r_header includeCompositeInfo=true |
ManifestAssemblyMvids (118) |
get_r2r_header includeCompositeInfo=true |
MethodDefEntryPoints (103) |
get_r2r_header includeMethodEntryPoints=true |
AvailableTypes (108) |
get_r2r_header includeAvailableTypes=true (metadata tokens) |
MethodIsGenericMap (121) |
get_r2r_header includeInfoMaps=true (generic-method tokens) |
EnclosingTypeMap (122) |
get_r2r_header includeInfoMaps=true (nested→enclosing type tokens) |
TypeGenericInfoMap (123) |
get_r2r_header includeInfoMaps=true (per-type generic info) |
ManifestMetadata (112) |
get_r2r_header includeManifestMetadata=true (ECMA blob handoff descriptor) |
HotColdMap (120) |
get_r2r_header includeHotColdMap=true ((cold, hot) RUNTIME_FUNCTION index pairs) |
Out of scope — sections encoded with the runtime's NativeFormat native
hashtables / nibble-compressed signature streams, or that embed an ECMA metadata
blob we do not parse into managed metadata. Decoding them requires the
NativeFormat hashtable reader plus Internal.TypeSystem type resolution —
partly redundant with dotnet-assembly-mcp (which owns managed metadata):
InstanceMethodEntryPoints,
InliningInfo*, ProfileDataInfo, PgoInstrumentationData, AttributePresence,
CrossModuleInlineInfo, the remaining *Map sections,
DelayLoadMethodCallThunks, ExceptionInfo, and DebugInfo. The
ManifestMetadata (112) blob is located (offset/size/version/streams) for
handoff but its managed metadata is decoded by dotnet-assembly-mcp.
Sidecar tier (V1+)
ILC emits structured sidecars on request:
| Artifact | Switch | What it gives us |
|---|---|---|
.mstat |
IlcGenerateMstatFile=true |
per-type / per-method native size |
.map |
IlcMapFileType=Normal |
symbol → address map |
| DGML | IlcGenerateDgmlFile=true |
reachability graph from the trimmer |
.mstat parsing answers "what blew up my AOT binary"; DGML reachability answers
"why was this type or method kept?".
CLI
The standalone CLI consumes DotnetNativeMcp.Core directly, without any MCP
package dependency. It mirrors the server's native-binary capabilities for
local shell workflows and ships in two release forms:
- NuGet global tool:
dotnet tool install -g dotnet-native-cli - Self-contained single-file archives:
dotnet-native-cli-<version>-<rid>.tar.gz/.zip
The root command exposes help plus a global --output json|table switch
(default json) and a repeatable --allow <path> trusted-root option that
feeds the shared PathAccessPolicy. See docs/cli.md for the
full CLI contract, including exit codes and allow-list behavior.
# install as a global tool
dotnet tool install -g dotnet-native-cli
dotnet-native-cli version --output table
# or run directly from source while developing
dotnet run --project src/DotnetNativeMcp.Cli -- --help
| Verb | Purpose |
|---|---|
version |
Show the CLI tool command name, informational version, and active path-policy roots. |
r2r header |
Decode the ReadyToRun header from a managed PE such as System.Private.CoreLib.dll. |
r2r runtime-functions |
Page through RUNTIME_FUNCTION entries from the ReadyToRun RuntimeFunctions section. |
disasm |
Disassemble a native image, inline bytes, or a raw blob with optional IL map/source annotations. |
resolve |
Resolve one or more addresses to symbols, source locations, and signatures. |
callers |
Find same-image and optional cross-image callers for a target address. |
symbols |
Page through native symbols with optional name filtering. |
imports |
Page through imported functions or imported libraries. |
size |
Read a paired .mstat sidecar and aggregate size by assembly/namespace/type/method/category. |
size-diff |
Diff two .mstat sidecars and optionally fail when growth crosses a threshold. |
strings |
Extract printable ASCII and UTF-16LE strings, with pagination and section filtering. |
retention |
Explain why a target symbol/type is retained using the paired DGML sidecar. |
# NativeAOT fixture used by the native-image examples below
sample_aot=tests/DotnetNativeMcp.Core.Tests/bin/Release/net10.0/fixtures/SampleAot/SampleAot
sample_aot_dir=tests/DotnetNativeMcp.Core.Tests/bin/Release/net10.0/fixtures/SampleAot
# R2R managed PE published alongside SampleAot
sample_r2r=tests/fixtures/SampleAot/bin/Release/net10.0/linux-x64/System.Private.CoreLib.dll
sample_r2r_dir=tests/fixtures/SampleAot/bin/Release/net10.0/linux-x64
dotnet-native-cli version --output table
dotnet-native-cli r2r header "$sample_r2r" --allow "$sample_r2r_dir" --output table
dotnet-native-cli r2r runtime-functions "$sample_r2r" --allow "$sample_r2r_dir" --limit 5
dotnet-native-cli disasm "$sample_aot" --allow "$sample_aot_dir" --address 0x401000 --max-instructions 16 --output table
dotnet-native-cli resolve "$sample_aot" --allow "$sample_aot_dir" --address 0x401000 --address 0x401010
dotnet-native-cli callers "$sample_aot" --allow "$sample_aot_dir" --address 0x401000 --image "$sample_aot"
dotnet-native-cli symbols "$sample_aot" --allow "$sample_aot_dir" --limit 10 --output table
dotnet-native-cli imports "$sample_aot" --allow "$sample_aot_dir" --kind libraries --output table
dotnet-native-cli size "$sample_aot" --allow "$sample_aot_dir" --group-by type --top-n 10
dotnet-native-cli size-diff "$sample_aot" "$sample_aot" --allow "$sample_aot_dir" --mstat-group-by category --top-n 5
dotnet-native-cli strings "$sample_aot" --allow "$sample_aot_dir" --min-length 8 --limit 20
dotnet-native-cli retention "$sample_aot" --allow "$sample_aot_dir" --target SampleAot.Program --output table
Install
MCP server
# stdio (local MCP client)
dotnet tool install -g dotnet-native-mcp
dotnet-native-mcp --stdio
# HTTP (sidecar / multi-client)
docker run --rm -p 8789:8080 \
-v /path/to/binaries:/binaries:ro \
ghcr.io/pedrosakuma/dotnet-native-mcp:latest
Standalone CLI release archives
Each GitHub release also includes self-contained single-file archives for the CLI and the server:
dotnet-native-cli-<version>-<rid>.tar.gz/.zipdotnet-native-mcp-<version>-<rid>.tar.gz/.zip
Default port: 8789. Slot picked to continue the convention started by
dotnet-diagnostics-mcp (8787) and dotnet-assembly-mcp (8788).
Running the triad with Docker Compose
To bring up all three servers together (diagnostics on 8787, assembly on 8788, native on 8789):
export BINARIES_DIR=/path/to/your/nativeaot/binaries
export ASSEMBLIES_DIR=/path/to/your/managed/assemblies
docker compose -f deploy/docker-compose.yml up -d
See docs/compose.md for the full guide: env vars, bearer-token setup, MCP client configuration, and how to attach to live processes.
Verifying releases
Every release artifact (NuGet package and self-contained binary archive) is
published with a SLSA build provenance attestation generated by
actions/attest-build-provenance
and signed by Sigstore via GitHub's OIDC issuer. The attestation proves the
artifact was built by this repository on a specific commit by GitHub-hosted
runners — no separate cert to install, no key to rotate.
Verify with the GitHub CLI. The --signer-workflow and --source-ref flags
bind verification to the exact release workflow file and tag, so an
attestation produced by a different workflow in the same repository (e.g. a
hypothetical CI workflow added in a PR) cannot pass these checks:
# Server NuGet package
gh attestation verify dotnet-native-mcp.0.5.4.nupkg \
--repo pedrosakuma/dotnet-native-mcp \
--signer-workflow pedrosakuma/dotnet-native-mcp/.github/workflows/release.yml \
--source-ref refs/tags/v0.5.4
# CLI NuGet package
gh attestation verify dotnet-native-cli.0.5.4.nupkg \
--repo pedrosakuma/dotnet-native-mcp \
--signer-workflow pedrosakuma/dotnet-native-mcp/.github/workflows/release.yml \
--source-ref refs/tags/v0.5.4
# CLI single-file archive
gh attestation verify dotnet-native-cli-0.5.4-linux-x64.tar.gz \
--repo pedrosakuma/dotnet-native-mcp \
--signer-workflow pedrosakuma/dotnet-native-mcp/.github/workflows/release.yml \
--source-ref refs/tags/v0.5.4
# Server single-file archive
gh attestation verify dotnet-native-mcp-0.5.4-linux-x64.tar.gz \
--repo pedrosakuma/dotnet-native-mcp \
--signer-workflow pedrosakuma/dotnet-native-mcp/.github/workflows/release.yml \
--source-ref refs/tags/v0.5.4
For maximum strictness, also pin to the exact release commit with
--source-digest <commit-sha> (look it up on the release page).
A passing verification confirms the build came from pedrosakuma/dotnet-native-mcp,
on the expected tag, produced by .github/workflows/release.yml.
Disk cache
find_native_callers maintains a persistent on-disk xref index so large NativeAOT
binaries are scanned only once across server restarts.
| Detail | Value |
|---|---|
| Location | ~/.cache/dotnet-native-mcp/<build-id>.xref (Linux: $XDG_CACHE_HOME/dotnet-native-mcp/ if set) |
| Cache key | ELF build-id / PE CodeView GUID+Age / Mach-O LC_UUID, falling back to a SHA-256 prefix of the file bytes |
| Format | 4-byte magic NXR1 + 4-byte version + JSON body |
| Invalidation | Automatic on binary rebuild (new build-id → new file). On format upgrade the old file is silently ignored and rebuilt. |
| Eviction | None (files accumulate by build-id). Run rm -rf ~/.cache/dotnet-native-mcp/ to clear manually. |
| Disable | Set DOTNET_NATIVE_MCP_XREF_CACHE=0 to bypass all disk I/O (useful in CI or read-only environments). |
Authentication
HTTP transport supports optional bearer-token auth. Leave it unset for local/dev
back-compat; set either NATIVE_MCP_BEARER_TOKEN or NativeMcp:BearerToken to
require Authorization: Bearer <token> on every /mcp request. /health
remains open. STDIO transport stays unauthenticated.
export NATIVE_MCP_BEARER_TOKEN="replace-me"
dotnet-native-mcp
Loopback-or-token invariant
The HTTP transport enforces a startup-time safety check: if any configured URL
binds to a non-loopback address (anything other than 127.0.0.1, ::1, or
localhost) and no bearer token is configured, the server refuses to
start. This prevents accidentally exposing the MCP tool surface — which can
open arbitrary files on the host — to anyone on the network.
To bind to a non-loopback address you must either:
- Set a bearer token (
NATIVE_MCP_BEARER_TOKEN/NativeMcp:BearerToken/MCP_BEARER_TOKEN), or - Explicitly opt out with
NativeMcp:AllowUnauthenticatedNonLoopback=true— only safe when a trusted reverse proxy in front of the server enforces authentication.
The check inspects Urls, ASPNETCORE_URLS, HTTP_PORTS / HTTPS_PORTS
(and the ASPNETCORE_* aliases), and Kestrel:Endpoints:*:Url configuration
sources.
Trusted-path allowlist (untrusted path hints)
Filesystem paths arrive off the wire from an LLM and are treated as untrusted
display hints, per the handoff contract.
Every path passed to load_native_binary, import_native_manifest,
disassemble (imagePath / ilMapPath) and the sidecar overrides
(mstatPath / dgmlPath) is canonicalised — .. flattened and symlinks /
junctions resolved to their true target — before any file is opened.
Enforcement is opt-in. Configure at least one trusted root to restrict opens
to that subtree (anything outside is refused with path_not_allowed):
# any of these turns enforcement on
export NATIVE_MCP_ALLOWED_ROOTS="/binaries:/app/assemblies" # PATH-separator list
export BINARIES_DIR=/binaries # single root (Compose tier)
# or NativeMcp:AllowedBinaryRoots in appsettings.json / config
When enforcing, the NuGet global packages cache, the .NET shared framework, and the system temp directory are always allowed in addition to your roots so handed-off binaries and staged sidecars keep resolving. Until a root is configured the server stays permissive (paths are still canonicalised) and logs a one-time startup warning.
Resource limits
To bound memory use and adversarial inputs, the server applies the following caps:
| Limit | Value | Applies to |
|---|---|---|
MaxImageBytes |
512 MiB | Whole-image reads (load_native_binary, raw disassembly paths) |
MaxDgmlBytes |
64 MiB | DGML sidecar file size |
MaxIlMapBytes |
16 MiB | .ilmap file size |
MaxManifestEntries |
1024 | import_native_manifest entries per call |
MaxStringMatches |
500,000 | Total matches collected by extract_strings |
MaxExtractedStringChars |
16,384 | Characters materialized for any single extracted string value |
MaxCallerSites |
100,000 | find_native_callers rows returned |
MaxDgmlNodes |
1,000,000 | DGML nodes parsed from a sidecar |
MaxDgmlEdges |
2,000,000 | DGML edges parsed from a sidecar |
MaxIlMapEntries |
1,048,576 | .ilmap entries parsed from a sidecar |
MaxPdbBytes |
64 MiB | PDB files read for SourceLink resolution |
MaxMapFileBytes |
256 MiB | .map sidecar file size |
MaxMapFileEntries |
5,000,000 | .map sidecar entries parsed |
MaxMstatBytes |
256 MiB | .mstat sidecar file size |
MaxMstatAttributions |
5,000,000 | .mstat attributions parsed |
MaxXrefCacheBytes |
256 MiB | xref disk cache size accepted on read |
MaxEmbeddedPdbCacheBytes |
32 MiB | extracted-PDB disk cache size accepted on read |
Error responses
Errors returned to MCP clients are sanitised: NativeError.Detail never carries
Exception.ToString() stack traces, and absolute filesystem paths supplied by
the caller are reduced to their file name. Full exception detail (including the
original path and stack trace) is logged server-side via the
SanitisedError.Sink hook (defaulting to System.Diagnostics.Trace) so
operators can still investigate parse failures.
Building blocks
Iced— MIT, .NET-native x86/x64 disassembler.System.Reflection.PortableExecutable— for PE headers and section reads.System.IO.Pipelines— for streaming reads of large native binaries.- A small ELF reader (Linux NativeAOT binaries are ELF).
- (V1) Capstone P/Invoke for ARM64 disassembly.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
This package has no dependencies.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.0 | 104 | 8/29/2026 |