feat: add PDB generation, PDB variable names, and dump_package tools
New features exposing remaining ilspycmd capabilities: 1. PDB Generation (generate_pdb flag in decompile_assembly) - Generates portable PDB files for debugging decompiled code - Requires output_dir to be specified 2. Use PDB Variable Names (use_pdb_variable_names flag) - Reads original variable names from existing PDB files - Greatly improves readability of decompiled output 3. New dump_package tool - Extracts assemblies from NuGet package structures - Useful for bulk analysis of package dependencies Version bump: 0.3.0 -> 0.4.0
This commit is contained in:
parent
32df2b0d24
commit
8776c9cf74
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "mcilspy"
|
name = "mcilspy"
|
||||||
version = "0.3.0"
|
version = "0.4.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"}
|
||||||
|
|||||||
@ -19,6 +19,8 @@ from .models import (
|
|||||||
AssemblyInfoRequest,
|
AssemblyInfoRequest,
|
||||||
DecompileRequest,
|
DecompileRequest,
|
||||||
DecompileResponse,
|
DecompileResponse,
|
||||||
|
DumpPackageRequest,
|
||||||
|
DumpPackageResponse,
|
||||||
GenerateDiagrammerRequest,
|
GenerateDiagrammerRequest,
|
||||||
ListTypesRequest,
|
ListTypesRequest,
|
||||||
ListTypesResponse,
|
ListTypesResponse,
|
||||||
@ -257,6 +259,13 @@ class ILSpyWrapper:
|
|||||||
if request.nested_directories:
|
if request.nested_directories:
|
||||||
args.append("--nested-directories")
|
args.append("--nested-directories")
|
||||||
|
|
||||||
|
# PDB generation options
|
||||||
|
if request.generate_pdb:
|
||||||
|
args.append("-genpdb")
|
||||||
|
|
||||||
|
if request.use_pdb_variable_names:
|
||||||
|
args.append("-usepdb")
|
||||||
|
|
||||||
# Disable update check for automation
|
# Disable update check for automation
|
||||||
args.append("--disable-updatecheck")
|
args.append("--disable-updatecheck")
|
||||||
|
|
||||||
@ -354,6 +363,12 @@ class ILSpyWrapper:
|
|||||||
if request.show_il_sequence_points:
|
if request.show_il_sequence_points:
|
||||||
args.append("--il-sequence-points")
|
args.append("--il-sequence-points")
|
||||||
|
|
||||||
|
# PDB options (use_pdb_variable_names works with stdout mode)
|
||||||
|
if request.use_pdb_variable_names:
|
||||||
|
args.append("-usepdb")
|
||||||
|
|
||||||
|
# Note: generate_pdb requires output directory, so it's not used in stdout mode
|
||||||
|
|
||||||
# Disable update check for automation
|
# Disable update check for automation
|
||||||
args.append("--disable-updatecheck")
|
args.append("--disable-updatecheck")
|
||||||
|
|
||||||
@ -595,6 +610,59 @@ class ILSpyWrapper:
|
|||||||
logger.exception(f"Error generating diagrammer: {e}")
|
logger.exception(f"Error generating diagrammer: {e}")
|
||||||
return {"success": False, "error_message": str(e)}
|
return {"success": False, "error_message": str(e)}
|
||||||
|
|
||||||
|
async def dump_package(self, request: DumpPackageRequest) -> DumpPackageResponse:
|
||||||
|
"""Dump package assemblies into a folder.
|
||||||
|
|
||||||
|
Extracts all assemblies from a NuGet package structure into a flat
|
||||||
|
directory for easier analysis.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request: Dump package request with assembly path and output directory
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DumpPackageResponse with success status and list of dumped assemblies
|
||||||
|
"""
|
||||||
|
if not os.path.exists(request.assembly_path):
|
||||||
|
return DumpPackageResponse(
|
||||||
|
success=False,
|
||||||
|
error_message=f"Path not found: {request.assembly_path}",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validate PE signature if it's a single assembly file
|
||||||
|
if os.path.isfile(request.assembly_path):
|
||||||
|
is_valid, pe_error = _validate_pe_signature(request.assembly_path)
|
||||||
|
if not is_valid:
|
||||||
|
return DumpPackageResponse(success=False, error_message=pe_error)
|
||||||
|
|
||||||
|
# Create output directory if it doesn't exist
|
||||||
|
os.makedirs(request.output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
args = [request.assembly_path, "-d", "-o", request.output_dir, "--disable-updatecheck"]
|
||||||
|
|
||||||
|
try:
|
||||||
|
return_code, stdout, stderr = await self._run_command(args)
|
||||||
|
|
||||||
|
if return_code == 0:
|
||||||
|
# List the dumped assemblies
|
||||||
|
dumped_files = []
|
||||||
|
if os.path.exists(request.output_dir):
|
||||||
|
for f in os.listdir(request.output_dir):
|
||||||
|
if f.endswith((".dll", ".exe")):
|
||||||
|
dumped_files.append(f)
|
||||||
|
|
||||||
|
return DumpPackageResponse(
|
||||||
|
success=True,
|
||||||
|
output_path=request.output_dir,
|
||||||
|
assemblies_dumped=sorted(dumped_files),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
error_msg = stderr or stdout or "Unknown error occurred"
|
||||||
|
return DumpPackageResponse(success=False, error_message=error_msg)
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
logger.exception(f"Error dumping package: {e}")
|
||||||
|
return DumpPackageResponse(success=False, error_message=str(e))
|
||||||
|
|
||||||
async def get_assembly_info(self, request: AssemblyInfoRequest) -> AssemblyInfo:
|
async def get_assembly_info(self, request: AssemblyInfoRequest) -> AssemblyInfo:
|
||||||
"""Get detailed information about an assembly by decompiling assembly attributes.
|
"""Get detailed information about an assembly by decompiling assembly attributes.
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,9 @@ class DecompileRequest(BaseModel):
|
|||||||
remove_dead_stores: bool = False
|
remove_dead_stores: bool = False
|
||||||
show_il_sequence_points: bool = False
|
show_il_sequence_points: bool = False
|
||||||
nested_directories: bool = False
|
nested_directories: bool = False
|
||||||
|
# PDB-related options
|
||||||
|
generate_pdb: bool = False # Generate portable PDB file
|
||||||
|
use_pdb_variable_names: bool = False # Use variable names from existing PDB
|
||||||
|
|
||||||
|
|
||||||
class ListTypesRequest(BaseModel):
|
class ListTypesRequest(BaseModel):
|
||||||
@ -144,6 +147,22 @@ class GenerateDiagrammerRequest(BaseModel):
|
|||||||
report_excluded: bool = False
|
report_excluded: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class DumpPackageRequest(BaseModel):
|
||||||
|
"""Request to dump package assemblies into a folder."""
|
||||||
|
|
||||||
|
assembly_path: str # Path to the assembly or NuGet package folder
|
||||||
|
output_dir: str # Required: directory to dump assemblies into
|
||||||
|
|
||||||
|
|
||||||
|
class DumpPackageResponse(BaseModel):
|
||||||
|
"""Response from dump package operation."""
|
||||||
|
|
||||||
|
success: bool
|
||||||
|
output_path: str | None = None
|
||||||
|
assemblies_dumped: list[str] = Field(default_factory=list)
|
||||||
|
error_message: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class AssemblyInfoRequest(BaseModel):
|
class AssemblyInfoRequest(BaseModel):
|
||||||
"""Request to get assembly information."""
|
"""Request to get assembly information."""
|
||||||
|
|
||||||
|
|||||||
@ -537,11 +537,12 @@ async def install_ilspy(
|
|||||||
f"✅ **Success!** ilspycmd{version_info} has been {'updated' if update else 'installed'}.\n\n"
|
f"✅ **Success!** ilspycmd{version_info} has been {'updated' if update else 'installed'}.\n\n"
|
||||||
f"Path: `{new_status['ilspycmd_path']}`\n\n"
|
f"Path: `{new_status['ilspycmd_path']}`\n\n"
|
||||||
"All ILSpy-based decompilation tools are now available:\n"
|
"All ILSpy-based decompilation tools are now available:\n"
|
||||||
"- `decompile_assembly`\n"
|
"- `decompile_assembly` (with PDB generation support)\n"
|
||||||
"- `list_types`\n"
|
"- `list_types`\n"
|
||||||
"- `search_types`\n"
|
"- `search_types`\n"
|
||||||
"- `search_strings`\n"
|
"- `search_strings`\n"
|
||||||
"- `generate_diagrammer`\n"
|
"- `generate_diagrammer`\n"
|
||||||
|
"- `dump_package`\n"
|
||||||
"- `get_assembly_info`"
|
"- `get_assembly_info`"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
@ -581,6 +582,8 @@ async def decompile_assembly(
|
|||||||
remove_dead_stores: bool = False,
|
remove_dead_stores: bool = False,
|
||||||
show_il_sequence_points: bool = False,
|
show_il_sequence_points: bool = False,
|
||||||
nested_directories: bool = False,
|
nested_directories: bool = False,
|
||||||
|
generate_pdb: bool = False,
|
||||||
|
use_pdb_variable_names: bool = False,
|
||||||
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.
|
||||||
@ -590,6 +593,7 @@ async def decompile_assembly(
|
|||||||
- Extract specific types by fully qualified name
|
- Extract specific types by fully qualified name
|
||||||
- Generate compilable project structures for analysis
|
- Generate compilable project structures for analysis
|
||||||
- View IL (Intermediate Language) code for low-level analysis
|
- View IL (Intermediate Language) code for low-level analysis
|
||||||
|
- Generate PDB files for debugging decompiled code
|
||||||
|
|
||||||
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.
|
||||||
@ -605,6 +609,8 @@ async def decompile_assembly(
|
|||||||
remove_dead_stores: Strip unused variable assignments from output
|
remove_dead_stores: Strip unused variable assignments from output
|
||||||
show_il_sequence_points: Include debugging sequence points in IL output (implies show_il_code)
|
show_il_sequence_points: Include debugging sequence points in IL output (implies show_il_code)
|
||||||
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
|
||||||
|
use_pdb_variable_names: Use original variable names from existing PDB if available. Improves readability
|
||||||
"""
|
"""
|
||||||
# Validate assembly path before any processing
|
# Validate assembly path before any processing
|
||||||
try:
|
try:
|
||||||
@ -642,6 +648,8 @@ async def decompile_assembly(
|
|||||||
remove_dead_stores=remove_dead_stores,
|
remove_dead_stores=remove_dead_stores,
|
||||||
show_il_sequence_points=show_il_sequence_points,
|
show_il_sequence_points=show_il_sequence_points,
|
||||||
nested_directories=nested_directories,
|
nested_directories=nested_directories,
|
||||||
|
generate_pdb=generate_pdb,
|
||||||
|
use_pdb_variable_names=use_pdb_variable_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = await wrapper.decompile(request)
|
response = await wrapper.decompile(request)
|
||||||
@ -832,6 +840,74 @@ async def generate_diagrammer(
|
|||||||
return _format_error(e)
|
return _format_error(e)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def dump_package(
|
||||||
|
assembly_path: str,
|
||||||
|
output_dir: str,
|
||||||
|
ctx: Context | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""Dump package assemblies into a folder.
|
||||||
|
|
||||||
|
Extracts all assemblies from a NuGet package folder structure into a flat
|
||||||
|
directory for easier analysis. This is useful for:
|
||||||
|
- Bulk decompilation of NuGet packages
|
||||||
|
- Analyzing package dependencies
|
||||||
|
- Extracting DLLs from complex package structures
|
||||||
|
|
||||||
|
The output directory will contain all .dll and .exe files found in the
|
||||||
|
package structure, making them easier to analyze with other tools.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assembly_path: Path to the assembly or NuGet package folder to extract from
|
||||||
|
output_dir: Directory to dump assemblies into (will be created if needed)
|
||||||
|
"""
|
||||||
|
# Basic path validation - dump_package can work with directories too
|
||||||
|
if not assembly_path or not assembly_path.strip():
|
||||||
|
return _format_error(ValueError("Assembly path cannot be empty"), "path validation")
|
||||||
|
|
||||||
|
resolved_path = os.path.realpath(os.path.expanduser(assembly_path.strip()))
|
||||||
|
if not os.path.exists(resolved_path):
|
||||||
|
return _format_error(FileNotFoundError(f"Path not found: {resolved_path}"), "path validation")
|
||||||
|
|
||||||
|
if not output_dir or not output_dir.strip():
|
||||||
|
return _format_error(ValueError("Output directory is required"), "validation")
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Dumping package assemblies from: {resolved_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapper = get_wrapper(ctx)
|
||||||
|
|
||||||
|
from .models import DumpPackageRequest
|
||||||
|
|
||||||
|
request = DumpPackageRequest(
|
||||||
|
assembly_path=resolved_path,
|
||||||
|
output_dir=output_dir.strip(),
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await wrapper.dump_package(request)
|
||||||
|
|
||||||
|
if response.success:
|
||||||
|
content = f"# Package Dump Complete\n\n"
|
||||||
|
content += f"**Output directory**: `{response.output_path}`\n\n"
|
||||||
|
|
||||||
|
if response.assemblies_dumped:
|
||||||
|
content += f"**Assemblies extracted** ({len(response.assemblies_dumped)}):\n\n"
|
||||||
|
for asm in response.assemblies_dumped:
|
||||||
|
content += f"- `{asm}`\n"
|
||||||
|
else:
|
||||||
|
content += "No assemblies found in package.\n"
|
||||||
|
|
||||||
|
content += "\n**TIP**: Use `list_types` or `decompile_assembly` on any of these assemblies for analysis."
|
||||||
|
return content
|
||||||
|
else:
|
||||||
|
return f"Failed to dump package: {response.error_message}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error dumping package: {e}")
|
||||||
|
return _format_error(e)
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def get_assembly_info(assembly_path: str, ctx: Context | None = None) -> str:
|
async def get_assembly_info(assembly_path: str, ctx: Context | None = None) -> str:
|
||||||
"""Get metadata and version information about a .NET assembly.
|
"""Get metadata and version information about a .NET assembly.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user