dotnet-assembly-cli 0.22.0

dotnet tool install --global dotnet-assembly-cli --version 0.22.0
                    
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-assembly-cli --version 0.22.0
                    
This package contains a .NET tool you can call from the shell/command line.
#tool dotnet:?package=dotnet-assembly-cli&version=0.22.0
                    
nuke :add-package dotnet-assembly-cli --version 0.22.0
                    

dotnet-assembly-mcp

Status: 22 tools shipped, dual transport (stdio + HTTP), packaged as dotnet tool, Docker image, and self-contained single-file binaries. Latest release: v0.14.0 — collapsed find_member_references (field/property/event in one tool) and get_method_il(format=raw|text|scan) (issue #83, breaking).

An MCP server for static navigation of compiled .NET assemblies — types, methods, attributes, signatures, IL, cross-references, and on-demand decompilation — designed as a token-efficient alternative to feeding source code into an LLM context.

Why

When an AI agent needs to understand what a method does, the default today is to read the source file. For a 1,000-LOC class that's ~4–8k tokens, and most of it is irrelevant.

This server lets the agent drill down:

load_assembly(path)                  →  moduleVersionId + method count   (~30 tokens)
get_method(mvid, token)              →  signature, attributes, IL size   (~30 tokens)
get_method_il(mvid, token, format='raw')   →  raw IL hex, instruction count    (~80 tokens)
get_method_il(mvid, token, format='scan')  →  outbound calls / fields / types  (~150 tokens)
decompile_method(mvid, token)        →  C# body, hard-capped             (~200–500 tokens)
find_callers(mvid, token)            →  reverse call graph (intra+cross) (~100 tokens)
get_method_source(mvid, token)       →  PDB file/lines + SourceLink URL  (~40 tokens)

Closed generic instantiations are first-class: get_method / find_callers accept genericTypeArguments so the agent gets int Echo(int) instead of T Echo(T) and find_callers narrows to callers whose MethodSpec matches the requested instantiation. Producers that already have a MethodSpec row in the caller's module can skip the string-rendering step and supply methodSpecModuleVersionId + methodSpecMetadataToken as a fast-path; when both forms are present they are cross-checked and a mismatch yields generic_instantiation_mismatch. See docs/handoff-contract.md §3.5.

The agent pays only for what it actually needs to see.

Install

For the full guide (single-file binaries, systemd / launchd / Scheduled Task supervisors, Kubernetes manifest), see docs/consumer-install.md.

As a global dotnet tool (stdio — local MCP clients)

dotnet tool install -g dotnet-assembly-mcp
dotnet-assembly-mcp --stdio    # speak MCP over STDIN/STDOUT

Requires the .NET 10 runtime. Logs go to STDERR so STDOUT stays a clean JSON-RPC channel.

As a Docker image (HTTP — sidecar / multi-client)

docker run --rm -p 8788:8080 \
  -v /path/to/assemblies:/assemblies:ro \
  ghcr.io/pedrosakuma/dotnet-assembly-mcp:latest
# MCP endpoint: http://localhost:8788/mcp
# Health:       http://localhost:8788/health

Or build locally: docker build -t dotnet-assembly-mcp:dev -f deploy/Dockerfile ..

The diagnostics server emits MethodIdentity / TypeIdentity handles. As of dotnet-diagnostics-mcp #28, the diagnostics server already resolves PDBs locally and stamps SourceLocation directly onto every CPU-sample hotspot identity — so for dev workflows where the source tree is open in your editor, the diagnostics server is sufficient on its own.

Pairing this server with diagnostics is recommended when you also want:

  • Stripped binaries / NativeAOT (no PDB, no inline source).
  • Third-party assemblies you don't have source for.
  • Decompilation (decompile_method), reverse cross-reference (find_callers, find_type_references, …).

Run both together:

export ASSEMBLIES_DIR=/abs/path/to/your/published/binaries
docker compose -f deploy/docker-compose.yml up -d
# diagnostics: http://localhost:8787/mcp
# assembly:    http://localhost:8788/mcp

The same docker-compose.yml ships in pedrosakuma/dotnet-diagnostics-mcp:deploy/docker-compose.yml — bring it up from either checkout. Set MCP_BEARER_TOKEN on the host to gate both servers with one shared token.

Verifying releases

Every release artifact (NuGet package, self-contained binary archive, GHCR container image) 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:

# NuGet package
gh attestation verify dotnet-assembly-mcp.0.18.0.nupkg \
  --repo pedrosakuma/dotnet-assembly-mcp

# Self-contained binary tarball / zip
gh attestation verify dotnet-assembly-mcp-0.18.0-linux-x64.tar.gz \
  --repo pedrosakuma/dotnet-assembly-mcp

# Container image (attestation is published to the registry alongside the image)
gh attestation verify oci://ghcr.io/pedrosakuma/dotnet-assembly-mcp:0.18.0 \
  --repo pedrosakuma/dotnet-assembly-mcp

A passing verification confirms the build came from pedrosakuma/dotnet-assembly-mcp on the expected commit and tag.

Client configuration

Claude Desktop / Cursor / VS Code / Copilot CLI (stdio)

mcp.json (Claude Desktop: ~/Library/Application Support/Claude/claude_desktop_config.json):

{
  "mcpServers": {
    "dotnet-assembly-mcp": {
      "command": "dotnet-assembly-mcp",
      "args": ["--stdio"]
    }
  }
}

If the tool isn't on PATH, point command at the absolute path (e.g. ~/.dotnet/tools/dotnet-assembly-mcp).

Streamable HTTP

{
  "mcpServers": {
    "dotnet-assembly-mcp": {
      "url": "http://localhost:8788/mcp"
    }
  }
}

Tools

All tools share the same response envelope (summary, data, hints, error); hints advertise the suggested next tool so an agent can chain without rediscovering the API. Cross-module xref tools (find_callers, find_type_references, find_member_references, find_string_references, find_attribute_targets, list_derived_types) all use the same matching convention: same-module hits by metadata token, cross-module hits by (assembly simple name, type full name, member signature) against the child module's TypeRef / MemberRef rows.

Discovery & loading

Tool Purpose
load_assembly Load a .dll/.exe from disk (idempotent by MVID)
list_assemblies List currently loaded modules
list_assembly_references Outbound AssemblyRef rows for one module
list_resources ManifestResource rows (embedded resources) for one module
import_assembly_manifest Bulk-register a list of paths under configured roots

Type & method enumeration (Tier-1)

Tool Purpose
list_types Paginated TypeDef listing, filterable by namespace / name / kind
get_type Resolve (mvid, token) to a type summary (base type, interfaces, kind)
list_derived_types Walk subclasses and interface implementers across every loaded module (directOnly / transitive)
list_members Enumerate fields / properties / events of a type
list_methods Paginated MethodDef listing, filterable by declaring type
find_method Module-wide MethodDef search by regex on name / signature
list_attributes Custom attributes on a module / type / method / parameter / field / property / event

Single-method resolution (Tier-2 / Tier-3)

Tool Purpose
get_method Resolve (mvid, token) to a method summary; accepts genericTypeArguments / genericMethodArguments for a closed signature view
get_method_il IL reader for a method, dispatched by format: raw (hex IL bytes + max-stack + counts), text (ildasm-style textual dump, capped + LRU-cached), scan (structured outbound references — calls, fields, types, strings)
decompile_method C# body via ICSharpCode.Decompiler (hard-capped, LRU-cached)
decompile_type C# decompilation of a whole TypeDef — members in declaration order (hard-capped, LRU-cached)
get_method_source PDB-resolved file/lines plus SourceLink URL (embedded PDB or sibling .pdb)

Reverse cross-reference (Tier-4)

Tool Purpose
find_callers Every method whose IL calls a given method; narrows by instantiation when genericMethodArguments is supplied
find_type_references Every site referencing a TypeDef (field/parameter/return/local types + newobj / castclass / isinst / box / ldtoken / generic args)
find_member_references Inbound xref for a field, property, or event — dispatched by handle prefix (f: / p: / e:); accessor narrows property to getter/setter and event to add/remove/raise
find_string_references Every method whose IL emits ldstr for a given literal (exact / contains / regex)
find_attribute_targets Reverse custom-attribute index: every assembly/type/method/parameter/field/property/event bearing a given attribute

CLI (human-driven front-end)

The same engine ships as a standalone terminal tool, dotnet-assembly-cli, for when you (not an agent) want to navigate an assembly. It is a thin shell over the same orchestration the MCP server uses — every MCP tool has a matching subcommand — but renders human-readable text by default instead of an MCP envelope.

dotnet tool install -g dotnet-assembly-cli
dotnet-assembly-cli --help        # list every subcommand
dotnet-assembly-cli list-types --help

A worked walkthrough

Start from a path, drill down to a method, then pivot through the call graph — the same loop an agent runs, but readable in your terminal. Paths may be relative or absolute (and a leading ~ is expanded) — the CLI resolves them against your current directory before handing an absolute path to the engine.

DLL=./bin/Release/net10.0/MyLib.dll   # relative paths are fine

# 1. What's in here? (a path-taking command loads the module for you)
dotnet-assembly-cli list-types "$DLL"
#   25 type(s).
#   ...
#     FullName: SampleLib.OrderService
#     Handle:   t:b613bdf8-…:0x02000007

# 2. Find a method by name regex — gives you its (mvid, token) + handle
dotnet-assembly-cli find-method "$DLL" "Process"
#   3 match(es) for /Process/.
#     Handle:    m:b613bdf8-…:0x0600000D
#     Signature: int SampleLib.OrderService.Process(int)

# 3. Decompile it (--assembly loads the module before resolving the token)
dotnet-assembly-cli decompile-method b613bdf8-… 0x0600000D --assembly "$DLL"
#   SampleLib.OrderService.Process — 240 chars of C#.
#   Source:
#     public int Process(int orderId) { _counter++; … }

# 4. Who calls it? --load primes the index so the handle resolves. As a
#    recursive global option it can go before OR after the subcommand.
dotnet-assembly-cli find-callers b613bdf8-… 0x0600000D --load "$DLL"
#   1 caller(s) in 1 module (built).
#     Display: SampleLib.OrderService+<ProcessAsync>d__6.MoveNext

# Pipe any command through --json for the full MCP-shaped envelope
dotnet-assembly-cli find-callers b613bdf8-… 0x0600000D --load "$DLL" --json | jq '.Data.Callers'

The CLI is stateless per-invocation. Each run builds a fresh index that is discarded on exit. There is therefore no standalone load or list-assemblies subcommand — they would have no meaning across processes — and everything must reach the index within the same command line: pass a path positional, a method command's --assembly, or one or more --load <path>. Cross-module queries (find-callers, find-type-references, find-string-references, …) only see the modules you loaded — their output reports the corpus size (… in N module(s)) so an empty result over 1 module is your cue to --load the rest of the app.

Shortcut: explain-type / explain-method

Steps 1–3 above chase a handle and a token by hand — fine for an agent, tedious for a human. The two composed commands collapse that loop: give them an assembly plus a type name (and optionally a method name) and they resolve everything internally.

# Whole-type overview in one shot: summary, attributes, members and methods grouped.
dotnet-assembly-cli explain-type "$DLL" SampleLib.OrderService

# A method by name — every overload, each with its source location (file:line via PDB).
dotnet-assembly-cli explain-method "$DLL" SampleLib.OrderService Process

# Add --decompile to print the C# body under each overload.
dotnet-assembly-cli explain-method "$DLL" SampleLib.OrderService Compute --decompile

# Who transitively calls a method? A recursive caller tree, resolved by name.
dotnet-assembly-cli callgraph "$DLL" SampleLib.OrderService Compute --depth 3

# What changed in the public surface between two builds of an assembly?
dotnet-assembly-cli diff-assemblies "$OLD_DLL" "$NEW_DLL"

explain-method matches the method name exactly by default (and lists near-misses if there is none); pass --contains for substring matching. Both honour the global --json flag, which emits the full AssemblyResult envelope instead of the human text view.

callgraph builds one tree per matched overload, drawing each method's (transitive) callers across all loaded modules. Bound it with --depth (caller levels, default 3) and --max-nodes (total nodes, default 200); nodes are marked [cycle] for recursion and [more callers not shown] when the depth limit is reached. It is a MethodDef/IL call-path tree, so generic methods appear once (not per closed instantiation).

diff-assemblies compares the externally-visible public surface of two assemblies (a type is visible only when its whole declaring chain is public): types added / removed, and, for types in both, public / protected members added / removed / signature-changed plus type-shape changes (kind / base / interfaces). Member identity is name + generic arity + parameter list, so a return-type, visibility or modifier (static / virtual / abstract / sealed / readonly / const) change on the same signature is reported as a change rather than an add + remove. Property / event accessors appear as their get_ / set_ / add_ / remove_ methods. Finding differences still exits 0 (a diff is not an error); only an unreadable input assembly exits 1. Type identity is compared by full name (signatures render type references by full name without assembly identity), so a type that keeps its full name but moves to a different assembly — e.g. a dependency version swap or type forward — is not flagged as a change.

Subcommands

The CLI exposes 20 of the 22 MCP tools as 1:1 subcommands, plus four human-oriented composed commands. The two stateful lifecycle tools (load_assembly, list_assemblies) are intentionally omitted: in a one-shot CLI they have no standalone meaning (use the global --load <path> option to prime the index instead).

Group Commands
Lifecycle import-manifest
Methods get-method, decompile-method, decompile-type, get-method-il, list-methods, find-method, find-callers, get-method-source
Types list-types, list-assembly-references, list-resources, list-attributes, get-type, list-derived-types, list-members
References find-string-references, find-attribute-targets, find-member-references, find-type-references
Analysis (composed) explain-type, explain-method, callgraph, diff-assemblies

Run dotnet-assembly-cli <command> --help for each command's arguments and options.

Global options & exit codes

Two options are honoured by every subcommand:

Option Effect
--json Emit the full AssemblyResult envelope as indented JSON (scriptable; identical to the MCP data). Without it, you get a human-readable rendering of the result.
--load <path> Load an assembly (relative or absolute path) into this invocation's index before the command runs. Repeatable, and — being recursive — may appear before or after the subcommand. Because the CLI is one-shot, a handle (m:<mvid>:0x…) only resolves once its module is loaded, and cross-module queries only search loaded modules; --load (or a path-taking subcommand such as find-method, or a token command's --assembly) is how you prime the index.
Exit code Meaning
0 Success.
1 The operation returned an error result (e.g. unknown MVID, absolute-path violation), or no command/an unknown command was given. The error message is printed to stderr.
2 Invalid argument value (e.g. an unparseable --kind / --mode).

The architecture: a shared DotnetAssemblyMcp.Application project holds the tool orchestration; the MCP Server and the Cli are both thin hosts over it, so the two never drift.

Companion project

Scope-disjoint from pedrosakuma/dotnet-diagnostics-mcp, which performs dynamic diagnostics (attach, EventPipe sampling, GC, exceptions) on a running .NET process. Together they form a closed loop:

[dotnet-diagnostics-mcp]            [dotnet-assembly-mcp]
   ──────────────────────             ──────────────────────
   list_dotnet_processes              load_assembly
   collect_cpu_sample        ──┐  ┌─→ get_method
   collect_exceptions          │  │   get_method_il (format='scan')
                               │  │   decompile_method
                               ▼  │   find_callers
                        (MethodIdentity)

The handoff contract — MethodIdentity = (moduleVersionId, metadataToken) plus optional genericTypeArguments (§3.5) — lives in docs/handoff-contract.md and is also served at assembly://contract/method-identity as an MCP resource.

MCP resources

In addition to the tool surface, the server publishes a small set of read-only resources that MCP clients can subscribe to or fetch directly. None of them require a tool call:

URI Content
assembly://contract/method-identity Full text of docs/handoff-contract.md — the producer/consumer wire contract for MethodIdentity.
assembly://manifest/loaded JSON array of every currently-loaded module (mvid, path, methodCount). Mirrors list_assemblies without consuming a tool slot.
assembly://manifest/loaded/{mvid} JSON object for one module by MVID. Returns 404-style empty body when the MVID isn't loaded.

Clients that support resource subscriptions get a notification whenever the loaded-module set changes (e.g. after load_assembly or a file-watcher reload).

This server does not replace SourceLink / TraceLog source resolution. It is what the agent reaches for when:

  • the deployed binary has no PDB or no SourceLink,
  • the target is a third-party NuGet dependency,
  • the runtime is NativeAOT-trimmed and metadata at runtime is sparse,
  • or the agent just wants a structural overview without pulling 8 KB of source.

get_method_source is the second-chance source resolver: it reads the on-disk PDB (embedded portable PDB first, then sibling .pdb) so the agent doesn't need a separate SourceLink fetch when one is available locally.

Building blocks

License

MIT — see LICENSE.

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.22.0 46 6/4/2026
0.21.0 48 6/3/2026
0.20.1 46 6/3/2026
0.20.0 48 6/3/2026