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]
|
||||
name = "mcilspy"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
description = "MCP Server for ILSpy .NET Decompiler"
|
||||
authors = [
|
||||
{name = "Ryan Malloy", email = "ryan@supported.systems"}
|
||||
|
||||
@ -19,6 +19,8 @@ from .models import (
|
||||
AssemblyInfoRequest,
|
||||
DecompileRequest,
|
||||
DecompileResponse,
|
||||
DumpPackageRequest,
|
||||
DumpPackageResponse,
|
||||
GenerateDiagrammerRequest,
|
||||
ListTypesRequest,
|
||||
ListTypesResponse,
|
||||
@ -257,6 +259,13 @@ class ILSpyWrapper:
|
||||
if request.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
|
||||
args.append("--disable-updatecheck")
|
||||
|
||||
@ -354,6 +363,12 @@ class ILSpyWrapper:
|
||||
if request.show_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
|
||||
args.append("--disable-updatecheck")
|
||||
|
||||
@ -595,6 +610,59 @@ class ILSpyWrapper:
|
||||
logger.exception(f"Error generating diagrammer: {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:
|
||||
"""Get detailed information about an assembly by decompiling assembly attributes.
|
||||
|
||||
|
||||
@ -93,6 +93,9 @@ class DecompileRequest(BaseModel):
|
||||
remove_dead_stores: bool = False
|
||||
show_il_sequence_points: 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):
|
||||
@ -144,6 +147,22 @@ class GenerateDiagrammerRequest(BaseModel):
|
||||
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):
|
||||
"""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"Path: `{new_status['ilspycmd_path']}`\n\n"
|
||||
"All ILSpy-based decompilation tools are now available:\n"
|
||||
"- `decompile_assembly`\n"
|
||||
"- `decompile_assembly` (with PDB generation support)\n"
|
||||
"- `list_types`\n"
|
||||
"- `search_types`\n"
|
||||
"- `search_strings`\n"
|
||||
"- `generate_diagrammer`\n"
|
||||
"- `dump_package`\n"
|
||||
"- `get_assembly_info`"
|
||||
)
|
||||
else:
|
||||
@ -581,6 +582,8 @@ async def decompile_assembly(
|
||||
remove_dead_stores: bool = False,
|
||||
show_il_sequence_points: bool = False,
|
||||
nested_directories: bool = False,
|
||||
generate_pdb: bool = False,
|
||||
use_pdb_variable_names: bool = False,
|
||||
ctx: Context | None = None,
|
||||
) -> str:
|
||||
"""Decompile a .NET assembly to readable C# source code.
|
||||
@ -590,6 +593,7 @@ async def decompile_assembly(
|
||||
- Extract specific types by fully qualified name
|
||||
- Generate compilable project structures for 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
|
||||
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
|
||||
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
|
||||
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
|
||||
try:
|
||||
@ -642,6 +648,8 @@ async def decompile_assembly(
|
||||
remove_dead_stores=remove_dead_stores,
|
||||
show_il_sequence_points=show_il_sequence_points,
|
||||
nested_directories=nested_directories,
|
||||
generate_pdb=generate_pdb,
|
||||
use_pdb_variable_names=use_pdb_variable_names,
|
||||
)
|
||||
|
||||
response = await wrapper.decompile(request)
|
||||
@ -832,6 +840,74 @@ async def generate_diagrammer(
|
||||
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()
|
||||
async def get_assembly_info(assembly_path: str, ctx: Context | None = None) -> str:
|
||||
"""Get metadata and version information about a .NET assembly.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user