https://github.com/pedrosakuma/dotnet-assembly-mcp
MCP server for static navigation of .NET assemblies — types, methods, attributes, decompilation. Companion to dotnet-diagnostics-mcp.
https://github.com/pedrosakuma/dotnet-assembly-mcp
ai-agents assembly decompiler dotnet ilspy llm mcp mcp-server reflection static-analysis
Last synced: 28 days ago
JSON representation
MCP server for static navigation of .NET assemblies — types, methods, attributes, decompilation. Companion to dotnet-diagnostics-mcp.
- Host: GitHub
- URL: https://github.com/pedrosakuma/dotnet-assembly-mcp
- Owner: pedrosakuma
- License: mit
- Created: 2026-05-18T22:14:55.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-05-22T17:25:14.000Z (about 1 month ago)
- Last Synced: 2026-05-31T18:33:29.060Z (about 1 month ago)
- Topics: ai-agents, assembly, decompiler, dotnet, ilspy, llm, mcp, mcp-server, reflection, static-analysis
- Language: C#
- Homepage: https://github.com/pedrosakuma/dotnet-diagnostics-mcp
- Size: 684 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# 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`](https://github.com/pedrosakuma/dotnet-assembly-mcp/releases) — 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`](./docs/handoff-contract.md#35-generic-instantiations).
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`](./docs/consumer-install.md).
### As a global `dotnet tool` (stdio — local MCP clients)
```bash
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)
```bash
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](https://github.com/pedrosakuma/dotnet-diagnostics-mcp/issues/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:
```bash
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`](https://github.com/pedrosakuma/dotnet-diagnostics-mcp/blob/main/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`](https://github.com/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:
```bash
# 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`):
```jsonc
{
"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
```jsonc
{
"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.
```bash
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 must be **absolute**; the index
keys modules by MVID, not by relative path.)
```bash
DLL=$(realpath ./bin/Release/net10.0/MyLib.dll)
# 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
dotnet-assembly-cli --load "$DLL" find-callers b613bdf8-… 0x0600000D
# 1 caller(s) in 1 module (built).
# Display: SampleLib.OrderService+d__6.MoveNext
# Pipe any command through --json for the full MCP-shaped envelope
dotnet-assembly-cli --load "$DLL" find-callers b613bdf8-… 0x0600000D --json | jq '.Data.Callers'
```
### 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.
```bash
# 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 22 MCP tools each have a matching 1:1 subcommand, plus four human-oriented composed commands:
| Group | Commands |
|---|---|
| **Lifecycle** | `load`, `list-assemblies`, `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 --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 ` | Load an assembly into the index before the command runs. Repeatable. Because the CLI is one-shot, a handle (`m::0x…`) only resolves once its module is loaded — `--load` (or a path-taking subcommand such as `find-method`, or a token command's `--assembly`) is how you do that. |
| 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`](https://github.com/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`](./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`](./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`](https://learn.microsoft.com/dotnet/standard/metadata-and-self-describing-components) — metadata-only reads, never `Assembly.Load`
- [`ICSharpCode.Decompiler`](https://github.com/icsharpcode/ILSpy) — full decompiler engine used by ILSpy
- [`ModelContextProtocol`](https://github.com/modelcontextprotocol/csharp-sdk) C# SDK 1.3.0
- [`System.CommandLine`](https://github.com/dotnet/command-line-api) — argument parsing for the `dotnet-assembly-cli` front-end
## License
MIT — see [`LICENSE`](./LICENSE).