Architecture
mcilspy is structured as a thin MCP layer over two independent backends: ilspycmd for full decompilation and dnfile for direct metadata parsing. The design keeps the FastMCP tool definitions separate from the backend logic, so either engine can be used (or replaced) independently.
Data Flow
Section titled “Data Flow”flowchart LR
Client["MCP Client"]
Server["server.py\n(FastMCP tools)"]
Wrapper["ilspy_wrapper.py\n(async subprocess)"]
ILSpy["ilspycmd CLI"]
MetaReader["metadata_reader.py\n(dnfile parser)"]
PE["PE/.NET binary"]
Client -->|"JSON-RPC"| Server
Server -->|"decompilation tools"| Wrapper
Wrapper -->|"create_subprocess_exec"| ILSpy
ILSpy -->|"reads"| PE
Server -->|"metadata tools"| MetaReader
MetaReader -->|"dnfile.dnPE"| PE
Decompilation tools flow through the ILSpyWrapper, which manages ilspycmd as an async subprocess. Metadata tools bypass ilspycmd entirely and read PE metadata tables through dnfile. Both paths share the same Pydantic models for type-safe data exchange.
Module Map
Section titled “Module Map”| Module | Lines | Responsibility |
|---|---|---|
server.py | 2,022 | FastMCP tool definitions for all 16 tools, 2 prompts, lifespan management, input validation, and error formatting |
ilspy_wrapper.py | 767 | Async subprocess wrapper around ilspycmd — builds CLI arguments, runs the process, parses stdout/stderr, enforces timeouts |
metadata_reader.py | 684 | dnfile-based PE metadata parsing — reads MethodDef, Field, Property, Event, ManifestResource, and Assembly tables directly |
models.py | 263 | Pydantic BaseModel classes for all request/response types, plus LanguageVersion and EntityType enums |
il_parser.py | 147 | Post-processing extraction of individual methods from type-level IL or C# output (used by decompile_method) |
constants.py | 54 | Shared configuration values — timeouts, output limits, search limits, entity type lists |
utils.py | 50 | Helper for locating the ilspycmd binary across platforms (PATH, ~/.dotnet/tools, Windows USERPROFILE) |
Key Design Patterns
Section titled “Key Design Patterns”Lazy Initialization
Section titled “Lazy Initialization”The ILSpyWrapper singleton is not created at server startup. It is lazily initialized on the first call to a decompilation tool via get_wrapper(). This means the server starts successfully even if ilspycmd is not installed — the metadata tools work fine, and the diagnostics tools (check_ilspy_installation, install_ilspy) remain available.
_cached_wrapper: ILSpyWrapper | None = None
def get_wrapper(ctx: Context | None = None) -> ILSpyWrapper: global _cached_wrapper if _cached_wrapper is None: _cached_wrapper = ILSpyWrapper() return _cached_wrapper5-Minute Subprocess Timeout
Section titled “5-Minute Subprocess Timeout”Every ilspycmd invocation is wrapped with asyncio.wait_for(..., timeout=300.0). This prevents runaway processes from corrupted or adversarial assemblies. The timeout constant lives in constants.py:
DECOMPILE_TIMEOUT_SECONDS: float = 300.0 # 5 minutesIf a process exceeds this limit, it is killed and a clear error message is returned to the MCP client.
PATH Auto-Discovery
Section titled “PATH Auto-Discovery”MCP servers often run in restricted environments where ~/.dotnet/tools is not in PATH. The find_ilspycmd_path() utility in utils.py checks three locations:
- Standard PATH (via
shutil.which) ~/.dotnet/tools/ilspycmd(default fordotnet tool install --global)- Windows
%USERPROFILE%\.dotnet\tools\when it differs from~
Secure Subprocess Execution
Section titled “Secure Subprocess Execution”All subprocess calls use asyncio.create_subprocess_exec (not create_subprocess_shell). Assembly paths are passed as individual arguments, never interpolated into shell strings. This eliminates shell injection as an attack vector — important because assembly paths come from untrusted MCP client input.
Output Truncation Guard
Section titled “Output Truncation Guard”The decompile_assembly tool accepts a max_output_chars parameter (default 100,000). When decompiled output exceeds this limit, the full text is written to a temp file and the tool returns a truncated preview with the file path. This prevents large assemblies from overwhelming the MCP client’s context window. Setting max_output_chars=0 disables truncation.
Standardized Error Format
Section titled “Standardized Error Format”All tools use a consistent _format_error() helper that produces messages in the format:
**Error** (context): descriptionThis makes errors easy to parse in both conversational and programmatic contexts.
Entry Points
Section titled “Entry Points”The package defines two entry points:
- CLI:
mcilspycommand (via[project.scripts]in pyproject.toml) callsserver:main(), which starts the FastMCP server on stdio transport. - Module:
python -m mcilspytriggers__main__.py, which also callsmain().
Both paths print a startup banner to stderr (stdout is reserved for the MCP JSON-RPC protocol) and then enter the FastMCP event loop.