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? |
|
||||
|------|--------------|:------------------:|
|
||||
| `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 |
|
||||
| `search_types` | Find types by name pattern | Yes |
|
||||
| `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 |
|
||||
|
||||
**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)
|
||||
- [Installation Options](#installation-options)
|
||||
- [Tool Reference](#tool-reference)
|
||||
- [Decompilation Tools](#decompilation-tools-requires-ilspycmd)
|
||||
- [Decompilation & Extraction Tools](#decompilation-tools-requires-ilspycmd)
|
||||
- [Metadata Tools](#metadata-tools-no-ilspycmd-required)
|
||||
- [Installation Tools](#installation--diagnostics)
|
||||
- [Configuration](#configuration)
|
||||
@ -177,6 +178,30 @@ The primary reverse-engineering tool. Decompiles .NET assemblies to readable C#.
|
||||
```
|
||||
</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>
|
||||
<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
|
||||
|
||||
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
|
||||
|
||||
@ -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 |
|
||||
| `generate_pdb` | boolean | ✗ | false | Generate portable PDB file (requires `output_dir`) |
|
||||
| `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:**
|
||||
- `CSharp1` through `CSharp12_0`
|
||||
@ -68,10 +69,52 @@ Decompiles a .NET assembly to C# source code. This is the primary tool for rever
|
||||
**Response:**
|
||||
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.
|
||||
|
||||
> **Note:** Tool numbering in this document reflects the addition of `decompile_method` at position 2.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
@ -104,7 +147,7 @@ Returns a formatted list of types organized by namespace, including:
|
||||
- Type kind (Class, Interface, etc.)
|
||||
- Namespace
|
||||
|
||||
### 3. search_types
|
||||
### 4. search_types
|
||||
|
||||
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:**
|
||||
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.
|
||||
|
||||
@ -179,7 +222,7 @@ Search for string literals in assembly code. Crucial for reverse engineering - f
|
||||
**Response:**
|
||||
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.
|
||||
|
||||
@ -208,7 +251,7 @@ Generates an interactive HTML diagram showing assembly type relationships.
|
||||
**Response:**
|
||||
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.
|
||||
|
||||
@ -238,7 +281,7 @@ Dump package assemblies into a folder. Extracts all assemblies from a NuGet pack
|
||||
**Response:**
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
|
||||
### 8. search_methods
|
||||
### 9. search_methods
|
||||
|
||||
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:**
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -364,7 +407,7 @@ Search for properties in an assembly by name pattern. Uses direct metadata parsi
|
||||
- Locate data model fields
|
||||
- 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.
|
||||
|
||||
@ -381,7 +424,7 @@ List all events defined in an assembly. Uses direct metadata parsing of the Even
|
||||
- Discover observer patterns
|
||||
- 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.
|
||||
|
||||
@ -396,7 +439,7 @@ List all embedded resources in an assembly. Uses direct metadata parsing of the
|
||||
- Discover localization resources
|
||||
- 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.
|
||||
|
||||
@ -427,7 +470,7 @@ Returns comprehensive assembly information including:
|
||||
|
||||
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.
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "mcilspy"
|
||||
version = "0.4.0"
|
||||
version = "0.5.0"
|
||||
description = "MCP Server for ILSpy .NET Decompiler"
|
||||
authors = [
|
||||
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
||||
|
||||
@ -28,6 +28,11 @@ MAX_UNPARSED_LOG_LINES: int = 3
|
||||
# Preview length for unparsed line debug messages
|
||||
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
|
||||
# =============================================================================
|
||||
|
||||
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 re
|
||||
import shutil
|
||||
import tempfile
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from mcp.server.fastmcp import Context, FastMCP
|
||||
|
||||
from .constants import (
|
||||
ALL_ENTITY_TYPES,
|
||||
DEFAULT_MAX_OUTPUT_CHARS,
|
||||
DEFAULT_MAX_SEARCH_RESULTS,
|
||||
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 .models import EntityType, LanguageVersion
|
||||
from .utils import find_ilspycmd_path
|
||||
@ -584,6 +585,7 @@ async def decompile_assembly(
|
||||
nested_directories: bool = False,
|
||||
generate_pdb: bool = False,
|
||||
use_pdb_variable_names: bool = False,
|
||||
max_output_chars: int = DEFAULT_MAX_OUTPUT_CHARS,
|
||||
ctx: Context | None = None,
|
||||
) -> str:
|
||||
"""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
|
||||
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:
|
||||
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
|
||||
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
|
||||
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
|
||||
try:
|
||||
@ -656,10 +660,57 @@ async def decompile_assembly(
|
||||
|
||||
if response.success:
|
||||
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}"
|
||||
if response.type_name:
|
||||
content += f" - {response.type_name}"
|
||||
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```csharp\n{response.source_code}\n```"
|
||||
content += f"\n\n```{code_fence}\n{source}\n```"
|
||||
return content
|
||||
else:
|
||||
return f"Decompilation successful! Files saved to: {response.output_path}"
|
||||
@ -671,6 +722,129 @@ async def decompile_assembly(
|
||||
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()
|
||||
async def list_types(
|
||||
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.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
@ -595,3 +596,286 @@ class TestHelperFunctions:
|
||||
result = server._detect_platform()
|
||||
|
||||
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