Compare commits
2 Commits
ad72b013ca
...
ff365b0c44
| Author | SHA1 | Date | |
|---|---|---|---|
| ff365b0c44 | |||
| f15905b350 |
29
README.md
29
README.md
@ -79,6 +79,7 @@ Learn from decompiled implementations when documentation falls short.
|
|||||||
| Tool | What It Does | Requires ilspycmd? |
|
| Tool | What It Does | Requires ilspycmd? |
|
||||||
|------|--------------|:------------------:|
|
|------|--------------|:------------------:|
|
||||||
| `decompile_assembly` | Full C# source recovery (with PDB generation) | Yes |
|
| `decompile_assembly` | Full C# source recovery (with PDB generation) | Yes |
|
||||||
|
| `decompile_method` | Extract a specific method from a type | Yes |
|
||||||
| `list_types` | Enumerate classes, interfaces, enums | Yes |
|
| `list_types` | Enumerate classes, interfaces, enums | Yes |
|
||||||
| `search_types` | Find types by name pattern | Yes |
|
| `search_types` | Find types by name pattern | Yes |
|
||||||
| `search_strings` | Find hardcoded strings (URLs, keys) | No |
|
| `search_strings` | Find hardcoded strings (URLs, keys) | No |
|
||||||
@ -95,7 +96,7 @@ Learn from decompiled implementations when documentation falls short.
|
|||||||
| `install_ilspy` | Auto-install .NET SDK + ilspycmd | No |
|
| `install_ilspy` | Auto-install .NET SDK + ilspycmd | No |
|
||||||
|
|
||||||
**7 tools work immediately** (via [dnfile](https://github.com/malwarefrank/dnfile)) — no .NET SDK required.
|
**7 tools work immediately** (via [dnfile](https://github.com/malwarefrank/dnfile)) — no .NET SDK required.
|
||||||
**8 more tools** unlock with `ilspycmd` for full decompilation power.
|
**9 more tools** unlock with `ilspycmd` for full decompilation power.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -139,7 +140,7 @@ dotnet tool install --global ilspycmd
|
|||||||
- [Features at a Glance](#features-at-a-glance)
|
- [Features at a Glance](#features-at-a-glance)
|
||||||
- [Installation Options](#installation-options)
|
- [Installation Options](#installation-options)
|
||||||
- [Tool Reference](#tool-reference)
|
- [Tool Reference](#tool-reference)
|
||||||
- [Decompilation Tools](#decompilation-tools-requires-ilspycmd)
|
- [Decompilation & Extraction Tools](#decompilation-tools-requires-ilspycmd)
|
||||||
- [Metadata Tools](#metadata-tools-no-ilspycmd-required)
|
- [Metadata Tools](#metadata-tools-no-ilspycmd-required)
|
||||||
- [Installation Tools](#installation--diagnostics)
|
- [Installation Tools](#installation--diagnostics)
|
||||||
- [Configuration](#configuration)
|
- [Configuration](#configuration)
|
||||||
@ -177,6 +178,30 @@ The primary reverse-engineering tool. Decompiles .NET assemblies to readable C#.
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary><strong>decompile_method</strong> — Extract a specific method from a type</summary>
|
||||||
|
|
||||||
|
When a type is too large to return in full, use this to extract just the method you need.
|
||||||
|
Decompiles the containing type, then extracts the named method. IL mode (default) is most reliable.
|
||||||
|
|
||||||
|
| Parameter | Required | Description |
|
||||||
|
|-----------|:--------:|-------------|
|
||||||
|
| `assembly_path` | Yes | Path to .dll or .exe |
|
||||||
|
| `type_name` | Yes | Fully qualified type (e.g., `MyNamespace.MyClass`) |
|
||||||
|
| `method_name` | Yes | Method to extract (all overloads returned) |
|
||||||
|
| `show_il_code` | No | Output IL instead of C# (default: true) |
|
||||||
|
| `language_version` | No | C# version (default: Latest) |
|
||||||
|
| `use_pdb_variable_names` | No | Use original variable names from PDB |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"assembly_path": "/path/to/MyApp.dll",
|
||||||
|
"type_name": "MyApp.Core.CyBGElaborator",
|
||||||
|
"method_name": "DoElaborate"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>list_types</strong> — Enumerate types in an assembly</summary>
|
<summary><strong>list_types</strong> — Enumerate types in an assembly</summary>
|
||||||
|
|
||||||
|
|||||||
73
docs/API.md
73
docs/API.md
@ -4,7 +4,7 @@ This document provides detailed API documentation for mcilspy.
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
mcilspy provides a Model Context Protocol (MCP) interface to the ILSpy .NET decompiler. It exposes **15 tools** and two prompts for interacting with .NET assemblies.
|
mcilspy provides a Model Context Protocol (MCP) interface to the ILSpy .NET decompiler. It exposes **16 tools** and two prompts for interacting with .NET assemblies.
|
||||||
|
|
||||||
### Tool Categories
|
### Tool Categories
|
||||||
|
|
||||||
@ -46,6 +46,7 @@ Decompiles a .NET assembly to C# source code. This is the primary tool for rever
|
|||||||
| `nested_directories` | boolean | ✗ | false | Use nested directories for namespaces |
|
| `nested_directories` | boolean | ✗ | false | Use nested directories for namespaces |
|
||||||
| `generate_pdb` | boolean | ✗ | false | Generate portable PDB file (requires `output_dir`) |
|
| `generate_pdb` | boolean | ✗ | false | Generate portable PDB file (requires `output_dir`) |
|
||||||
| `use_pdb_variable_names` | boolean | ✗ | false | Use original variable names from existing PDB |
|
| `use_pdb_variable_names` | boolean | ✗ | false | Use original variable names from existing PDB |
|
||||||
|
| `max_output_chars` | integer | ✗ | 100000 | Max characters to return inline. Output exceeding this is saved to a temp file with a truncated preview. Set to 0 to disable. |
|
||||||
|
|
||||||
**Language Versions:**
|
**Language Versions:**
|
||||||
- `CSharp1` through `CSharp12_0`
|
- `CSharp1` through `CSharp12_0`
|
||||||
@ -68,10 +69,52 @@ Decompiles a .NET assembly to C# source code. This is the primary tool for rever
|
|||||||
**Response:**
|
**Response:**
|
||||||
Returns decompiled C# source code as text, or information about saved files if `output_dir` is specified.
|
Returns decompiled C# source code as text, or information about saved files if `output_dir` is specified.
|
||||||
|
|
||||||
### 2. list_types
|
**Output Truncation:** When output exceeds `max_output_chars`, the full output is saved to a temporary file and the response includes:
|
||||||
|
- A truncation notice with character counts
|
||||||
|
- The path to the full output file
|
||||||
|
- Recovery options (read file, use `decompile_method`, adjust limit)
|
||||||
|
- A preview of the first portion of the output
|
||||||
|
|
||||||
|
### 2. decompile_method
|
||||||
|
|
||||||
|
Extracts a specific method from a .NET type. Decompiles the containing type, then uses pattern matching to extract just the method(s) you need. Handles overloads automatically.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) |
|
||||||
|
| `type_name` | string | ✓ | - | Fully qualified type name (e.g., "MyNamespace.MyClass") |
|
||||||
|
| `method_name` | string | ✓ | - | Method name to extract (all overloads returned) |
|
||||||
|
| `show_il_code` | boolean | ✗ | true | Output IL bytecode (default, more reliable for extraction) |
|
||||||
|
| `language_version` | string | ✗ | "Latest" | C# language version to use |
|
||||||
|
| `use_pdb_variable_names` | boolean | ✗ | false | Use original variable names from existing PDB |
|
||||||
|
|
||||||
|
**IL Extraction** (default, `show_il_code=true`): Uses ECMA-335 standard delimiters — `.method` directives and `} // end of method` comments — for reliable extraction.
|
||||||
|
|
||||||
|
**C# Extraction** (`show_il_code=false`): Uses method signature matching with brace-depth counting. Works well but may miss edge cases with unconventional formatting.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "decompile_method",
|
||||||
|
"arguments": {
|
||||||
|
"assembly_path": "/path/to/MyAssembly.dll",
|
||||||
|
"type_name": "MyNamespace.CyBGElaborator",
|
||||||
|
"method_name": "DoElaborate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
Returns the extracted method body with code fencing. If multiple overloads match, all are returned with overload labels.
|
||||||
|
|
||||||
|
### 3. list_types
|
||||||
|
|
||||||
Lists types (classes, interfaces, structs, etc.) in a .NET assembly. Typically the **first tool to use** when analyzing an unknown assembly.
|
Lists types (classes, interfaces, structs, etc.) in a .NET assembly. Typically the **first tool to use** when analyzing an unknown assembly.
|
||||||
|
|
||||||
|
> **Note:** Tool numbering in this document reflects the addition of `decompile_method` at position 2.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
|
|
||||||
| Parameter | Type | Required | Default | Description |
|
| Parameter | Type | Required | Default | Description |
|
||||||
@ -104,7 +147,7 @@ Returns a formatted list of types organized by namespace, including:
|
|||||||
- Type kind (Class, Interface, etc.)
|
- Type kind (Class, Interface, etc.)
|
||||||
- Namespace
|
- Namespace
|
||||||
|
|
||||||
### 3. search_types
|
### 4. search_types
|
||||||
|
|
||||||
Search for types by name pattern. Essential for finding specific classes in large assemblies.
|
Search for types by name pattern. Essential for finding specific classes in large assemblies.
|
||||||
|
|
||||||
@ -142,7 +185,7 @@ Search for types by name pattern. Essential for finding specific classes in larg
|
|||||||
**Response:**
|
**Response:**
|
||||||
Returns matching types grouped by namespace with full names for use with `decompile_assembly`.
|
Returns matching types grouped by namespace with full names for use with `decompile_assembly`.
|
||||||
|
|
||||||
### 4. search_strings
|
### 5. search_strings
|
||||||
|
|
||||||
Search for string literals in assembly code. Crucial for reverse engineering - finds hardcoded strings.
|
Search for string literals in assembly code. Crucial for reverse engineering - finds hardcoded strings.
|
||||||
|
|
||||||
@ -179,7 +222,7 @@ Search for string literals in assembly code. Crucial for reverse engineering - f
|
|||||||
**Response:**
|
**Response:**
|
||||||
Returns matching code lines grouped by type, with context about the containing method.
|
Returns matching code lines grouped by type, with context about the containing method.
|
||||||
|
|
||||||
### 5. generate_diagrammer
|
### 6. generate_diagrammer
|
||||||
|
|
||||||
Generates an interactive HTML diagram showing assembly type relationships.
|
Generates an interactive HTML diagram showing assembly type relationships.
|
||||||
|
|
||||||
@ -208,7 +251,7 @@ Generates an interactive HTML diagram showing assembly type relationships.
|
|||||||
**Response:**
|
**Response:**
|
||||||
Returns success status and output directory path. The HTML file can be opened in a web browser to view the interactive diagram.
|
Returns success status and output directory path. The HTML file can be opened in a web browser to view the interactive diagram.
|
||||||
|
|
||||||
### 6. dump_package
|
### 7. dump_package
|
||||||
|
|
||||||
Dump package assemblies into a folder. Extracts all assemblies from a NuGet package folder structure into a flat directory for easier analysis.
|
Dump package assemblies into a folder. Extracts all assemblies from a NuGet package folder structure into a flat directory for easier analysis.
|
||||||
|
|
||||||
@ -238,7 +281,7 @@ Dump package assemblies into a folder. Extracts all assemblies from a NuGet pack
|
|||||||
**Response:**
|
**Response:**
|
||||||
Returns a summary including output directory path and list of extracted assemblies.
|
Returns a summary including output directory path and list of extracted assemblies.
|
||||||
|
|
||||||
### 7. get_assembly_info
|
### 8. get_assembly_info
|
||||||
|
|
||||||
Gets metadata and version information about a .NET assembly. Uses ilspycmd to extract detailed assembly attributes.
|
Gets metadata and version information about a .NET assembly. Uses ilspycmd to extract detailed assembly attributes.
|
||||||
|
|
||||||
@ -273,7 +316,7 @@ Returns assembly metadata including:
|
|||||||
|
|
||||||
These tools use [dnfile](https://github.com/malwarefrank/dnfile) for direct PE/metadata parsing. They do **not require ilspycmd** to be installed.
|
These tools use [dnfile](https://github.com/malwarefrank/dnfile) for direct PE/metadata parsing. They do **not require ilspycmd** to be installed.
|
||||||
|
|
||||||
### 8. search_methods
|
### 9. search_methods
|
||||||
|
|
||||||
Search for methods in an assembly by name pattern. Uses direct metadata parsing of the MethodDef table.
|
Search for methods in an assembly by name pattern. Uses direct metadata parsing of the MethodDef table.
|
||||||
|
|
||||||
@ -310,7 +353,7 @@ Search for methods in an assembly by name pattern. Uses direct metadata parsing
|
|||||||
**Response:**
|
**Response:**
|
||||||
Returns matching methods grouped by declaring type, showing visibility modifiers (public, static, virtual, abstract).
|
Returns matching methods grouped by declaring type, showing visibility modifiers (public, static, virtual, abstract).
|
||||||
|
|
||||||
### 9. search_fields
|
### 10. search_fields
|
||||||
|
|
||||||
Search for fields and constants in an assembly. Uses direct metadata parsing of the Field table.
|
Search for fields and constants in an assembly. Uses direct metadata parsing of the Field table.
|
||||||
|
|
||||||
@ -344,7 +387,7 @@ Search for fields and constants in an assembly. Uses direct metadata parsing of
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 10. search_properties
|
### 11. search_properties
|
||||||
|
|
||||||
Search for properties in an assembly by name pattern. Uses direct metadata parsing of the Property table.
|
Search for properties in an assembly by name pattern. Uses direct metadata parsing of the Property table.
|
||||||
|
|
||||||
@ -364,7 +407,7 @@ Search for properties in an assembly by name pattern. Uses direct metadata parsi
|
|||||||
- Locate data model fields
|
- Locate data model fields
|
||||||
- Discover API response/request properties
|
- Discover API response/request properties
|
||||||
|
|
||||||
### 11. list_events
|
### 12. list_events
|
||||||
|
|
||||||
List all events defined in an assembly. Uses direct metadata parsing of the Event table.
|
List all events defined in an assembly. Uses direct metadata parsing of the Event table.
|
||||||
|
|
||||||
@ -381,7 +424,7 @@ List all events defined in an assembly. Uses direct metadata parsing of the Even
|
|||||||
- Discover observer patterns
|
- Discover observer patterns
|
||||||
- Analyze UI event handlers
|
- Analyze UI event handlers
|
||||||
|
|
||||||
### 12. list_resources
|
### 13. list_resources
|
||||||
|
|
||||||
List all embedded resources in an assembly. Uses direct metadata parsing of the ManifestResource table.
|
List all embedded resources in an assembly. Uses direct metadata parsing of the ManifestResource table.
|
||||||
|
|
||||||
@ -396,7 +439,7 @@ List all embedded resources in an assembly. Uses direct metadata parsing of the
|
|||||||
- Discover localization resources
|
- Discover localization resources
|
||||||
- Locate embedded assemblies
|
- Locate embedded assemblies
|
||||||
|
|
||||||
### 13. get_metadata_summary
|
### 14. get_metadata_summary
|
||||||
|
|
||||||
Get a comprehensive metadata summary with accurate statistics. Uses dnfile for direct metadata counts.
|
Get a comprehensive metadata summary with accurate statistics. Uses dnfile for direct metadata counts.
|
||||||
|
|
||||||
@ -427,7 +470,7 @@ Returns comprehensive assembly information including:
|
|||||||
|
|
||||||
These tools help manage ilspycmd installation and diagnose issues.
|
These tools help manage ilspycmd installation and diagnose issues.
|
||||||
|
|
||||||
### 14. check_ilspy_installation
|
### 15. check_ilspy_installation
|
||||||
|
|
||||||
Check if ilspycmd and dotnet CLI are installed and working. Use this to diagnose issues with decompilation tools.
|
Check if ilspycmd and dotnet CLI are installed and working. Use this to diagnose issues with decompilation tools.
|
||||||
|
|
||||||
@ -447,7 +490,7 @@ Returns installation status including:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### 15. install_ilspy
|
### 16. install_ilspy
|
||||||
|
|
||||||
Install or update ilspycmd, the ILSpy command-line decompiler. Automatically detects your platform and package manager to provide optimal installation instructions.
|
Install or update ilspycmd, the ILSpy command-line decompiler. Automatically detects your platform and package manager to provide optimal installation instructions.
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcilspy"
|
name = "mcilspy"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
description = "MCP Server for ILSpy .NET Decompiler"
|
description = "MCP Server for ILSpy .NET Decompiler"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
||||||
|
|||||||
@ -28,6 +28,11 @@ MAX_UNPARSED_LOG_LINES: int = 3
|
|||||||
# Preview length for unparsed line debug messages
|
# Preview length for unparsed line debug messages
|
||||||
UNPARSED_LINE_PREVIEW_LENGTH: int = 100
|
UNPARSED_LINE_PREVIEW_LENGTH: int = 100
|
||||||
|
|
||||||
|
# Maximum characters to return inline from decompile_assembly before
|
||||||
|
# truncating and saving the full output to a temp file.
|
||||||
|
# Set to 0 in a tool call to disable truncation entirely.
|
||||||
|
DEFAULT_MAX_OUTPUT_CHARS: int = 100_000
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Search Limits
|
# Search Limits
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|||||||
147
src/mcilspy/il_parser.py
Normal file
147
src/mcilspy/il_parser.py
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"""Method-level extraction from decompiled IL and C# output.
|
||||||
|
|
||||||
|
ilspycmd only supports type-level filtering (-t flag). When a type is
|
||||||
|
very large, clients need to extract individual methods from the full
|
||||||
|
type output. This module provides that post-processing step.
|
||||||
|
|
||||||
|
IL extraction relies on ECMA-335 standard delimiters:
|
||||||
|
- Start: `.method` directive
|
||||||
|
- End: `} // end of method TypeName::MethodName`
|
||||||
|
|
||||||
|
C# extraction uses signature matching with brace-depth counting.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def extract_il_method(il_source: str, method_name: str) -> list[str]:
|
||||||
|
"""Extract method(s) by name from IL output.
|
||||||
|
|
||||||
|
Splits on `.method` directives and matches against
|
||||||
|
``} // end of method ...::MethodName`` markers.
|
||||||
|
Returns list of matching method blocks (handles overloads).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
il_source: Full IL source code from ilspycmd
|
||||||
|
method_name: Method name to extract (matched against end-of-method comment)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of IL method blocks as strings. Empty list if no matches.
|
||||||
|
"""
|
||||||
|
if not il_source or not method_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
results: list[str] = []
|
||||||
|
lines = il_source.split("\n")
|
||||||
|
in_method = False
|
||||||
|
current_block: list[str] = []
|
||||||
|
brace_depth = 0
|
||||||
|
found_opening = False
|
||||||
|
|
||||||
|
# Pattern for end-of-method comment: } // end of method TypeName::MethodName
|
||||||
|
end_pattern = re.compile(
|
||||||
|
r"^\s*\}\s*//\s*end of method\s+\S+::" + re.escape(method_name) + r"\s*$"
|
||||||
|
)
|
||||||
|
# Pattern for .method directive start
|
||||||
|
method_start = re.compile(r"^\s*\.method\s+")
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
if not in_method:
|
||||||
|
if method_start.match(line):
|
||||||
|
in_method = True
|
||||||
|
current_block = [line]
|
||||||
|
brace_depth = line.count("{") - line.count("}")
|
||||||
|
found_opening = brace_depth > 0
|
||||||
|
else:
|
||||||
|
current_block.append(line)
|
||||||
|
brace_depth += line.count("{") - line.count("}")
|
||||||
|
|
||||||
|
if not found_opening and brace_depth > 0:
|
||||||
|
found_opening = True
|
||||||
|
|
||||||
|
if end_pattern.match(line):
|
||||||
|
results.append("\n".join(current_block))
|
||||||
|
in_method = False
|
||||||
|
current_block = []
|
||||||
|
brace_depth = 0
|
||||||
|
elif found_opening and brace_depth <= 0:
|
||||||
|
# Reached closing brace without matching our method name —
|
||||||
|
# this was a different method, discard it
|
||||||
|
in_method = False
|
||||||
|
current_block = []
|
||||||
|
brace_depth = 0
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def extract_csharp_method(cs_source: str, method_name: str) -> list[str]:
|
||||||
|
"""Extract method(s) by name from C# output.
|
||||||
|
|
||||||
|
Finds method signatures matching the name, then uses
|
||||||
|
brace-depth counting to find the closing brace.
|
||||||
|
Returns list of matching method blocks (handles overloads).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cs_source: Full C# source code from ilspycmd
|
||||||
|
method_name: Method name to extract
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of C# method blocks as strings. Empty list if no matches.
|
||||||
|
"""
|
||||||
|
if not cs_source or not method_name:
|
||||||
|
return []
|
||||||
|
|
||||||
|
results: list[str] = []
|
||||||
|
lines = cs_source.split("\n")
|
||||||
|
|
||||||
|
# Match method signatures — handles modifiers, return types, generics
|
||||||
|
# Examples:
|
||||||
|
# public void DoElaborate(...)
|
||||||
|
# private static async Task<int> DoElaborate<T>(...)
|
||||||
|
# internal override bool DoElaborate(
|
||||||
|
sig_pattern = re.compile(
|
||||||
|
r"^(\s*)" # leading whitespace (capture for indent level)
|
||||||
|
r"(?:(?:public|private|protected|internal|static|virtual|override|"
|
||||||
|
r"abstract|async|sealed|new|extern|unsafe|partial|readonly)\s+)*"
|
||||||
|
r".+\s+" # return type (greedy — handles generics with spaces like List<Dictionary<string, int>>)
|
||||||
|
+ re.escape(method_name)
|
||||||
|
+ r"\s*(?:<[^>]*>)?" # optional generic type parameters
|
||||||
|
r"\s*\(", # opening paren
|
||||||
|
)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
match = sig_pattern.match(lines[i])
|
||||||
|
if match:
|
||||||
|
# Found a method signature — collect until braces balance
|
||||||
|
block: list[str] = [lines[i]]
|
||||||
|
brace_depth = lines[i].count("{") - lines[i].count("}")
|
||||||
|
j = i + 1
|
||||||
|
|
||||||
|
# If the signature hasn't opened a brace yet, keep scanning
|
||||||
|
# for the opening brace (multi-line signatures)
|
||||||
|
found_opening = brace_depth > 0
|
||||||
|
|
||||||
|
while j < len(lines):
|
||||||
|
block.append(lines[j])
|
||||||
|
brace_depth += lines[j].count("{") - lines[j].count("}")
|
||||||
|
|
||||||
|
if not found_opening and brace_depth > 0:
|
||||||
|
found_opening = True
|
||||||
|
|
||||||
|
if found_opening and brace_depth <= 0:
|
||||||
|
# Balanced — we have the full method body
|
||||||
|
results.append("\n".join(block))
|
||||||
|
i = j + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
j += 1
|
||||||
|
else:
|
||||||
|
# Reached end of file without balancing — include what we have
|
||||||
|
if block:
|
||||||
|
results.append("\n".join(block))
|
||||||
|
i = j
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return results
|
||||||
@ -4,17 +4,18 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
|
import tempfile
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
|
||||||
from mcp.server.fastmcp import Context, FastMCP
|
from mcp.server.fastmcp import Context, FastMCP
|
||||||
|
|
||||||
from .constants import (
|
from .constants import (
|
||||||
ALL_ENTITY_TYPES,
|
ALL_ENTITY_TYPES,
|
||||||
|
DEFAULT_MAX_OUTPUT_CHARS,
|
||||||
DEFAULT_MAX_SEARCH_RESULTS,
|
DEFAULT_MAX_SEARCH_RESULTS,
|
||||||
MAX_ERROR_OUTPUT_CHARS,
|
MAX_ERROR_OUTPUT_CHARS,
|
||||||
MAX_LINE_LENGTH,
|
|
||||||
MAX_MATCHES_PER_TYPE,
|
|
||||||
)
|
)
|
||||||
|
from .il_parser import extract_csharp_method, extract_il_method
|
||||||
from .ilspy_wrapper import ILSpyWrapper
|
from .ilspy_wrapper import ILSpyWrapper
|
||||||
from .models import EntityType, LanguageVersion
|
from .models import EntityType, LanguageVersion
|
||||||
from .utils import find_ilspycmd_path
|
from .utils import find_ilspycmd_path
|
||||||
@ -584,6 +585,7 @@ async def decompile_assembly(
|
|||||||
nested_directories: bool = False,
|
nested_directories: bool = False,
|
||||||
generate_pdb: bool = False,
|
generate_pdb: bool = False,
|
||||||
use_pdb_variable_names: bool = False,
|
use_pdb_variable_names: bool = False,
|
||||||
|
max_output_chars: int = DEFAULT_MAX_OUTPUT_CHARS,
|
||||||
ctx: Context | None = None,
|
ctx: Context | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Decompile a .NET assembly to readable C# source code.
|
"""Decompile a .NET assembly to readable C# source code.
|
||||||
@ -597,6 +599,7 @@ async def decompile_assembly(
|
|||||||
|
|
||||||
WORKFLOW TIP: Start with `list_types` to discover available types, then use
|
WORKFLOW TIP: Start with `list_types` to discover available types, then use
|
||||||
this tool with `type_name` to decompile specific classes of interest.
|
this tool with `type_name` to decompile specific classes of interest.
|
||||||
|
If a type is very large, use `decompile_method` to extract individual methods.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
assembly_path: Full path to the .NET assembly file (.dll or .exe)
|
assembly_path: Full path to the .NET assembly file (.dll or .exe)
|
||||||
@ -611,6 +614,7 @@ async def decompile_assembly(
|
|||||||
nested_directories: Organize output files in namespace-based directory hierarchy
|
nested_directories: Organize output files in namespace-based directory hierarchy
|
||||||
generate_pdb: Generate a portable PDB file (requires output_dir). Enables debugging of decompiled code
|
generate_pdb: Generate a portable PDB file (requires output_dir). Enables debugging of decompiled code
|
||||||
use_pdb_variable_names: Use original variable names from existing PDB if available. Improves readability
|
use_pdb_variable_names: Use original variable names from existing PDB if available. Improves readability
|
||||||
|
max_output_chars: Maximum characters to return inline (default: 100000). Output exceeding this limit is saved to a temp file and a truncated preview is returned. Set to 0 to disable truncation.
|
||||||
"""
|
"""
|
||||||
# Validate assembly path before any processing
|
# Validate assembly path before any processing
|
||||||
try:
|
try:
|
||||||
@ -656,10 +660,57 @@ async def decompile_assembly(
|
|||||||
|
|
||||||
if response.success:
|
if response.success:
|
||||||
if response.source_code:
|
if response.source_code:
|
||||||
|
code_fence = "il" if (show_il_code or show_il_sequence_points) else "csharp"
|
||||||
|
source = response.source_code
|
||||||
|
source_len = len(source)
|
||||||
|
|
||||||
|
# Check if truncation is needed
|
||||||
|
if max_output_chars > 0 and source_len > max_output_chars:
|
||||||
|
# Save full output to a temp file so the client can access it
|
||||||
|
suffix = ".il" if code_fence == "il" else ".cs"
|
||||||
|
tmp = tempfile.NamedTemporaryFile( # noqa: SIM115
|
||||||
|
delete=False,
|
||||||
|
prefix="mcilspy_full_output_",
|
||||||
|
suffix=suffix,
|
||||||
|
mode="w",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
tmp.write(source)
|
||||||
|
tmp.close()
|
||||||
|
saved_path = tmp.name
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Output truncated: {source_len} chars -> {max_output_chars} chars. "
|
||||||
|
f"Full output saved to {saved_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build truncated response with recovery info
|
||||||
|
preview_chars = max_output_chars - 1000 # Reserve space for header
|
||||||
|
if preview_chars < 0:
|
||||||
|
preview_chars = max_output_chars
|
||||||
|
|
||||||
content = f"# Decompilation result: {response.assembly_name}"
|
content = f"# Decompilation result: {response.assembly_name}"
|
||||||
if response.type_name:
|
if response.type_name:
|
||||||
content += f" - {response.type_name}"
|
content += f" - {response.type_name}"
|
||||||
content += f"\n\n```csharp\n{response.source_code}\n```"
|
content += (
|
||||||
|
f"\n\n**Output truncated** ({source_len:,} chars exceeded "
|
||||||
|
f"{max_output_chars:,} char limit)\n\n"
|
||||||
|
f"Full output saved to: `{saved_path}`\n\n"
|
||||||
|
"**Recovery options:**\n"
|
||||||
|
"- Read the full file directly from the path above\n"
|
||||||
|
"- Use `decompile_method` to extract specific methods\n"
|
||||||
|
"- Call again with `max_output_chars=0` to disable truncation\n"
|
||||||
|
f"- Call again with a larger `max_output_chars` value\n\n"
|
||||||
|
f"**Preview** (first {preview_chars:,} chars):\n\n"
|
||||||
|
f"```{code_fence}\n{source[:preview_chars]}\n```"
|
||||||
|
)
|
||||||
|
return content
|
||||||
|
|
||||||
|
# Normal case: output fits within limit
|
||||||
|
content = f"# Decompilation result: {response.assembly_name}"
|
||||||
|
if response.type_name:
|
||||||
|
content += f" - {response.type_name}"
|
||||||
|
content += f"\n\n```{code_fence}\n{source}\n```"
|
||||||
return content
|
return content
|
||||||
else:
|
else:
|
||||||
return f"Decompilation successful! Files saved to: {response.output_path}"
|
return f"Decompilation successful! Files saved to: {response.output_path}"
|
||||||
@ -671,6 +722,129 @@ async def decompile_assembly(
|
|||||||
return _format_error(e)
|
return _format_error(e)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def decompile_method(
|
||||||
|
assembly_path: str,
|
||||||
|
type_name: str,
|
||||||
|
method_name: str,
|
||||||
|
show_il_code: bool = True,
|
||||||
|
language_version: str = "Latest",
|
||||||
|
use_pdb_variable_names: bool = False,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Decompile a specific method from a .NET type.
|
||||||
|
|
||||||
|
When a type is too large to return in full, use this tool to extract just
|
||||||
|
the method you need. It decompiles the containing type, then extracts the
|
||||||
|
named method from the output.
|
||||||
|
|
||||||
|
IL mode (default) is most reliable because IL output has standard ECMA-335
|
||||||
|
delimiters. C# mode uses signature matching with brace-depth counting.
|
||||||
|
|
||||||
|
If multiple overloads exist, all matching methods are returned.
|
||||||
|
|
||||||
|
WORKFLOW TIP: Use `search_methods` to find method names, then this tool
|
||||||
|
to get the full implementation. If decompile_assembly hit the output limit,
|
||||||
|
this tool extracts just what you need.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assembly_path: Full path to the .NET assembly file (.dll or .exe)
|
||||||
|
type_name: Fully qualified type name (e.g., "MyNamespace.MyClass"). Required to scope the decompilation
|
||||||
|
method_name: Name of the method to extract (e.g., "DoElaborate"). All overloads with this name are returned
|
||||||
|
show_il_code: Output IL bytecode instead of C# (default: True, more reliable for extraction)
|
||||||
|
language_version: C# version for output syntax. Options: CSharp1-CSharp12_0, Preview, Latest (default)
|
||||||
|
use_pdb_variable_names: Use original variable names from existing PDB if available
|
||||||
|
"""
|
||||||
|
# Validate assembly path
|
||||||
|
try:
|
||||||
|
validated_path = _validate_assembly_path(assembly_path)
|
||||||
|
except AssemblyPathError as e:
|
||||||
|
return _format_error(e, "path validation")
|
||||||
|
|
||||||
|
if not type_name or not type_name.strip():
|
||||||
|
return _format_error(ValueError("type_name is required"), "validation")
|
||||||
|
|
||||||
|
if not method_name or not method_name.strip():
|
||||||
|
return _format_error(ValueError("method_name is required"), "validation")
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(
|
||||||
|
f"Extracting method '{method_name}' from type '{type_name}' "
|
||||||
|
f"in: {validated_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapper = get_wrapper(ctx)
|
||||||
|
|
||||||
|
# Validate language version
|
||||||
|
try:
|
||||||
|
lang_ver = LanguageVersion(language_version)
|
||||||
|
except ValueError:
|
||||||
|
valid_versions = [v.value for v in LanguageVersion]
|
||||||
|
return (
|
||||||
|
f"Invalid language version: '{language_version}'\n\n"
|
||||||
|
f"Valid options: {', '.join(valid_versions)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
from .models import DecompileRequest
|
||||||
|
|
||||||
|
request = DecompileRequest(
|
||||||
|
assembly_path=validated_path,
|
||||||
|
type_name=type_name.strip(),
|
||||||
|
language_version=lang_ver,
|
||||||
|
show_il_code=show_il_code,
|
||||||
|
use_pdb_variable_names=use_pdb_variable_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await wrapper.decompile(request)
|
||||||
|
|
||||||
|
if not response.success:
|
||||||
|
return f"Decompilation failed: {response.error_message}"
|
||||||
|
|
||||||
|
if not response.source_code:
|
||||||
|
return f"No source code returned for type '{type_name}'"
|
||||||
|
|
||||||
|
# Extract the method from the full type output
|
||||||
|
clean_method_name = method_name.strip()
|
||||||
|
if show_il_code:
|
||||||
|
methods = extract_il_method(response.source_code, clean_method_name)
|
||||||
|
code_fence = "il"
|
||||||
|
else:
|
||||||
|
methods = extract_csharp_method(response.source_code, clean_method_name)
|
||||||
|
code_fence = "csharp"
|
||||||
|
|
||||||
|
if not methods:
|
||||||
|
return (
|
||||||
|
f"Method '{clean_method_name}' not found in type '{type_name}'.\n\n"
|
||||||
|
"Possible causes:\n"
|
||||||
|
"- Method name is misspelled (matching is exact)\n"
|
||||||
|
"- Method is inherited from a base type (try decompiling the base type)\n"
|
||||||
|
"- Method is a compiler-generated accessor (try the property name)\n\n"
|
||||||
|
"**TIP**: Use `search_methods` with `type_filter` to find available methods."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Format output
|
||||||
|
overload_note = ""
|
||||||
|
if len(methods) > 1:
|
||||||
|
overload_note = f" ({len(methods)} overloads)"
|
||||||
|
|
||||||
|
content = (
|
||||||
|
f"# Method: {clean_method_name}{overload_note}\n"
|
||||||
|
f"**Type**: `{type_name}` | **Assembly**: `{response.assembly_name}`\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
for i, method_block in enumerate(methods):
|
||||||
|
if len(methods) > 1:
|
||||||
|
content += f"### Overload {i + 1}\n\n"
|
||||||
|
content += f"```{code_fence}\n{method_block}\n```\n\n"
|
||||||
|
|
||||||
|
return content.rstrip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Method extraction error: {e}")
|
||||||
|
return _format_error(e)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def list_types(
|
async def list_types(
|
||||||
assembly_path: str,
|
assembly_path: str,
|
||||||
|
|||||||
231
tests/test_il_parser.py
Normal file
231
tests/test_il_parser.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
"""Tests for IL and C# method extraction from decompiled output."""
|
||||||
|
|
||||||
|
from mcilspy.il_parser import extract_csharp_method, extract_il_method
|
||||||
|
|
||||||
|
# ── IL extraction fixtures ────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_IL = """\
|
||||||
|
.class public auto ansi beforefieldinit MyNamespace.MyClass
|
||||||
|
extends [mscorlib]System.Object
|
||||||
|
{
|
||||||
|
.method public hidebysig
|
||||||
|
instance void DoElaborate (
|
||||||
|
int32 x
|
||||||
|
) cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: nop
|
||||||
|
IL_0001: ldarg.1
|
||||||
|
IL_0002: call instance void MyNamespace.MyClass::Helper(int32)
|
||||||
|
IL_0007: nop
|
||||||
|
IL_0008: ret
|
||||||
|
} // end of method MyClass::DoElaborate
|
||||||
|
|
||||||
|
.method public hidebysig
|
||||||
|
instance void DoElaborate (
|
||||||
|
int32 x,
|
||||||
|
string y
|
||||||
|
) cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: nop
|
||||||
|
IL_0001: ret
|
||||||
|
} // end of method MyClass::DoElaborate
|
||||||
|
|
||||||
|
.method private hidebysig
|
||||||
|
instance void Helper (
|
||||||
|
int32 val
|
||||||
|
) cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: nop
|
||||||
|
IL_0001: ret
|
||||||
|
} // end of method MyClass::Helper
|
||||||
|
|
||||||
|
} // end of class MyNamespace.MyClass
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
SAMPLE_CSHARP = """\
|
||||||
|
namespace MyNamespace
|
||||||
|
{
|
||||||
|
public class MyClass
|
||||||
|
{
|
||||||
|
public void DoElaborate(int x)
|
||||||
|
{
|
||||||
|
Helper(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DoElaborate(int x, string y)
|
||||||
|
{
|
||||||
|
Console.WriteLine(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Helper(int val)
|
||||||
|
{
|
||||||
|
// internal helper
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<bool> ProcessAsync(
|
||||||
|
string input,
|
||||||
|
CancellationToken ct)
|
||||||
|
{
|
||||||
|
await Task.Delay(100, ct);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ── IL extraction tests ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractILMethod:
|
||||||
|
"""Tests for extract_il_method()."""
|
||||||
|
|
||||||
|
def test_extracts_single_method(self):
|
||||||
|
"""Should extract the Helper method."""
|
||||||
|
results = extract_il_method(SAMPLE_IL, "Helper")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert ".method private hidebysig" in results[0]
|
||||||
|
assert "end of method MyClass::Helper" in results[0]
|
||||||
|
|
||||||
|
def test_extracts_overloaded_methods(self):
|
||||||
|
"""Should extract both DoElaborate overloads."""
|
||||||
|
results = extract_il_method(SAMPLE_IL, "DoElaborate")
|
||||||
|
assert len(results) == 2
|
||||||
|
# First overload has one param
|
||||||
|
assert "int32 x" in results[0]
|
||||||
|
# Second overload has two params
|
||||||
|
assert "string y" in results[1]
|
||||||
|
|
||||||
|
def test_returns_empty_for_nonexistent_method(self):
|
||||||
|
"""Should return empty list when method doesn't exist."""
|
||||||
|
results = extract_il_method(SAMPLE_IL, "NonExistent")
|
||||||
|
assert results == []
|
||||||
|
|
||||||
|
def test_returns_empty_for_empty_input(self):
|
||||||
|
"""Should handle empty inputs gracefully."""
|
||||||
|
assert extract_il_method("", "Foo") == []
|
||||||
|
assert extract_il_method(SAMPLE_IL, "") == []
|
||||||
|
assert extract_il_method("", "") == []
|
||||||
|
|
||||||
|
def test_method_name_is_exact_match(self):
|
||||||
|
"""Should not match partial method names."""
|
||||||
|
results = extract_il_method(SAMPLE_IL, "Do")
|
||||||
|
assert results == []
|
||||||
|
|
||||||
|
def test_method_name_is_exact_match_helper(self):
|
||||||
|
"""Should not match 'Help' when looking for 'Helper'."""
|
||||||
|
results = extract_il_method(SAMPLE_IL, "Help")
|
||||||
|
assert results == []
|
||||||
|
|
||||||
|
def test_preserves_method_body(self):
|
||||||
|
"""Extracted method should include the full body."""
|
||||||
|
results = extract_il_method(SAMPLE_IL, "Helper")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert ".maxstack 8" in results[0]
|
||||||
|
assert "IL_0000: nop" in results[0]
|
||||||
|
assert "IL_0001: ret" in results[0]
|
||||||
|
|
||||||
|
|
||||||
|
# ── C# extraction tests ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractCSharpMethod:
|
||||||
|
"""Tests for extract_csharp_method()."""
|
||||||
|
|
||||||
|
def test_extracts_single_method(self):
|
||||||
|
"""Should extract the Helper method."""
|
||||||
|
results = extract_csharp_method(SAMPLE_CSHARP, "Helper")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert "private void Helper" in results[0]
|
||||||
|
assert "// internal helper" in results[0]
|
||||||
|
|
||||||
|
def test_extracts_overloaded_methods(self):
|
||||||
|
"""Should extract both DoElaborate overloads."""
|
||||||
|
results = extract_csharp_method(SAMPLE_CSHARP, "DoElaborate")
|
||||||
|
assert len(results) == 2
|
||||||
|
|
||||||
|
def test_returns_empty_for_nonexistent_method(self):
|
||||||
|
"""Should return empty list when method doesn't exist."""
|
||||||
|
results = extract_csharp_method(SAMPLE_CSHARP, "NonExistent")
|
||||||
|
assert results == []
|
||||||
|
|
||||||
|
def test_returns_empty_for_empty_input(self):
|
||||||
|
"""Should handle empty inputs gracefully."""
|
||||||
|
assert extract_csharp_method("", "Foo") == []
|
||||||
|
assert extract_csharp_method(SAMPLE_CSHARP, "") == []
|
||||||
|
|
||||||
|
def test_extracts_multiline_signature(self):
|
||||||
|
"""Should handle method signatures that span multiple lines."""
|
||||||
|
results = extract_csharp_method(SAMPLE_CSHARP, "ProcessAsync")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert "async Task<bool>" in results[0]
|
||||||
|
assert "CancellationToken ct" in results[0]
|
||||||
|
assert "return true;" in results[0]
|
||||||
|
|
||||||
|
def test_preserves_method_body(self):
|
||||||
|
"""Extracted method should include the full body."""
|
||||||
|
results = extract_csharp_method(SAMPLE_CSHARP, "Helper")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert "// internal helper" in results[0]
|
||||||
|
|
||||||
|
def test_method_name_is_exact_match(self):
|
||||||
|
"""Should not match partial method names via regex anchoring."""
|
||||||
|
# "Do" should not match "DoElaborate" because the pattern requires
|
||||||
|
# the name followed by optional generics and opening paren
|
||||||
|
results = extract_csharp_method(SAMPLE_CSHARP, "Do")
|
||||||
|
assert results == []
|
||||||
|
|
||||||
|
|
||||||
|
# ── Edge case tests ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestEdgeCases:
|
||||||
|
"""Tests for edge cases in both extractors."""
|
||||||
|
|
||||||
|
def test_il_method_with_special_chars_in_name(self):
|
||||||
|
"""Methods with dots or special chars in type name should work."""
|
||||||
|
il = """\
|
||||||
|
.method public hidebysig
|
||||||
|
instance void Process () cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: ret
|
||||||
|
} // end of method Outer+Nested::Process
|
||||||
|
"""
|
||||||
|
results = extract_il_method(il, "Process")
|
||||||
|
assert len(results) == 1
|
||||||
|
|
||||||
|
def test_csharp_method_with_generic_return(self):
|
||||||
|
"""Should handle generic return types."""
|
||||||
|
cs = """\
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
public List<Dictionary<string, int>> Transform(IEnumerable<string> input)
|
||||||
|
{
|
||||||
|
return new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
results = extract_csharp_method(cs, "Transform")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert "return new();" in results[0]
|
||||||
|
|
||||||
|
def test_csharp_method_expression_body(self):
|
||||||
|
"""Expression-bodied methods should be extracted (they use => not {})."""
|
||||||
|
cs = """\
|
||||||
|
public class Foo
|
||||||
|
{
|
||||||
|
public int GetValue()
|
||||||
|
{
|
||||||
|
return 42;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
results = extract_csharp_method(cs, "GetValue")
|
||||||
|
assert len(results) == 1
|
||||||
|
assert "return 42;" in results[0]
|
||||||
@ -4,6 +4,7 @@ These tests exercise the @mcp.tool() decorated functions in server.py.
|
|||||||
We mock the ILSpyWrapper to test the tool logic independently of ilspycmd.
|
We mock the ILSpyWrapper to test the tool logic independently of ilspycmd.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -595,3 +596,286 @@ class TestHelperFunctions:
|
|||||||
result = server._detect_platform()
|
result = server._detect_platform()
|
||||||
|
|
||||||
assert result["system"] == "windows"
|
assert result["system"] == "windows"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("bypass_path_validation")
|
||||||
|
class TestDecompileOutputTruncation:
|
||||||
|
"""Tests for decompile_assembly output truncation guard."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_small_output_not_truncated(self):
|
||||||
|
"""Output within the limit should be returned as-is."""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
source_code="public class Small { }",
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_assembly("/path/to/test.dll")
|
||||||
|
|
||||||
|
assert "Output truncated" not in result
|
||||||
|
assert "public class Small { }" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_large_output_truncated(self):
|
||||||
|
"""Output exceeding the limit should be truncated with file path."""
|
||||||
|
large_code = "x" * 200_000 # 200K chars, exceeds 100K default
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="LargeAssembly",
|
||||||
|
source_code=large_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_assembly("/path/to/test.dll")
|
||||||
|
|
||||||
|
assert "Output truncated" in result
|
||||||
|
assert "200,000 chars" in result
|
||||||
|
assert "mcilspy_full_output_" in result
|
||||||
|
assert "decompile_method" in result
|
||||||
|
assert "max_output_chars=0" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_truncation_saves_full_file(self):
|
||||||
|
"""The full output should be saved to a readable temp file."""
|
||||||
|
large_code = "public class Big { " + ("int field; " * 20_000) + "}"
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="BigAssembly",
|
||||||
|
source_code=large_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_assembly("/path/to/test.dll")
|
||||||
|
|
||||||
|
# Extract file path from result
|
||||||
|
assert "mcilspy_full_output_" in result
|
||||||
|
# Find the path in the result
|
||||||
|
for line in result.split("\n"):
|
||||||
|
if "mcilspy_full_output_" in line:
|
||||||
|
# Extract path from markdown backticks
|
||||||
|
path = line.split("`")[1] if "`" in line else ""
|
||||||
|
if path and os.path.exists(path):
|
||||||
|
with open(path) as f:
|
||||||
|
saved = f.read()
|
||||||
|
assert saved == large_code
|
||||||
|
os.unlink(path) # cleanup
|
||||||
|
break
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_truncation_disabled_with_zero(self):
|
||||||
|
"""Setting max_output_chars=0 should disable truncation."""
|
||||||
|
large_code = "x" * 200_000
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="LargeAssembly",
|
||||||
|
source_code=large_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_assembly(
|
||||||
|
"/path/to/test.dll", max_output_chars=0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Output truncated" not in result
|
||||||
|
assert large_code in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_il_output_uses_il_fence(self):
|
||||||
|
"""IL output should use ```il code fences."""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
source_code=".method public void Main() {}",
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_assembly(
|
||||||
|
"/path/to/test.dll", show_il_code=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "```il" in result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("bypass_path_validation")
|
||||||
|
class TestDecompileMethod:
|
||||||
|
"""Tests for decompile_method tool."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_extracts_il_method(self):
|
||||||
|
"""Should extract a method from IL output."""
|
||||||
|
il_source = """\
|
||||||
|
.method public hidebysig instance void DoWork () cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: ret
|
||||||
|
} // end of method MyClass::DoWork
|
||||||
|
"""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
source_code=il_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="NS.MyClass",
|
||||||
|
method_name="DoWork",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Method: DoWork" in result
|
||||||
|
assert "IL_0000: ret" in result
|
||||||
|
assert "```il" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_extracts_csharp_method(self):
|
||||||
|
"""Should extract a method from C# output."""
|
||||||
|
cs_source = """\
|
||||||
|
public class MyClass
|
||||||
|
{
|
||||||
|
public void DoWork()
|
||||||
|
{
|
||||||
|
Console.WriteLine("hello");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
source_code=cs_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="NS.MyClass",
|
||||||
|
method_name="DoWork",
|
||||||
|
show_il_code=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "Method: DoWork" in result
|
||||||
|
assert "Console.WriteLine" in result
|
||||||
|
assert "```csharp" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_method_not_found(self):
|
||||||
|
"""Should return helpful message when method not found."""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
source_code=".method void Other() {} // end of method X::Other",
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="NS.MyClass",
|
||||||
|
method_name="NonExistent",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "not found" in result
|
||||||
|
assert "search_methods" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_decompile_failure_propagated(self):
|
||||||
|
"""Should propagate decompilation failures."""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=False,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
error_message="Type not found",
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="NS.Missing",
|
||||||
|
method_name="Foo",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "failed" in result.lower()
|
||||||
|
assert "Type not found" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_empty_type_name_rejected(self):
|
||||||
|
"""Should reject empty type_name."""
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="",
|
||||||
|
method_name="Foo",
|
||||||
|
)
|
||||||
|
assert "Error" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_empty_method_name_rejected(self):
|
||||||
|
"""Should reject empty method_name."""
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="NS.MyClass",
|
||||||
|
method_name="",
|
||||||
|
)
|
||||||
|
assert "Error" in result
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_multiple_overloads_labeled(self):
|
||||||
|
"""Multiple overloads should be labeled with overload numbers."""
|
||||||
|
il_source = """\
|
||||||
|
.method public hidebysig instance void Work (int32 x) cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: ret
|
||||||
|
} // end of method MyClass::Work
|
||||||
|
|
||||||
|
.method public hidebysig instance void Work (string s) cil managed
|
||||||
|
{
|
||||||
|
.maxstack 8
|
||||||
|
IL_0000: ret
|
||||||
|
} // end of method MyClass::Work
|
||||||
|
"""
|
||||||
|
mock_response = DecompileResponse(
|
||||||
|
success=True,
|
||||||
|
assembly_name="TestAssembly",
|
||||||
|
source_code=il_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_wrapper = MagicMock()
|
||||||
|
mock_wrapper.decompile = AsyncMock(return_value=mock_response)
|
||||||
|
|
||||||
|
with patch.object(server, "get_wrapper", return_value=mock_wrapper):
|
||||||
|
result = await server.decompile_method(
|
||||||
|
"/path/to/test.dll",
|
||||||
|
type_name="NS.MyClass",
|
||||||
|
method_name="Work",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert "2 overloads" in result
|
||||||
|
assert "Overload 1" in result
|
||||||
|
assert "Overload 2" in result
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user