diff --git a/pyproject.toml b/pyproject.toml index 9e44031..7976b14 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ilspy-mcp-server" -version = "0.1.0" +version = "0.1.1" description = "MCP Server for ILSpy .NET Decompiler" authors = [ {name = "Borealin", email = "me@borealin.cn"} diff --git a/src/ilspy_mcp_server/ilspy_wrapper.py b/src/ilspy_mcp_server/ilspy_wrapper.py index 5fd9407..931d790 100644 --- a/src/ilspy_mcp_server/ilspy_wrapper.py +++ b/src/ilspy_mcp_server/ilspy_wrapper.py @@ -113,29 +113,17 @@ class ILSpyWrapper: if request.create_project: args.append("-p") - # Add IL code flags - if request.show_il_sequence_points: - args.append("--il-sequence-points") - elif request.show_il_code: + # Add IL code flag + if request.show_il_code: 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 for ref_path in request.reference_paths: args.extend(["-r", ref_path]) - # Add optimization flags + # Add optimization flag if request.remove_dead_code: args.append("--no-dead-code") - if request.remove_dead_stores: - args.append("--no-dead-stores") # Add directory structure flag if request.nested_directories: diff --git a/src/ilspy_mcp_server/models.py b/src/ilspy_mcp_server/models.py index 1dc3389..36469ab 100644 --- a/src/ilspy_mcp_server/models.py +++ b/src/ilspy_mcp_server/models.py @@ -1,10 +1,6 @@ -"""Data models for ILSpy MCP Server.""" - -from typing import Optional, List, Dict, Any -from pydantic import BaseModel, Field, validator +from typing import Optional, List +from pydantic import BaseModel, Field from enum import Enum -import os - class LanguageVersion(str, Enum): """C# Language versions supported by ILSpy.""" @@ -26,7 +22,6 @@ class LanguageVersion(str, Enum): PREVIEW = "Preview" LATEST = "Latest" - class EntityType(str, Enum): """Entity types that can be listed.""" CLASS = "c" @@ -34,91 +29,23 @@ class EntityType(str, Enum): STRUCT = "s" DELEGATE = "d" ENUM = "e" - - class DecompileRequest(BaseModel): """Request to decompile a .NET assembly.""" - assembly_path: str = Field(..., description="Path to the .NET assembly file") - output_dir: Optional[str] = Field(None, description="Output directory for decompiled files") - type_name: Optional[str] = Field(None, description="Fully qualified name of the type to decompile") - language_version: LanguageVersion = Field(LanguageVersion.LATEST, description="C# language version") - create_project: bool = Field(False, description="Create a compilable project") - show_il_code: bool = Field(False, description="Show IL code") - show_il_sequence_points: bool = Field(False, description="Show IL with sequence points") - generate_pdb: bool = Field(False, description="Generate PDB file") - use_pdb: Optional[str] = Field(None, description="Path to PDB file for variable names") - 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 - + assembly_path: str + output_dir: Optional[str] = None + type_name: Optional[str] = None + language_version: LanguageVersion = LanguageVersion.LATEST + create_project: bool = False + show_il_code: bool = False + reference_paths: List[str] = Field(default_factory=list) + remove_dead_code: bool = False + nested_directories: bool = False class ListTypesRequest(BaseModel): """Request to list types in an assembly.""" - assembly_path: str = Field(..., description="Path to the .NET assembly file") - entity_types: List[EntityType] = Field(default_factory=lambda: [EntityType.CLASS], description="Types of entities to list") - reference_paths: List[str] = Field(default_factory=list, description="Reference assembly paths") - - @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 - + assembly_path: str + entity_types: List[EntityType] = Field(default_factory=lambda: [EntityType.CLASS]) + reference_paths: List[str] = Field(default_factory=list) class TypeInfo(BaseModel): """Information about a type in an assembly.""" @@ -127,7 +54,6 @@ class TypeInfo(BaseModel): kind: str namespace: Optional[str] = None - class DecompileResponse(BaseModel): """Response from decompilation operation.""" success: bool @@ -137,7 +63,6 @@ class DecompileResponse(BaseModel): assembly_name: str type_name: Optional[str] = None - class ListTypesResponse(BaseModel): """Response from list types operation.""" success: bool @@ -145,59 +70,19 @@ class ListTypesResponse(BaseModel): total_count: int = 0 error_message: Optional[str] = None - class GenerateDiagrammerRequest(BaseModel): """Request to generate HTML diagrammer.""" - assembly_path: str = Field(..., description="Path to the .NET assembly file") - output_dir: Optional[str] = Field(None, description="Output directory for diagrammer") - include_pattern: Optional[str] = Field(None, description="Regex pattern for types to include") - exclude_pattern: Optional[str] = Field(None, description="Regex pattern for types to exclude") - docs_path: Optional[str] = Field(None, description="Path to XML documentation file") - strip_namespaces: List[str] = Field(default_factory=list, description="Namespaces to strip from docs") - report_excluded: bool = Field(False, description="Generate report of excluded types") - - @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 - + assembly_path: str + output_dir: Optional[str] = None + include_pattern: Optional[str] = None + exclude_pattern: Optional[str] = None + docs_path: Optional[str] = None + strip_namespaces: List[str] = Field(default_factory=list) + report_excluded: bool = False class AssemblyInfoRequest(BaseModel): """Request to get assembly information.""" - assembly_path: str = Field(..., description="Path to the .NET assembly file") - - @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 - + assembly_path: str class AssemblyInfo(BaseModel): """Information about an assembly.""" diff --git a/src/ilspy_mcp_server/server.py b/src/ilspy_mcp_server/server.py index 71475bf..0d6b3eb 100644 --- a/src/ilspy_mcp_server/server.py +++ b/src/ilspy_mcp_server/server.py @@ -1,350 +1,243 @@ -"""MCP Server for ILSpy .NET Decompiler.""" - -import asyncio -import json import logging import os -import sys -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 typing import Optional +from mcp.server.fastmcp import FastMCP, Context from .ilspy_wrapper import ILSpyWrapper -from .models import ( - DecompileRequest, ListTypesRequest, GenerateDiagrammerRequest, - AssemblyInfoRequest, LanguageVersion, EntityType -) +from .models import LanguageVersion, EntityType -# Set up logging +# Setup logging log_level = os.getenv('LOGLEVEL', 'INFO').upper() -numeric_level = getattr(logging, log_level, logging.INFO) logging.basicConfig( - level=numeric_level, + level=getattr(logging, log_level, logging.INFO), format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger(__name__) -# Create the MCP server -server = Server("ilspy-mcp-server") +# Create FastMCP server - much simpler than before! +mcp = FastMCP("ilspy-mcp-server") -# Global ILSpy wrapper instance +# Global ILSpy wrapper ilspy_wrapper: Optional[ILSpyWrapper] = None - -@server.list_tools() -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.""" +def get_wrapper() -> ILSpyWrapper: + """Get ILSpy wrapper instance""" global ilspy_wrapper - if ilspy_wrapper is None: - try: - ilspy_wrapper = ILSpyWrapper() - except RuntimeError as e: - return CallToolResult( - content=[TextContent(type="text", text=f"Error: {str(e)}")] - ) + ilspy_wrapper = ILSpyWrapper() + return ilspy_wrapper + +@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: - if name == "decompile_assembly": - request = DecompileRequest(**arguments) - response = await ilspy_wrapper.decompile(request) - - if response.success: - if response.source_code: - content = f"# Decompiled: {response.assembly_name}" - if response.type_name: - content += f" - {response.type_name}" - content += "\n\n```csharp\n" + response.source_code + "\n```" - else: - content = f"Decompilation successful. Files saved to: {response.output_path}" + 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 + ) + + 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: - content = f"Decompilation failed: {response.error_message}" - - 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)] - ) - + return f"Decompilation successful! Files saved to: {response.output_path}" 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.")] - ) + return f"Decompilation failed: {response.error_message}" + except Exception as e: - logger.error(f"Unexpected error in tool {name}: {e}") - return CallToolResult( - content=[TextContent(type="text", text=f"Unexpected Error: {str(e)}. Please check the logs for more details.")] - ) + logger.error(f"Decompilation error: {e}") + return f"Error: {str(e)}" - -@server.list_prompts() -async def handle_list_prompts() -> ListPromptsResult: - """List available prompts.""" - return ListPromptsResult( - 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") +@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() - 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: 1. The overall structure and organization of the assembly @@ -355,22 +248,11 @@ Please help me understand: Focus area: {focus_area} Start by listing the types in the assembly, then provide insights based on what you find.""" - - return GetPromptResult( - description=f"Analysis of .NET assembly: {assembly_path}", - messages=[ - PromptMessage( - 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}". + +@mcp.prompt() +def decompile_and_explain_prompt(assembly_path: str, type_name: str) -> str: + """Prompt template for decompiling and explaining specific types""" + return f"""I want to understand the type "{type_name}" from the assembly "{assembly_path}". Please: 1. Decompile this specific type @@ -380,37 +262,7 @@ Please: Type to analyze: {type_name} 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__": - asyncio.run(main()) \ No newline at end of file + # FastMCP automatically handles running + mcp.run() \ No newline at end of file