--- title: Architecture description: Internal architecture of mcilspy -- modules, data flow, and key design patterns. --- import { Aside } from "@astrojs/starlight/components"; 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 ```mermaid 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 | 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 ### 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. ```python _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_wrapper ``` ### 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`: ```python DECOMPILE_TIMEOUT_SECONDS: float = 300.0 # 5 minutes ``` If a process exceeds this limit, it is killed and a clear error message is returned to the MCP client. ### 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: 1. Standard PATH (via `shutil.which`) 2. `~/.dotnet/tools/ilspycmd` (default for `dotnet tool install --global`) 3. Windows `%USERPROFILE%\.dotnet\tools\` when it differs from `~` ### 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 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 All tools use a consistent `_format_error()` helper that produces messages in the format: ``` **Error** (context): description ``` This makes errors easy to parse in both conversational and programmatic contexts. ## Entry Points The package defines two entry points: - **CLI**: `mcilspy` command (via `[project.scripts]` in pyproject.toml) calls `server:main()`, which starts the FastMCP server on stdio transport. - **Module**: `python -m mcilspy` triggers `__main__.py`, which also calls `main()`. Both paths print a startup banner to stderr (stdout is reserved for the MCP JSON-RPC protocol) and then enter the FastMCP event loop.