An MCP (Model Context Protocol) server for static analysis and patching of .NET binaries.
Uses dnlib for IL parsing and editing, and the ILSpy/dnSpy 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.
- 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_ididentifiers
┌─────────────────────────────────┐
│ 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.
# 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:8001Prerequisites: Python 3.11+, mono-complete, and the .NET assemblies in dll/.
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 8001CLI options:
| Option | Default | Description |
|---|---|---|
--host |
0.0.0.0 |
Bind address |
--port |
8001 |
Bind port |
--binary |
(none) | Optional binary to pre-load at startup |
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>"
]
}
},
}
You first needs to upload the file using the /upload endpoint:
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.
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.
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 patched binary produced by pythonnet_save_binary
curl "http://localhost:8001/download?path=/data/pythonnet_projects/session1/malware_patched.dll" \
-o malware_patched.dllDownloads are restricted to files inside /data/pythonnet_projects/ and /data/samples/.
All tool names are prefixed with pythonnet_. The analysis_id parameter defaults to "default" and identifies the session.
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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. |
| 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). |
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. |
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. |
| 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.
| 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 |
Python packages (see requirements.txt):
pythonnet≥ 3.0 — Python/.NET CLR interopfastmcp— MCP server frameworkmcp≥ 1.0 — MCP SDKclick,requests,aiofiles
System (installed in the Docker image): mono-complete
.NET assemblies (bundled in dll/):
| Assembly | License | Source |
|---|---|---|
dnlib.dll |
MIT | 0xd4d/dnlib |
ICSharpCode.Decompiler.dll |
MIT | icsharpcode/ILSpy |
ICSharpCode.NRefactory*.dll |
MIT | ILSpy / NRefactory |
dnSpy.Decompiler*.dll |
GPL-3.0 | dnSpy/dnSpy |
dnSpy.Contracts.Logic.dll |
GPL-3.0 | 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 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:
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 (useshttp://localhost:8001/mcp), and respects thePYTHONNET_MCP_URLenvironment 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.
from services.pythonnet.tools import PythonnetToolRegistry
registry = PythonnetToolRegistry(session_id="sara-abc12")
result = registry.execute_tool("pythonnet_list_all_classes", {"analysis_id": "sara-abc12"})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.
In core/services_registry.py:
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",
}When running inside SARA's docker-compose-sara-dev.yml:
/data/samplesis 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}/.
The Python source files in this project are released under the MIT License — see LICENSE.
The bundled .NET assemblies in dll/ are subject to their own licenses — see the Dependencies section above, in particular the GPL-3.0 notice for the dnSpy assemblies.