Follows the warehack.ing cookie-cutter pattern (Caddy + multi-stage Docker). 34 content pages covering Getting Started, 7 Guides, 16 Tool Reference pages, MCP Prompts, Data Models, Changelog, and Development docs. Steel blue + amber theme with Pagefind search and Mermaid diagram support.
110 lines
5.3 KiB
Plaintext
110 lines
5.3 KiB
Plaintext
---
|
|
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.
|
|
|
|
<Aside type="note">
|
|
The module-level `_cached_wrapper` global is intentionally used instead of
|
|
FastMCP's lifespan context. This avoids complex context-threading through
|
|
every tool function while still providing lazy initialization and clean
|
|
shutdown (the lifespan clears the cache on exit).
|
|
</Aside>
|