dotnet-decompiler-mcp
# dotnet-decompiler-mcp
An [MCP](https://modelcontextprotocol.io/) server that decompiles and inspects
.NET assemblies to C# source, wrapping the [ICSharpCode.Decompiler](https://github.com/icsharpcode/ILSpy)
engine (ILSpy, v9.0.0.7889) behind a set of callable tools.
Point an MCP client at any .NET DLL and ask an LLM to "decompile `Foo.Bar`,
then list its public methods" — it returns structured results (C# source, type
summaries, symbol matches) the model can reason over.
## What it does
- **Decompile** a whole type or a single member to readable C# source (with
comments — the ILSpy "gold standard" output).
- **Inspect** an assembly: list types, list namespaces with counts, get a
detailed member summary of one type (methods, fields, properties, events,
attributes, base type).
- **Search** for symbols by name across one or more assemblies
(case-insensitive substring, filterable by kind).
- A **`check_env`** diagnostic tool reports the active Python, .NET runtime,
and engine version.
All .NET access is via reflection through [pythonnet](https://pythonnet.github.io/)
on CoreCLR; no .NET SDK install is required on the host beyond the runtime.
## Requirements
- **Python ≥ 3.14**
- **.NET 8 runtime** on the host (the decompiler targets `net8.0`; the bundled
CoreCLR runtime pack is `8.0.6`, the minimum host runtime is `8.0.17`).
- **[uv](https://docs.astral.sh/uv/)** to run the project.
## Install
```bash
git clone <repo-url> dotnet-decompiler-mcp
cd dotnet-decompiler-mcp
uv sync
```
The ILSpy engine DLL ships in `lib/ICSharpCode.Decompiler.dll` — nothing else
to download.
## Environment variables
These are set **automatically** at runtime by the bootstrap
(`runtime/clr.py`); you normally do not need to configure them. They are listed
for completeness / troubleshooting.
| Variable | Value | Purpose |
|---|---|---|
| `PYTHONNET_RUNTIME` | `coreclr` | Selects CoreCLR over Mono. |
| `PYTHONNET_CORECLR_RUNTIME_CONFIG` | path to a temp `*.runtimeconfig.json` | Tells CoreCLR which framework (`Microsoft.NETCore.App` 8.0.6) to load. The file is generated into the system temp dir on first boot. |
If the runtime fails to start, run `check_env` (below) — it reports the
resolved versions.
## MCP client registration
The server speaks MCP over **stdio**. Register it with any MCP client.
### ZCode / OpenCode (`.zcode/config.json`)
```json
{
"mcp": {
"servers": {
"dotnet-decompiler": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--project",
"/absolute/path/to/dotnet-decompiler-mcp",
"python",
"-m",
"dotnet_decompiler_mcp"
],
"env": {}
}
}
}
}
```
Write this to the workspace `.zcode/config.json` or the user-level
`~/.zcode/v2/config.json`, then restart the client.
### Generic `mcpServers` (Claude Desktop and others)
```json
{
"mcpServers": {
"dotnet-decompiler": {
"command": "uv",
"args": [
"run",
"--project",
"/absolute/path/to/dotnet-decompiler-mcp",
"python",
"-m",
"dotnet_decompiler_mcp"
]
}
}
}
```
You can also run it directly to smoke-test the stdio server:
```bash
uv run python -m dotnet_decompiler_mcp
```
It should start without errors and wait on stdin. Ctrl+C to exit.
## Tools
Seven tools are registered. `asm_paths` (where present) is an optional list of
extra assembly search directories used to resolve cross-DLL references; pass it
when the target assembly depends on types in other DLLs.
| Tool | Arguments | Returns | Purpose |
|---|---|---|---|
| `check_env` | *(none)* | `EnvInfo` | Startup diagnostics: Python, .NET runtime, engine version, lib dir. |
| `decompile_type` | `dll`, `type_name`, `asm_paths?` | `DecompileResult` | Decompile a whole type to C# source. |
| `decompile_member` | `dll`, `type_name`, `member_name`, `asm_paths?` | `MemberDecompileResult` | Decompile a single method/field/property/event. |
| `list_types` | `dll`, `namespace?`, `asm_paths?` | `TypeListResult` | List types, optionally filtered by namespace prefix. |
| `list_namespaces` | `dll`, `asm_paths?` | `NamespaceListResult` | Distinct namespaces with type counts. |
| `get_type_summary` | `dll`, `type_name`, `asm_paths?` | `TypeSummary` | Detailed public-member breakdown of one type. |
| `search_symbol` | `dlls`, `query`, `kind?`, `asm_paths?` | `SearchResult` | Case-insensitive symbol search across one or more DLLs. |
Notes:
- `type_name` is a fully-qualified, case-sensitive name (e.g.
`ICSharpCode.Decompiler.DecompilerSettings`).
- `namespace` is a **prefix** filter — `ICSharpCode` matches
`ICSharpCode.Decompiler` but not `ICSharpCodeX`; empty string = all types.
- `search_symbol` `kind` is one of `type` / `method` / `field` / `property` /
`event` / `any` (default `any`).
## Usage examples
These target the engine DLL bundled in `lib/` — the same cases the test suite
pins, so they are known to work. Replace the path with any .NET assembly you
want to explore.
```text
check_env
→ EnvInfo(decompiler_version="9.0.0.7889", net_tfm_target="net8.0", ...)
decompile_type("lib/ICSharpCode.Decompiler.dll",
"ICSharpCode.Decompiler.DecompilerSettings")
→ DecompileResult(source="public class DecompilerSettings : ...", ...)
decompile_member("lib/ICSharpCode.Decompiler.dll",
"ICSharpCode.Decompiler.DecompilerSettings",
"GetMinimumRequiredVersion")
→ MemberDecompileResult(source="public static Version ...", ...)
get_type_summary("lib/ICSharpCode.Decompiler.dll",
"ICSharpCode.Decompiler.DecompilerSettings")
→ TypeSummary(base_type="System.Object",
events=[EventInfo(name="PropertyChanged", ...)], ...)
search_symbol(["lib/ICSharpCode.Decompiler.dll"], "GetMinimum", kind="method")
→ SearchResult(matches=[SymbolMatch(kind="method",
name="GetMinimumRequiredVersion", type="...DecompilerSettings")])
```
## Errors
The engine raises typed exceptions; the tool layer does **not** catch them, so
they surface to the MCP client as `is_error` responses with the exception
message. All engine errors inherit from `DecompilerError`.
| Error | Meaning |
|---|---|
| `DllNotFoundError` | The `dll` path does not exist or is not readable. |
| `TypeNotFoundError` | `type_name` was not found in the assembly. |
| `MemberNotFoundError` | `member_name` was not found in the type. |
| `DecompilationFailedError` | A reflected .NET call raised; the original exception text is appended as `(inner: ...)`. |
| `InvalidArgumentError` | An argument was rejected (e.g. an unknown `kind`). |
## Development
```bash
uv run pytest # 65 tests (incl. real-.NET integration tests)
uv run ruff check # lint (D/E/F/W, Google docstring convention)
```
### Architecture
Four one-way layers, no cycles:
```
config.py constants (single source of truth)
runtime/ CoreCLR bootstrap via pythonnet (init_runtime, ensure_initialised)
engine/ CSharpDecompiler reflection wrappers (cache, decompile, metadata, version, errors)
tools/ thin async wrappers (list[str]|None -> tuple|None), errors bubble up
server.py FastMCP assembly + tool registration
models/ Pydantic response DTOs (anemic, serialization only)
```
Dependency direction: `server → tools → engine → runtime/models/config`.
## Status
Milestones M0–M4 complete: runtime bootstrap, decompile (type/member),
inspect (list/summary) + search, error handling, and documentation.
TDQS
Scored across 7 tools
Each tool has a clearly distinct purpose: environment checking, decompiling types, decompiling members, getting type summaries, listing namespaces, listing types, and searching symbols. No overlap or ambiguity.
All tool names follow a consistent verb_noun snake_case pattern: check_env, decompile_member, decompile_type, get_type_summary, list_namespaces, list_types, search_symbol. The pattern is uniform and predictable.
Seven tools is an ideal size for a decompiler MCP server, covering diagnostics, exploration, and decompilation without being overwhelming or insufficient.
The tools cover the core exploration and decompilation workflow (list namespaces, types, summary, decompile type/member, search). However, missing are options for decompilation settings (e.g., with/without comments, debug info), which would make it fully complete.