RePythonNET-MCP
by SEKOIA-IO
README.md
# RePythonNET-MCP
An MCP (Model Context Protocol) server for static analysis and patching of .NET binaries.
Uses [dnlib](https://github.com/0xd4d/dnlib) for IL parsing and editing, and the [ILSpy/dnSpy](https://github.com/icsharpcode/ILSpy) decompiler engine for C# source reconstruction. Exposes 38 tools over a standard MCP interface, making it easy to integrate with any MCP-compatible LLM agent.
Designed to run as a standalone Docker container, and also ships with integration files for SARA — an internal automated malware reverse engineering system developed at Sekoia.io.
---
## Features
- **Decompile** any .NET class or method to readable C#
- **Enumerate** types, methods, fields, imports, resources, entry point
- **Analyse** IL opcodes, call graphs, callers, string literals
- **Detect** P/Invoke declarations, reflection usage, large byte arrays
- **Rename** obfuscated classes, methods, and fields (in-memory, non-destructive)
- **Patch** IL instructions: replace strings, NOP out instructions, bypass checks, invert branches, patch static fields
- **Save** the modified binary to disk
- **Upload** binaries via HTTP, or load directly from a URL
- Multi-session: analyse several binaries in parallel with separate `analysis_id` identifiers
---
## Architecture
```
┌─────────────────────────────────┐
│ Any MCP client / LLM agent │
│ (curl, Python, SARA, etc.) │
└──────────────┬──────────────────┘
│ JSON-RPC over HTTP
▼
┌─────────────────────────────────┐
│ pythonnet-mcp (port 8001) │
│ │
│ server.py │
│ ├── FastMCP (38 tools) │
│ ├── POST /upload │
│ └── GET /download │
│ │
│ Runtime: Python + Mono │
│ .NET libs: dnlib, ILSpy/dnSpy │
└─────────────────────────────────┘
```
The server is stateless across requests but holds in-memory sessions keyed by `analysis_id`. Each session stores the loaded dnlib module and a lazily-initialised ILSpy decompiler instance.
---
## Quick Start
### Docker (recommended)
```bash
# Create the required directories (first time only)
mkdir -p data/samples data/pythonnet_projects logs
# Build and run
docker compose -f docker-compose.yml up --build
# The server listens on http://localhost:8001
```
### Direct (without Docker)
Prerequisites: Python 3.11+, `mono-complete`, and the `.NET` assemblies in `dll/`.
```bash
pip install -r requirements.txt
# Override default paths to avoid needing write access to /data/
export PYTHONNET_PROJECTS_DIR=./data/pythonnet_projects
export SAMPLES_DIR=./data/samples
mkdir -p data/pythonnet_projects data/samples
python server.py --host 0.0.0.0 --port 8001
```
CLI options:
| Option | Default | Description |
|--------|---------|-------------|
| `--host` | `0.0.0.0` | Bind address |
| `--port` | `8001` | Bind port |
| `--binary` | _(none)_ | Optional binary to pre-load at startup |
---
### Claude Desktop Configuration
The file `scripts/wrapper.py` is a wrapper to put on your host.
Then configure Claude Desktop (`claude_desktop_config.json`) to execute this file.
```
{
"mcpServers": {
"pythonnet": {
"command": "<path to python>",
"args": [
"<path to wrapper.py>"
]
}
},
}
```
## Usage
### Upload a binary
You first needs to upload the file using the `/upload` endpoint:
```bash
curl -F file=@malware.dll http://localhost:8001/upload
# {"path": "/data/pythonnet_projects/_uploads/malware.dll", "size_bytes": 45056}
```
You can then ask Claude Desktop to use this MCP service to load and analyze the binary.
You can use `pythonnet_list_uploaded_files` to list already uploaded files.
### Call an MCP tool (raw JSON-RPC)
```bash
curl -s -X POST http://localhost:8001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "pythonnet_load_binary",
"arguments": {
"binary_path": "/data/pythonnet_projects/_uploads/malware.dll",
"analysis_id": "session1"
}
}
}'
```
Session isolation: append `?session_id=<value>` to the URL to keep sessions independent when running parallel analyses from multiple clients.
### List available tools
```bash
curl -s -X POST http://localhost:8001/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
### Download a file
```bash
# Download a patched binary produced by pythonnet_save_binary
curl "http://localhost:8001/download?path=/data/pythonnet_projects/session1/malware_patched.dll" \
-o malware_patched.dll
```
Downloads are restricted to files inside `/data/pythonnet_projects/` and `/data/samples/`.
---
## Tools Reference
All tool names are prefixed with `pythonnet_`. The `analysis_id` parameter defaults to `"default"` and identifies the session.
### Loading
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_load_binary` | `binary_path`, `analysis_id` | Load a .NET binary from disk. Copies it to a project directory before opening so the original is never modified. |
| `pythonnet_load_from_url` | `url`, `analysis_id` | Download a .NET binary from HTTP/HTTPS and load it. |
| `pythonnet_unload_binary` | `analysis_id` | Unload a binary and free its memory. |
| `pythonnet_list_uploaded_files` | `extension` | List binaries in the upload staging area and the samples directory. |
| `pythonnet_list_active_sessions` | — | List all active and on-disk analysis sessions. |
### Enumeration
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_list_all_classes` | `analysis_id`, `namespace_filter` | List all types defined in the binary. |
| `pythonnet_list_all_methods` | `analysis_id`, `class_filter` | List all methods that have a body. |
| `pythonnet_list_all_methods_inside_class` | `class_name`, `analysis_id` | List all methods (including abstract/interface) inside a specific class. |
### Decompilation
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_decompile_class` | `class_name`, `analysis_id` | Decompile an entire class to C# source. |
| `pythonnet_decompile_method` | `class_name`, `method_name`, `analysis_id`, `method_index` | Decompile a single method. Use `method_index` to disambiguate overloads. |
| `pythonnet_decompile_all_classes` | `analysis_id` | Decompile every class in the binary at once. |
### Metadata
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_get_module_info` | `analysis_id` | Assembly name, version, culture, runtime version, target framework. |
| `pythonnet_get_entry_point` | `analysis_id` | Managed entry point (`Main` or `EntryPoint` metadata). |
| `pythonnet_get_imports` | `analysis_id` | All external assemblies and types referenced by the binary. |
| `pythonnet_get_fields` | `class_name`, `analysis_id` | All fields of a class with their types and visibility. |
| `pythonnet_get_custom_attributes` | `class_name`, `analysis_id`, `method_name` | Custom attributes on a class or one of its methods. |
| `pythonnet_get_resources` | `analysis_id` | List all embedded resources. |
| `pythonnet_extract_resource` | `resource_name`, `analysis_id` | Extract a raw embedded resource to a file. |
### IL Analysis
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_get_opcodes` | `class_name`, `method_name`, `analysis_id`, `method_index` | Dump the raw IL instruction listing of a method. |
| `pythonnet_get_method_calls` | `class_name`, `method_name`, `analysis_id`, `method_index` | List all outgoing `call`/`callvirt` instructions from a method. |
| `pythonnet_get_callers` | `class_name`, `method_name`, `analysis_id` | Find all methods that call a given method (reverse call graph). |
| `pythonnet_get_call_graph` | `class_name`, `method_name`, `analysis_id`, `max_depth` | BFS call graph from a root method (default max depth: 5, internal calls only). |
| `pythonnet_find_dead_code` | `analysis_id` | Find methods that are never called from within the binary. |
### String and Name Search
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_get_strings` | `analysis_id`, `min_length` | List all unique `ldstr` string literals across all method bodies. |
| `pythonnet_search_string` | `search_string`, `analysis_id` | Find all methods that reference a given string literal. |
| `pythonnet_search_method_by_name` | `method_name`, `analysis_id` | Search for methods by partial name match across all classes. |
### Behavioral Indicators
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_find_pinvoke` | `analysis_id`, `dll_filter` | List all P/Invoke (`DllImport`) declarations. |
| `pythonnet_find_reflection_calls` | `analysis_id` | Find all uses of `System.Reflection` (Assembly.Load, GetMethod, Invoke, etc.). |
| `pythonnet_find_large_byte_arrays` | `analysis_id`, `min_size` | Find methods that allocate large byte arrays (potential payload buffers; default min: 256 bytes). |
### Renaming (Deobfuscation)
Changes are in-memory only. Call `pythonnet_save_binary` to persist.
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_rename_class` | `class_name`, `new_name`, `analysis_id`, `new_namespace` | Rename a class and optionally its namespace. |
| `pythonnet_rename_method` | `class_name`, `method_name`, `new_method_name`, `analysis_id`, `method_index` | Rename a method. |
| `pythonnet_rename_field` | `class_name`, `field_name`, `new_field_name`, `analysis_id` | Rename a field. |
### IL Patching
Changes are in-memory only. Call `pythonnet_save_binary` to persist.
| Tool | Key Parameters | Description |
|------|---------------|-------------|
| `pythonnet_patch_ldstr` | `class_name`, `method_name`, `il_offset`, `new_string`, `analysis_id` | Replace the string operand of an `ldstr` instruction at a given IL offset. |
| `pythonnet_nop_instructions` | `class_name`, `method_name`, `il_offsets`, `analysis_id` | Replace one or more instructions with `nop`. |
| `pythonnet_patch_return` | `class_name`, `method_name`, `return_value`, `analysis_id` | Replace a method body with a minimal return (useful to bypass a check). |
| `pythonnet_patch_branch` | `class_name`, `method_name`, `il_offset`, `mode`, `analysis_id` | Flip (`invert`), force (`always`/`never`), or NOP a conditional branch. |
| `pythonnet_set_field_constant` | `class_name`, `field_name`, `new_value`, `analysis_id` | Patch the initialization value of a static field in `.cctor`. |
| `pythonnet_save_binary` | `analysis_id`, `output_path` | Write the modified module to disk. |
---
## Volumes and Paths
| Path (in container) | Purpose | Notes |
|--------------------|---------|-------|
| `/data/samples/` | Read-only input samples | Mount your binary directory here |
| `/data/pythonnet_projects/` | Per-session working directories | Created automatically; writable |
| `/data/pythonnet_projects/_uploads/` | HTTP upload staging area | Used by the `/upload` endpoint |
When running standalone, adjust the volume mounts in `docker-compose.yml` to your local directory layout.
---
## Configuration
| Environment Variable | Default | Description |
|---------------------|---------|-------------|
| `WEB_SERVER_PORT` | `8001` | Port the server binds to |
| `PYTHONNET_PROJECTS_DIR` | `/data/pythonnet_projects` | Working directory for analysis sessions (uploads, patched binaries, extracted resources) |
| `SAMPLES_DIR` | `/data/samples` | Read-only input directory scanned by `pythonnet_list_uploaded_files` |
---
## Dependencies
**Python packages** (see `requirements.txt`):
- [`pythonnet`](https://github.com/pythonnet/pythonnet) ≥ 3.0 — Python/.NET CLR interop
- [`fastmcp`](https://github.com/jlowin/fastmcp) — MCP server framework
- [`mcp`](https://github.com/modelcontextprotocol/python-sdk) ≥ 1.0 — MCP SDK
- `click`, `requests`, `aiofiles`
**System** (installed in the Docker image): `mono-complete`
**.NET assemblies** (bundled in `dll/`):
| Assembly | License | Source |
|----------|---------|--------|
| `dnlib.dll` | MIT | [0xd4d/dnlib](https://github.com/0xd4d/dnlib) |
| `ICSharpCode.Decompiler.dll` | MIT | [icsharpcode/ILSpy](https://github.com/icsharpcode/ILSpy) |
| `ICSharpCode.NRefactory*.dll` | MIT | ILSpy / NRefactory |
| `dnSpy.Decompiler*.dll` | **GPL-3.0** | [dnSpy/dnSpy](https://github.com/dnSpy/dnSpy) |
| `dnSpy.Contracts.Logic.dll` | **GPL-3.0** | [dnSpy/dnSpy](https://github.com/dnSpy/dnSpy) |
> **License note:** The dnSpy assemblies are GPL-3.0. If you distribute a modified version of this project that includes those files, the GPL-3.0 terms apply to the distribution. Using the service as-is (running the Docker container) is not affected.
---
## SARA Integration
SARA is an internal automated malware reverse engineering system developed at Sekoia.io. It uses this service as its `.NET` analysis backend. Two additional files support that integration:
### `tools.py` — MCP client
A lightweight HTTP client that runs inside the SARA process and proxies tool calls to this server. It:
- Auto-detects whether it is running inside Docker (uses `http://pythonnet-mcp:8001/mcp`) or on the host (uses `http://localhost:8001/mcp`), and respects the `PYTHONNET_MCP_URL` environment variable.
- Fetches tool definitions from the server at runtime (`tools/list`) with a 5-minute cache — the server is the single source of truth for schemas.
- Normalises legacy relative paths (`data/samples/file.dll` → `/data/samples/file.dll`) before forwarding requests.
- Exposes `PythonnetToolRegistry`, the class that SARA's service registry instantiates per analysis run.
```python
from services.pythonnet.tools import PythonnetToolRegistry
registry = PythonnetToolRegistry(session_id="sara-abc12")
result = registry.execute_tool("pythonnet_list_all_classes", {"analysis_id": "sara-abc12"})
```
### `policy.py` — Tool usage policy
Controls tool call limits and deduplication inside SARA's LangGraph analysis loop. Not needed outside SARA.
| Tool | Limit |
|------|-------|
| `pythonnet_load_binary` | 1 per session |
| `pythonnet_list_all_classes` | 3 + lock_clears |
| `pythonnet_list_all_methods` | 3 + lock_clears |
| `pythonnet_list_all_methods_inside_class` | 20 + (lock_clears × 5) |
| `pythonnet_decompile_class` | 50 + (lock_clears × 10) |
| `pythonnet_decompile_method` | unlimited |
| `pythonnet_rename_class` | 20 + lock_clears |
| `pythonnet_rename_method` | 50 + (lock_clears × 10) |
`lock_clears` is incremented by SARA's loop when it detects the LLM is stuck, progressively relaxing limits.
### Registration in SARA
In `core/services_registry.py`:
```python
def _make_pythonnet_toolset(session_id: str):
from services.pythonnet.tools import PythonnetToolRegistry
return PythonnetToolRegistry(session_id=session_id)
SERVICE_FACTORIES["pythonnet"] = _make_pythonnet_toolset
PIPELINES["pythonnet"] = {
"name": "Pythonnet",
"description": ".NET assembly decompilation and patching",
"supported_formats": ["EXE", "DLL", ".NET"],
"capabilities": ["decompile", "patch", "rename"],
"services": ["pythonnet"],
"available": _pythonnet_available,
"loader_tool": "pythonnet_load_binary",
}
```
### Volume layout in SARA
When running inside SARA's `docker-compose-sara-dev.yml`:
- `/data/samples` is mounted **read-only** in the pythonnet-mcp container. The server never writes there.
- All write operations (working copies, patched binaries, extracted resources) go to `/data/pythonnet_projects/{analysis_id}/`.
---
## License
The Python source files in this project are released under the **MIT License** — see [LICENSE](LICENSE).
The bundled `.NET` assemblies in `dll/` are subject to their own licenses — see the [Dependencies](#dependencies) section above, in particular the GPL-3.0 notice for the dnSpy assemblies.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues