dotnet-native-mcp 0.5.6

dotnet tool install --global dotnet-native-mcp --version 0.5.6
                    
This package contains a .NET tool you can call from the shell/command line.
dotnet new tool-manifest
                    
if you are setting up this repo
dotnet tool install --local dotnet-native-mcp --version 0.5.6
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=dotnet-native-mcp&version=0.5.6
                    
nuke :add-package dotnet-native-mcp --version 0.5.6
                    

dotnet-native-mcp

Status: V1 in progress. Eleven 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. See the V0 tracking issue.

MCP server for navigating native .NET binaries — NativeAOT, R2R-only, single-file native — when ECMA-335 metadata is stripped or absent. 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 (11 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. 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, or method.
explain_retention Read the DGML reachability sidecar emitted by NativeAOT and return the shortest root → target path that kept a type or method reachable.
compare_native_binaries Diff two loaded images: build-id, format, arch, file/section size deltas, added/removed/size-changed symbols.
disassemble Iced x86/x64 disassembly with CALL/JMP cross-ref hints. Default 64 instructions, capped at 2048. ARM64 returns disassembly_unsupported. 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 image and return every CALL/JMP 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. ARM64 returns disassembly_unsupported. resolveSource=true (default) annotates each call site with file:line; set false to skip PDB I/O for large binaries.

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.

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?".

Install

# 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

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:

# 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

# Self-contained binary tarball / zip
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:

  1. Set a bearer token (NATIVE_MCP_BEARER_TOKEN / NativeMcp:BearerToken / MCP_BEARER_TOKEN), or
  2. 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.

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

This package has no dependencies.

Version Downloads Last Updated
0.5.6 137 5/22/2026
0.5.5 120 5/22/2026
0.5.4 120 5/22/2026
0.5.3 108 5/22/2026
0.5.2 118 5/21/2026
0.5.1 105 5/21/2026
0.5.0 108 5/21/2026
0.4.2 118 5/21/2026
0.4.1 113 5/21/2026