dotnet-assembly-cli
0.22.0
dotnet tool install --global dotnet-assembly-cli --version 0.22.0
dotnet new tool-manifest
dotnet tool install --local dotnet-assembly-cli --version 0.22.0
#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— collapsedfind_member_references(field/property/event in one tool) andget_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 ..
Joint with dotnet-diagnostics-mcp (recommended, optional)
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
loadorlist-assembliessubcommand — 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 over1 moduleis your cue to--loadthe 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).
Where it complements SourceLink
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
System.Reflection.Metadata— metadata-only reads, neverAssembly.LoadICSharpCode.Decompiler— full decompiler engine used by ILSpyModelContextProtocolC# SDK 1.3.0System.CommandLine— argument parsing for thedotnet-assembly-clifront-end
License
MIT — see LICENSE.
| 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.