version: 0.1.1 use fastmcp

This commit is contained in:
Borealin 2025-08-04 11:09:26 +08:00
parent b6a09eabfe
commit 57472070e2
4 changed files with 250 additions and 525 deletions

View File

@ -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"}

View File

@ -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:

View File

@ -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."""

View File

@ -1,350 +1,243 @@
"""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
)
# Set up 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() return ilspy_wrapper
except RuntimeError as e:
return CallToolResult( @mcp.tool()
content=[TextContent(type="text", text=f"Error: {str(e)}")] 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: try:
if name == "decompile_assembly": wrapper = get_wrapper()
request = DecompileRequest(**arguments)
response = await ilspy_wrapper.decompile(request) # Use simplified request object (no complex pydantic validation needed)
from .models import DecompileRequest
if response.success: request = DecompileRequest(
if response.source_code: assembly_path=assembly_path,
content = f"# Decompiled: {response.assembly_name}" output_dir=output_dir,
if response.type_name: type_name=type_name,
content += f" - {response.type_name}" language_version=LanguageVersion(language_version),
content += "\n\n```csharp\n" + response.source_code + "\n```" create_project=create_project,
else: show_il_code=show_il_code,
content = f"Decompilation successful. Files saved to: {response.output_path}" remove_dead_code=remove_dead_code,
nested_directories=nested_directories
)
response = await wrapper.decompile(request)
if response.success:
if response.source_code:
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```"
return content
else: else:
content = f"Decompilation failed: {response.error_message}" return f"Decompilation successful! Files saved to: {response.output_path}"
return CallToolResult(
content=[TextContent(type="text", text=content)]
)
elif name == "list_types":
request = ListTypesRequest(**arguments)
response = await ilspy_wrapper.list_types(request)
if response.success:
if response.types:
content = f"# Types in {arguments['assembly_path']}\n\n"
content += f"Found {response.total_count} types:\n\n"
# Group by namespace
by_namespace = {}
for type_info in response.types:
ns = type_info.namespace or "(Global)"
if ns not in by_namespace:
by_namespace[ns] = []
by_namespace[ns].append(type_info)
for namespace, types in sorted(by_namespace.items()):
content += f"## {namespace}\n\n"
for type_info in sorted(types, key=lambda t: t.name):
content += f"- **{type_info.name}** ({type_info.kind})\n"
content += f" - Full name: `{type_info.full_name}`\n"
content += "\n"
else:
content = "No types found in the assembly."
else:
content = f"Failed to list types: {response.error_message}"
return CallToolResult(
content=[TextContent(type="text", text=content)]
)
elif name == "generate_diagrammer":
request = GenerateDiagrammerRequest(**arguments)
response = await ilspy_wrapper.generate_diagrammer(request)
if response["success"]:
content = f"HTML diagrammer generated successfully!\n"
content += f"Output directory: {response['output_directory']}\n"
content += f"Open the HTML file in a web browser to view the interactive diagram."
else:
content = f"Failed to generate diagrammer: {response['error_message']}"
return CallToolResult(
content=[TextContent(type="text", text=content)]
)
elif name == "get_assembly_info":
request = AssemblyInfoRequest(**arguments)
info = await ilspy_wrapper.get_assembly_info(request)
content = f"# Assembly Information\n\n"
content += f"- **Name**: {info.name}\n"
content += f"- **Full Name**: {info.full_name}\n"
content += f"- **Location**: {info.location}\n"
content += f"- **Version**: {info.version}\n"
if info.target_framework:
content += f"- **Target Framework**: {info.target_framework}\n"
if info.runtime_version:
content += f"- **Runtime Version**: {info.runtime_version}\n"
content += f"- **Is Signed**: {info.is_signed}\n"
content += f"- **Has Debug Info**: {info.has_debug_info}\n"
return CallToolResult(
content=[TextContent(type="text", text=content)]
)
else: else:
return CallToolResult( return f"Decompilation failed: {response.error_message}"
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"Decompilation error: {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.")]
)
@mcp.tool()
@server.list_prompts() async def list_types(
async def handle_list_prompts() -> ListPromptsResult: assembly_path: str,
"""List available prompts.""" entity_types: list[str] = None,
return ListPromptsResult( ctx: Context = None
prompts=[ ) -> str:
Prompt( """List types (classes, interfaces, structs, etc.) in a .NET assembly
name="analyze_assembly",
description="Analyze a .NET assembly and provide insights about its structure and types", Args:
arguments=[ assembly_path: Path to the .NET assembly file (.dll or .exe)
PromptArgument( entity_types: Types of entities to list (c=class, i=interface, s=struct, d=delegate, e=enum)
name="assembly_path", """
description="Path to the .NET assembly file", if ctx:
required=True await ctx.info(f"Listing types in assembly: {assembly_path}")
),
PromptArgument( try:
name="focus_area", wrapper = get_wrapper()
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}". # 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
)
response = await wrapper.list_types(request)
if response.success and response.types:
content = f"# Types in {assembly_path}\n\n"
content += f"Found {response.total_count} types:\n\n"
# Group by namespace
by_namespace = {}
for type_info in response.types:
ns = type_info.namespace or "(Global)"
if ns not in by_namespace:
by_namespace[ns] = []
by_namespace[ns].append(type_info)
for namespace, types in sorted(by_namespace.items()):
content += f"## {namespace}\n\n"
for type_info in sorted(types, key=lambda t: t.name):
content += f"- **{type_info.name}** ({type_info.kind})\n"
content += f" - Full name: `{type_info.full_name}`\n"
content += "\n"
return 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
)
response = await wrapper.generate_diagrammer(request)
if response["success"]:
return f"HTML diagram generated successfully!\nOutput directory: {response['output_directory']}\nOpen the HTML file in a web browser to view the interactive diagram."
else:
return f"Failed to generate diagram: {response['error_message']}"
except Exception as e:
logger.error(f"Error generating diagram: {e}")
return f"Error: {str(e)}"
@mcp.tool()
async def get_assembly_info(
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"- **Name**: {info.name}\n"
content += f"- **Full Name**: {info.full_name}\n"
content += f"- **Location**: {info.location}\n"
content += f"- **Version**: {info.version}\n"
if info.target_framework:
content += f"- **Target Framework**: {info.target_framework}\n"
if info.runtime_version:
content += f"- **Runtime Version**: {info.runtime_version}\n"
content += f"- **Is Signed**: {info.is_signed}\n"
content += f"- **Has Debug Info**: {info.has_debug_info}\n"
return content
except Exception as e:
logger.error(f"Error getting assembly info: {e}")
return f"Error: {str(e)}"
# FastMCP automatically handles prompts
@mcp.prompt()
def analyze_assembly_prompt(assembly_path: str, focus_area: str = "types") -> str:
"""Prompt template for analyzing .NET assemblies"""
return 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
@ -355,22 +248,11 @@ Please help me understand:
Focus area: {focus_area} 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
@ -380,37 +262,7 @@ 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()