version: 0.1.1 use fastmcp
This commit is contained in:
parent
b6a09eabfe
commit
57472070e2
@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "ilspy-mcp-server"
|
name = "ilspy-mcp-server"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
description = "MCP Server for ILSpy .NET Decompiler"
|
description = "MCP Server for ILSpy .NET Decompiler"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Borealin", email = "me@borealin.cn"}
|
{name = "Borealin", email = "me@borealin.cn"}
|
||||||
|
|||||||
@ -113,29 +113,17 @@ class ILSpyWrapper:
|
|||||||
if request.create_project:
|
if request.create_project:
|
||||||
args.append("-p")
|
args.append("-p")
|
||||||
|
|
||||||
# Add IL code flags
|
# Add IL code flag
|
||||||
if request.show_il_sequence_points:
|
if request.show_il_code:
|
||||||
args.append("--il-sequence-points")
|
|
||||||
elif request.show_il_code:
|
|
||||||
args.append("-il")
|
args.append("-il")
|
||||||
|
|
||||||
# Add PDB generation
|
|
||||||
if request.generate_pdb:
|
|
||||||
args.append("-genpdb")
|
|
||||||
|
|
||||||
# Add PDB usage
|
|
||||||
if request.use_pdb:
|
|
||||||
args.extend(["-usepdb", request.use_pdb])
|
|
||||||
|
|
||||||
# Add reference paths
|
# Add reference paths
|
||||||
for ref_path in request.reference_paths:
|
for ref_path in request.reference_paths:
|
||||||
args.extend(["-r", ref_path])
|
args.extend(["-r", ref_path])
|
||||||
|
|
||||||
# Add optimization flags
|
# Add optimization flag
|
||||||
if request.remove_dead_code:
|
if request.remove_dead_code:
|
||||||
args.append("--no-dead-code")
|
args.append("--no-dead-code")
|
||||||
if request.remove_dead_stores:
|
|
||||||
args.append("--no-dead-stores")
|
|
||||||
|
|
||||||
# Add directory structure flag
|
# Add directory structure flag
|
||||||
if request.nested_directories:
|
if request.nested_directories:
|
||||||
|
|||||||
@ -1,10 +1,6 @@
|
|||||||
"""Data models for ILSpy MCP Server."""
|
from typing import Optional, List
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
from typing import Optional, List, Dict, Any
|
|
||||||
from pydantic import BaseModel, Field, validator
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
class LanguageVersion(str, Enum):
|
class LanguageVersion(str, Enum):
|
||||||
"""C# Language versions supported by ILSpy."""
|
"""C# Language versions supported by ILSpy."""
|
||||||
@ -26,7 +22,6 @@ class LanguageVersion(str, Enum):
|
|||||||
PREVIEW = "Preview"
|
PREVIEW = "Preview"
|
||||||
LATEST = "Latest"
|
LATEST = "Latest"
|
||||||
|
|
||||||
|
|
||||||
class EntityType(str, Enum):
|
class EntityType(str, Enum):
|
||||||
"""Entity types that can be listed."""
|
"""Entity types that can be listed."""
|
||||||
CLASS = "c"
|
CLASS = "c"
|
||||||
@ -34,91 +29,23 @@ class EntityType(str, Enum):
|
|||||||
STRUCT = "s"
|
STRUCT = "s"
|
||||||
DELEGATE = "d"
|
DELEGATE = "d"
|
||||||
ENUM = "e"
|
ENUM = "e"
|
||||||
|
|
||||||
|
|
||||||
class DecompileRequest(BaseModel):
|
class DecompileRequest(BaseModel):
|
||||||
"""Request to decompile a .NET assembly."""
|
"""Request to decompile a .NET assembly."""
|
||||||
assembly_path: str = Field(..., description="Path to the .NET assembly file")
|
assembly_path: str
|
||||||
output_dir: Optional[str] = Field(None, description="Output directory for decompiled files")
|
output_dir: Optional[str] = None
|
||||||
type_name: Optional[str] = Field(None, description="Fully qualified name of the type to decompile")
|
type_name: Optional[str] = None
|
||||||
language_version: LanguageVersion = Field(LanguageVersion.LATEST, description="C# language version")
|
language_version: LanguageVersion = LanguageVersion.LATEST
|
||||||
create_project: bool = Field(False, description="Create a compilable project")
|
create_project: bool = False
|
||||||
show_il_code: bool = Field(False, description="Show IL code")
|
show_il_code: bool = False
|
||||||
show_il_sequence_points: bool = Field(False, description="Show IL with sequence points")
|
reference_paths: List[str] = Field(default_factory=list)
|
||||||
generate_pdb: bool = Field(False, description="Generate PDB file")
|
remove_dead_code: bool = False
|
||||||
use_pdb: Optional[str] = Field(None, description="Path to PDB file for variable names")
|
nested_directories: bool = False
|
||||||
reference_paths: List[str] = Field(default_factory=list, description="Reference assembly paths")
|
|
||||||
remove_dead_code: bool = Field(False, description="Remove dead code")
|
|
||||||
remove_dead_stores: bool = Field(False, description="Remove dead stores")
|
|
||||||
nested_directories: bool = Field(False, description="Use nested directories for namespaces")
|
|
||||||
|
|
||||||
@validator('assembly_path')
|
|
||||||
def validate_assembly_path(cls, v):
|
|
||||||
"""Validate that the assembly path exists and has a valid extension."""
|
|
||||||
if not v:
|
|
||||||
raise ValueError("Assembly path cannot be empty")
|
|
||||||
|
|
||||||
if not os.path.exists(v):
|
|
||||||
raise ValueError(f"Assembly file not found: {v}")
|
|
||||||
|
|
||||||
valid_extensions = ['.dll', '.exe']
|
|
||||||
if not any(v.lower().endswith(ext) for ext in valid_extensions):
|
|
||||||
raise ValueError(f"Invalid assembly file extension. Expected: {', '.join(valid_extensions)}")
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator('output_dir')
|
|
||||||
def validate_output_dir(cls, v):
|
|
||||||
"""Validate output directory if specified."""
|
|
||||||
if v and not os.path.isdir(os.path.dirname(v) if os.path.dirname(v) else '.'):
|
|
||||||
raise ValueError(f"Output directory parent does not exist: {v}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator('use_pdb')
|
|
||||||
def validate_pdb_path(cls, v):
|
|
||||||
"""Validate PDB file path if specified."""
|
|
||||||
if v and not os.path.exists(v):
|
|
||||||
raise ValueError(f"PDB file not found: {v}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator('reference_paths')
|
|
||||||
def validate_reference_paths(cls, v):
|
|
||||||
"""Validate reference assembly paths."""
|
|
||||||
for ref_path in v:
|
|
||||||
if not os.path.exists(ref_path):
|
|
||||||
raise ValueError(f"Reference assembly not found: {ref_path}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class ListTypesRequest(BaseModel):
|
class ListTypesRequest(BaseModel):
|
||||||
"""Request to list types in an assembly."""
|
"""Request to list types in an assembly."""
|
||||||
assembly_path: str = Field(..., description="Path to the .NET assembly file")
|
assembly_path: str
|
||||||
entity_types: List[EntityType] = Field(default_factory=lambda: [EntityType.CLASS], description="Types of entities to list")
|
entity_types: List[EntityType] = Field(default_factory=lambda: [EntityType.CLASS])
|
||||||
reference_paths: List[str] = Field(default_factory=list, description="Reference assembly paths")
|
reference_paths: List[str] = Field(default_factory=list)
|
||||||
|
|
||||||
@validator('assembly_path')
|
|
||||||
def validate_assembly_path(cls, v):
|
|
||||||
"""Validate that the assembly path exists and has a valid extension."""
|
|
||||||
if not v:
|
|
||||||
raise ValueError("Assembly path cannot be empty")
|
|
||||||
|
|
||||||
if not os.path.exists(v):
|
|
||||||
raise ValueError(f"Assembly file not found: {v}")
|
|
||||||
|
|
||||||
valid_extensions = ['.dll', '.exe']
|
|
||||||
if not any(v.lower().endswith(ext) for ext in valid_extensions):
|
|
||||||
raise ValueError(f"Invalid assembly file extension. Expected: {', '.join(valid_extensions)}")
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator('reference_paths')
|
|
||||||
def validate_reference_paths(cls, v):
|
|
||||||
"""Validate reference assembly paths."""
|
|
||||||
for ref_path in v:
|
|
||||||
if not os.path.exists(ref_path):
|
|
||||||
raise ValueError(f"Reference assembly not found: {ref_path}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class TypeInfo(BaseModel):
|
class TypeInfo(BaseModel):
|
||||||
"""Information about a type in an assembly."""
|
"""Information about a type in an assembly."""
|
||||||
@ -127,7 +54,6 @@ class TypeInfo(BaseModel):
|
|||||||
kind: str
|
kind: str
|
||||||
namespace: Optional[str] = None
|
namespace: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class DecompileResponse(BaseModel):
|
class DecompileResponse(BaseModel):
|
||||||
"""Response from decompilation operation."""
|
"""Response from decompilation operation."""
|
||||||
success: bool
|
success: bool
|
||||||
@ -137,7 +63,6 @@ class DecompileResponse(BaseModel):
|
|||||||
assembly_name: str
|
assembly_name: str
|
||||||
type_name: Optional[str] = None
|
type_name: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class ListTypesResponse(BaseModel):
|
class ListTypesResponse(BaseModel):
|
||||||
"""Response from list types operation."""
|
"""Response from list types operation."""
|
||||||
success: bool
|
success: bool
|
||||||
@ -145,59 +70,19 @@ class ListTypesResponse(BaseModel):
|
|||||||
total_count: int = 0
|
total_count: int = 0
|
||||||
error_message: Optional[str] = None
|
error_message: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class GenerateDiagrammerRequest(BaseModel):
|
class GenerateDiagrammerRequest(BaseModel):
|
||||||
"""Request to generate HTML diagrammer."""
|
"""Request to generate HTML diagrammer."""
|
||||||
assembly_path: str = Field(..., description="Path to the .NET assembly file")
|
assembly_path: str
|
||||||
output_dir: Optional[str] = Field(None, description="Output directory for diagrammer")
|
output_dir: Optional[str] = None
|
||||||
include_pattern: Optional[str] = Field(None, description="Regex pattern for types to include")
|
include_pattern: Optional[str] = None
|
||||||
exclude_pattern: Optional[str] = Field(None, description="Regex pattern for types to exclude")
|
exclude_pattern: Optional[str] = None
|
||||||
docs_path: Optional[str] = Field(None, description="Path to XML documentation file")
|
docs_path: Optional[str] = None
|
||||||
strip_namespaces: List[str] = Field(default_factory=list, description="Namespaces to strip from docs")
|
strip_namespaces: List[str] = Field(default_factory=list)
|
||||||
report_excluded: bool = Field(False, description="Generate report of excluded types")
|
report_excluded: bool = False
|
||||||
|
|
||||||
@validator('assembly_path')
|
|
||||||
def validate_assembly_path(cls, v):
|
|
||||||
"""Validate that the assembly path exists and has a valid extension."""
|
|
||||||
if not v:
|
|
||||||
raise ValueError("Assembly path cannot be empty")
|
|
||||||
|
|
||||||
if not os.path.exists(v):
|
|
||||||
raise ValueError(f"Assembly file not found: {v}")
|
|
||||||
|
|
||||||
valid_extensions = ['.dll', '.exe']
|
|
||||||
if not any(v.lower().endswith(ext) for ext in valid_extensions):
|
|
||||||
raise ValueError(f"Invalid assembly file extension. Expected: {', '.join(valid_extensions)}")
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
@validator('docs_path')
|
|
||||||
def validate_docs_path(cls, v):
|
|
||||||
"""Validate XML documentation file path if specified."""
|
|
||||||
if v and not os.path.exists(v):
|
|
||||||
raise ValueError(f"Documentation file not found: {v}")
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class AssemblyInfoRequest(BaseModel):
|
class AssemblyInfoRequest(BaseModel):
|
||||||
"""Request to get assembly information."""
|
"""Request to get assembly information."""
|
||||||
assembly_path: str = Field(..., description="Path to the .NET assembly file")
|
assembly_path: str
|
||||||
|
|
||||||
@validator('assembly_path')
|
|
||||||
def validate_assembly_path(cls, v):
|
|
||||||
"""Validate that the assembly path exists and has a valid extension."""
|
|
||||||
if not v:
|
|
||||||
raise ValueError("Assembly path cannot be empty")
|
|
||||||
|
|
||||||
if not os.path.exists(v):
|
|
||||||
raise ValueError(f"Assembly file not found: {v}")
|
|
||||||
|
|
||||||
valid_extensions = ['.dll', '.exe']
|
|
||||||
if not any(v.lower().endswith(ext) for ext in valid_extensions):
|
|
||||||
raise ValueError(f"Invalid assembly file extension. Expected: {', '.join(valid_extensions)}")
|
|
||||||
|
|
||||||
return v
|
|
||||||
|
|
||||||
|
|
||||||
class AssemblyInfo(BaseModel):
|
class AssemblyInfo(BaseModel):
|
||||||
"""Information about an assembly."""
|
"""Information about an assembly."""
|
||||||
|
|||||||
@ -1,213 +1,133 @@
|
|||||||
"""MCP Server for ILSpy .NET Decompiler."""
|
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
from typing import Optional
|
||||||
from typing import Any, Dict, List, Optional
|
|
||||||
|
|
||||||
from mcp.server import Server
|
|
||||||
from mcp.server.models import InitializationOptions
|
|
||||||
from mcp.server.stdio import stdio_server
|
|
||||||
from mcp.types import (
|
|
||||||
CallToolRequest,
|
|
||||||
CallToolResult,
|
|
||||||
ListToolsRequest,
|
|
||||||
ListToolsResult,
|
|
||||||
Tool,
|
|
||||||
TextContent,
|
|
||||||
GetPromptRequest,
|
|
||||||
GetPromptResult,
|
|
||||||
ListPromptsRequest,
|
|
||||||
ListPromptsResult,
|
|
||||||
Prompt,
|
|
||||||
PromptMessage,
|
|
||||||
PromptArgument,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
from mcp.server.fastmcp import FastMCP, Context
|
||||||
from .ilspy_wrapper import ILSpyWrapper
|
from .ilspy_wrapper import ILSpyWrapper
|
||||||
from .models import (
|
from .models import LanguageVersion, EntityType
|
||||||
DecompileRequest, ListTypesRequest, GenerateDiagrammerRequest,
|
|
||||||
AssemblyInfoRequest, LanguageVersion, EntityType
|
|
||||||
)
|
|
||||||
|
|
||||||
# Setup logging
|
# Setup logging
|
||||||
log_level = os.getenv('LOGLEVEL', 'INFO').upper()
|
log_level = os.getenv('LOGLEVEL', 'INFO').upper()
|
||||||
numeric_level = getattr(logging, log_level, logging.INFO)
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=numeric_level,
|
level=getattr(logging, log_level, logging.INFO),
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||||
)
|
)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Create the MCP server
|
# Create FastMCP server - much simpler than before!
|
||||||
server = Server("ilspy-mcp-server")
|
mcp = FastMCP("ilspy-mcp-server")
|
||||||
|
|
||||||
# Global ILSpy wrapper instance
|
# Global ILSpy wrapper
|
||||||
ilspy_wrapper: Optional[ILSpyWrapper] = None
|
ilspy_wrapper: Optional[ILSpyWrapper] = None
|
||||||
|
|
||||||
|
def get_wrapper() -> ILSpyWrapper:
|
||||||
@server.list_tools()
|
"""Get ILSpy wrapper instance"""
|
||||||
async def handle_list_tools() -> ListToolsResult:
|
|
||||||
"""List available tools."""
|
|
||||||
return ListToolsResult(
|
|
||||||
tools=[
|
|
||||||
Tool(
|
|
||||||
name="decompile_assembly",
|
|
||||||
description="Decompile a .NET assembly to C# source code",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"assembly_path": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path to the .NET assembly file (.dll or .exe)"
|
|
||||||
},
|
|
||||||
"output_dir": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Output directory for decompiled files (optional)"
|
|
||||||
},
|
|
||||||
"type_name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Fully qualified name of specific type to decompile (optional)"
|
|
||||||
},
|
|
||||||
"language_version": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [lv.value for lv in LanguageVersion],
|
|
||||||
"description": "C# language version to use",
|
|
||||||
"default": "Latest"
|
|
||||||
},
|
|
||||||
"create_project": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Create a compilable project with multiple files",
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"show_il_code": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Show IL code instead of C#",
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"remove_dead_code": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Remove dead code during decompilation",
|
|
||||||
"default": False
|
|
||||||
},
|
|
||||||
"nested_directories": {
|
|
||||||
"type": "boolean",
|
|
||||||
"description": "Use nested directories for namespaces",
|
|
||||||
"default": False
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["assembly_path"]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Tool(
|
|
||||||
name="list_types",
|
|
||||||
description="List types (classes, interfaces, structs, etc.) in a .NET assembly",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"assembly_path": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path to the .NET assembly file (.dll or .exe)"
|
|
||||||
},
|
|
||||||
"entity_types": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [et.value for et in EntityType]
|
|
||||||
},
|
|
||||||
"description": "Types of entities to list (c=class, i=interface, s=struct, d=delegate, e=enum)",
|
|
||||||
"default": ["c"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["assembly_path"]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Tool(
|
|
||||||
name="generate_diagrammer",
|
|
||||||
description="Generate an interactive HTML diagrammer for visualizing assembly structure",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"assembly_path": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path to the .NET assembly file (.dll or .exe)"
|
|
||||||
},
|
|
||||||
"output_dir": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Output directory for the diagrammer (optional)"
|
|
||||||
},
|
|
||||||
"include_pattern": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Regex pattern for types to include (optional)"
|
|
||||||
},
|
|
||||||
"exclude_pattern": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Regex pattern for types to exclude (optional)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["assembly_path"]
|
|
||||||
}
|
|
||||||
),
|
|
||||||
Tool(
|
|
||||||
name="get_assembly_info",
|
|
||||||
description="Get basic information about a .NET assembly",
|
|
||||||
inputSchema={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"assembly_path": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path to the .NET assembly file (.dll or .exe)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["assembly_path"]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@server.call_tool()
|
|
||||||
async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResult:
|
|
||||||
"""Handle tool calls."""
|
|
||||||
global ilspy_wrapper
|
global ilspy_wrapper
|
||||||
|
|
||||||
if ilspy_wrapper is None:
|
if ilspy_wrapper is None:
|
||||||
try:
|
|
||||||
ilspy_wrapper = ILSpyWrapper()
|
ilspy_wrapper = ILSpyWrapper()
|
||||||
except RuntimeError as e:
|
return ilspy_wrapper
|
||||||
return CallToolResult(
|
|
||||||
content=[TextContent(type="text", text=f"Error: {str(e)}")]
|
@mcp.tool()
|
||||||
|
async def decompile_assembly(
|
||||||
|
assembly_path: str,
|
||||||
|
output_dir: str = None,
|
||||||
|
type_name: str = None,
|
||||||
|
language_version: str = "Latest",
|
||||||
|
create_project: bool = False,
|
||||||
|
show_il_code: bool = False,
|
||||||
|
remove_dead_code: bool = False,
|
||||||
|
nested_directories: bool = False,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""Decompile a .NET assembly to C# source code
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assembly_path: Path to the .NET assembly file (.dll or .exe)
|
||||||
|
output_dir: Output directory for decompiled files (optional)
|
||||||
|
type_name: Fully qualified name of specific type to decompile (optional)
|
||||||
|
language_version: C# language version to use (default: Latest)
|
||||||
|
create_project: Create a compilable project with multiple files
|
||||||
|
show_il_code: Show IL code instead of C#
|
||||||
|
remove_dead_code: Remove dead code during decompilation
|
||||||
|
nested_directories: Use nested directories for namespaces
|
||||||
|
"""
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Starting decompilation of assembly: {assembly_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapper = get_wrapper()
|
||||||
|
|
||||||
|
# Use simplified request object (no complex pydantic validation needed)
|
||||||
|
from .models import DecompileRequest
|
||||||
|
request = DecompileRequest(
|
||||||
|
assembly_path=assembly_path,
|
||||||
|
output_dir=output_dir,
|
||||||
|
type_name=type_name,
|
||||||
|
language_version=LanguageVersion(language_version),
|
||||||
|
create_project=create_project,
|
||||||
|
show_il_code=show_il_code,
|
||||||
|
remove_dead_code=remove_dead_code,
|
||||||
|
nested_directories=nested_directories
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
response = await wrapper.decompile(request)
|
||||||
if name == "decompile_assembly":
|
|
||||||
request = DecompileRequest(**arguments)
|
|
||||||
response = await ilspy_wrapper.decompile(request)
|
|
||||||
|
|
||||||
if response.success:
|
if response.success:
|
||||||
if response.source_code:
|
if response.source_code:
|
||||||
content = f"# Decompiled: {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 += "\n\n```csharp\n" + response.source_code + "\n```"
|
content += f"\n\n```csharp\n{response.source_code}\n```"
|
||||||
|
return content
|
||||||
else:
|
else:
|
||||||
content = f"Decompilation successful. Files saved to: {response.output_path}"
|
return f"Decompilation successful! Files saved to: {response.output_path}"
|
||||||
else:
|
else:
|
||||||
content = f"Decompilation failed: {response.error_message}"
|
return f"Decompilation failed: {response.error_message}"
|
||||||
|
|
||||||
return CallToolResult(
|
except Exception as e:
|
||||||
content=[TextContent(type="text", text=content)]
|
logger.error(f"Decompilation error: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def list_types(
|
||||||
|
assembly_path: str,
|
||||||
|
entity_types: list[str] = None,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""List types (classes, interfaces, structs, etc.) in a .NET assembly
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assembly_path: Path to the .NET assembly file (.dll or .exe)
|
||||||
|
entity_types: Types of entities to list (c=class, i=interface, s=struct, d=delegate, e=enum)
|
||||||
|
"""
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Listing types in assembly: {assembly_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapper = get_wrapper()
|
||||||
|
|
||||||
|
# Default to list only classes
|
||||||
|
if entity_types is None:
|
||||||
|
entity_types = ["c"]
|
||||||
|
|
||||||
|
# Convert to EntityType enums
|
||||||
|
entity_type_enums = []
|
||||||
|
for et in entity_types:
|
||||||
|
try:
|
||||||
|
entity_type_enums.append(EntityType(et))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
from .models import ListTypesRequest
|
||||||
|
request = ListTypesRequest(
|
||||||
|
assembly_path=assembly_path,
|
||||||
|
entity_types=entity_type_enums
|
||||||
)
|
)
|
||||||
|
|
||||||
elif name == "list_types":
|
response = await wrapper.list_types(request)
|
||||||
request = ListTypesRequest(**arguments)
|
|
||||||
response = await ilspy_wrapper.list_types(request)
|
|
||||||
|
|
||||||
if response.success:
|
if response.success and response.types:
|
||||||
if response.types:
|
content = f"# Types in {assembly_path}\n\n"
|
||||||
content = f"# Types in {arguments['assembly_path']}\n\n"
|
|
||||||
content += f"Found {response.total_count} types:\n\n"
|
content += f"Found {response.total_count} types:\n\n"
|
||||||
|
|
||||||
# Group by namespace
|
# Group by namespace
|
||||||
@ -224,33 +144,76 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResu
|
|||||||
content += f"- **{type_info.name}** ({type_info.kind})\n"
|
content += f"- **{type_info.name}** ({type_info.kind})\n"
|
||||||
content += f" - Full name: `{type_info.full_name}`\n"
|
content += f" - Full name: `{type_info.full_name}`\n"
|
||||||
content += "\n"
|
content += "\n"
|
||||||
else:
|
|
||||||
content = "No types found in the assembly."
|
|
||||||
else:
|
|
||||||
content = f"Failed to list types: {response.error_message}"
|
|
||||||
|
|
||||||
return CallToolResult(
|
return content
|
||||||
content=[TextContent(type="text", text=content)]
|
else:
|
||||||
|
return response.error_message or "No types found in assembly"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error listing types: {e}")
|
||||||
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def generate_diagrammer(
|
||||||
|
assembly_path: str,
|
||||||
|
output_dir: str = None,
|
||||||
|
include_pattern: str = None,
|
||||||
|
exclude_pattern: str = None,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""Generate an interactive HTML diagrammer for visualizing assembly structure
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assembly_path: Path to the .NET assembly file (.dll or .exe)
|
||||||
|
output_dir: Output directory for the diagrammer (optional)
|
||||||
|
include_pattern: Regex pattern for types to include (optional)
|
||||||
|
exclude_pattern: Regex pattern for types to exclude (optional)
|
||||||
|
"""
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Generating assembly diagram: {assembly_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapper = get_wrapper()
|
||||||
|
|
||||||
|
from .models import GenerateDiagrammerRequest
|
||||||
|
request = GenerateDiagrammerRequest(
|
||||||
|
assembly_path=assembly_path,
|
||||||
|
output_dir=output_dir,
|
||||||
|
include_pattern=include_pattern,
|
||||||
|
exclude_pattern=exclude_pattern
|
||||||
)
|
)
|
||||||
|
|
||||||
elif name == "generate_diagrammer":
|
response = await wrapper.generate_diagrammer(request)
|
||||||
request = GenerateDiagrammerRequest(**arguments)
|
|
||||||
response = await ilspy_wrapper.generate_diagrammer(request)
|
|
||||||
|
|
||||||
if response["success"]:
|
if response["success"]:
|
||||||
content = f"HTML diagrammer generated successfully!\n"
|
return f"HTML diagram generated successfully!\nOutput directory: {response['output_directory']}\nOpen the HTML file in a web browser to view the interactive diagram."
|
||||||
content += f"Output directory: {response['output_directory']}\n"
|
|
||||||
content += f"Open the HTML file in a web browser to view the interactive diagram."
|
|
||||||
else:
|
else:
|
||||||
content = f"Failed to generate diagrammer: {response['error_message']}"
|
return f"Failed to generate diagram: {response['error_message']}"
|
||||||
|
|
||||||
return CallToolResult(
|
except Exception as e:
|
||||||
content=[TextContent(type="text", text=content)]
|
logger.error(f"Error generating diagram: {e}")
|
||||||
)
|
return f"Error: {str(e)}"
|
||||||
|
|
||||||
elif name == "get_assembly_info":
|
@mcp.tool()
|
||||||
request = AssemblyInfoRequest(**arguments)
|
async def get_assembly_info(
|
||||||
info = await ilspy_wrapper.get_assembly_info(request)
|
assembly_path: str,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""Get basic information about a .NET assembly
|
||||||
|
|
||||||
|
Args:
|
||||||
|
assembly_path: Path to the .NET assembly file (.dll or .exe)
|
||||||
|
"""
|
||||||
|
if ctx:
|
||||||
|
await ctx.info(f"Getting assembly info: {assembly_path}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapper = get_wrapper()
|
||||||
|
|
||||||
|
from .models import AssemblyInfoRequest
|
||||||
|
request = AssemblyInfoRequest(assembly_path=assembly_path)
|
||||||
|
|
||||||
|
info = await wrapper.get_assembly_info(request)
|
||||||
|
|
||||||
content = f"# Assembly Information\n\n"
|
content = f"# Assembly Information\n\n"
|
||||||
content += f"- **Name**: {info.name}\n"
|
content += f"- **Name**: {info.name}\n"
|
||||||
@ -264,87 +227,17 @@ async def handle_call_tool(name: str, arguments: Dict[str, Any]) -> CallToolResu
|
|||||||
content += f"- **Is Signed**: {info.is_signed}\n"
|
content += f"- **Is Signed**: {info.is_signed}\n"
|
||||||
content += f"- **Has Debug Info**: {info.has_debug_info}\n"
|
content += f"- **Has Debug Info**: {info.has_debug_info}\n"
|
||||||
|
|
||||||
return CallToolResult(
|
return content
|
||||||
content=[TextContent(type="text", text=content)]
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return CallToolResult(
|
|
||||||
content=[TextContent(type="text", text=f"Unknown tool: {name}")]
|
|
||||||
)
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
# Handle validation errors with user-friendly messages
|
|
||||||
logger.warning(f"Validation error in tool {name}: {e}")
|
|
||||||
return CallToolResult(
|
|
||||||
content=[TextContent(type="text", text=f"Validation Error: {str(e)}")]
|
|
||||||
)
|
|
||||||
except FileNotFoundError as e:
|
|
||||||
logger.warning(f"File not found in tool {name}: {e}")
|
|
||||||
return CallToolResult(
|
|
||||||
content=[TextContent(type="text", text=f"File Not Found: {str(e)}")]
|
|
||||||
)
|
|
||||||
except PermissionError as e:
|
|
||||||
logger.warning(f"Permission error in tool {name}: {e}")
|
|
||||||
return CallToolResult(
|
|
||||||
content=[TextContent(type="text", text=f"Permission Error: {str(e)}. Please check file permissions.")]
|
|
||||||
)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Unexpected error in tool {name}: {e}")
|
logger.error(f"Error getting assembly info: {e}")
|
||||||
return CallToolResult(
|
return f"Error: {str(e)}"
|
||||||
content=[TextContent(type="text", text=f"Unexpected Error: {str(e)}. Please check the logs for more details.")]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# FastMCP automatically handles prompts
|
||||||
@server.list_prompts()
|
@mcp.prompt()
|
||||||
async def handle_list_prompts() -> ListPromptsResult:
|
def analyze_assembly_prompt(assembly_path: str, focus_area: str = "types") -> str:
|
||||||
"""List available prompts."""
|
"""Prompt template for analyzing .NET assemblies"""
|
||||||
return ListPromptsResult(
|
return f"""I need to analyze the .NET assembly at "{assembly_path}".
|
||||||
prompts=[
|
|
||||||
Prompt(
|
|
||||||
name="analyze_assembly",
|
|
||||||
description="Analyze a .NET assembly and provide insights about its structure and types",
|
|
||||||
arguments=[
|
|
||||||
PromptArgument(
|
|
||||||
name="assembly_path",
|
|
||||||
description="Path to the .NET assembly file",
|
|
||||||
required=True
|
|
||||||
),
|
|
||||||
PromptArgument(
|
|
||||||
name="focus_area",
|
|
||||||
description="Specific area to focus on (types, namespaces, dependencies)",
|
|
||||||
required=False
|
|
||||||
)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
Prompt(
|
|
||||||
name="decompile_and_explain",
|
|
||||||
description="Decompile a specific type and provide explanation of its functionality",
|
|
||||||
arguments=[
|
|
||||||
PromptArgument(
|
|
||||||
name="assembly_path",
|
|
||||||
description="Path to the .NET assembly file",
|
|
||||||
required=True
|
|
||||||
),
|
|
||||||
PromptArgument(
|
|
||||||
name="type_name",
|
|
||||||
description="Fully qualified name of the type to analyze",
|
|
||||||
required=True
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@server.get_prompt()
|
|
||||||
async def handle_get_prompt(name: str, arguments: Dict[str, str]) -> GetPromptResult:
|
|
||||||
"""Handle prompt requests."""
|
|
||||||
if name == "analyze_assembly":
|
|
||||||
assembly_path = arguments.get("assembly_path", "")
|
|
||||||
focus_area = arguments.get("focus_area", "types")
|
|
||||||
|
|
||||||
prompt_text = f"""I need to analyze the .NET assembly at "{assembly_path}".
|
|
||||||
|
|
||||||
Please help me understand:
|
Please help me understand:
|
||||||
1. The overall structure and organization of the assembly
|
1. The overall structure and organization of the assembly
|
||||||
@ -356,21 +249,10 @@ Focus area: {focus_area}
|
|||||||
|
|
||||||
Start by listing the types in the assembly, then provide insights based on what you find."""
|
Start by listing the types in the assembly, then provide insights based on what you find."""
|
||||||
|
|
||||||
return GetPromptResult(
|
@mcp.prompt()
|
||||||
description=f"Analysis of .NET assembly: {assembly_path}",
|
def decompile_and_explain_prompt(assembly_path: str, type_name: str) -> str:
|
||||||
messages=[
|
"""Prompt template for decompiling and explaining specific types"""
|
||||||
PromptMessage(
|
return f"""I want to understand the type "{type_name}" from the assembly "{assembly_path}".
|
||||||
role="user",
|
|
||||||
content=TextContent(type="text", text=prompt_text)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
elif name == "decompile_and_explain":
|
|
||||||
assembly_path = arguments.get("assembly_path", "")
|
|
||||||
type_name = arguments.get("type_name", "")
|
|
||||||
|
|
||||||
prompt_text = f"""I want to understand the type "{type_name}" from the assembly "{assembly_path}".
|
|
||||||
|
|
||||||
Please:
|
Please:
|
||||||
1. Decompile this specific type
|
1. Decompile this specific type
|
||||||
@ -381,36 +263,6 @@ Please:
|
|||||||
Type to analyze: {type_name}
|
Type to analyze: {type_name}
|
||||||
Assembly: {assembly_path}"""
|
Assembly: {assembly_path}"""
|
||||||
|
|
||||||
return GetPromptResult(
|
|
||||||
description=f"Decompilation and analysis of {type_name}",
|
|
||||||
messages=[
|
|
||||||
PromptMessage(
|
|
||||||
role="user",
|
|
||||||
content=TextContent(type="text", text=prompt_text)
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown prompt: {name}")
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Main entry point for the server."""
|
|
||||||
async with stdio_server() as (read_stream, write_stream):
|
|
||||||
await server.run(
|
|
||||||
read_stream,
|
|
||||||
write_stream,
|
|
||||||
InitializationOptions(
|
|
||||||
server_name="ilspy-mcp-server",
|
|
||||||
server_version="0.1.0",
|
|
||||||
capabilities=server.get_capabilities(
|
|
||||||
notification_options=None,
|
|
||||||
experimental_capabilities={}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
# FastMCP automatically handles running
|
||||||
|
mcp.run()
|
||||||
Loading…
x
Reference in New Issue
Block a user