init: init version
This commit is contained in:
commit
b6a09eabfe
31
.gitignore
vendored
Normal file
31
.gitignore
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Borealin
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
12
MANIFEST.in
Normal file
12
MANIFEST.in
Normal file
@ -0,0 +1,12 @@
|
||||
# Include important files in the source distribution
|
||||
include README.md
|
||||
include LICENSE
|
||||
include requirements.txt
|
||||
include mcp_config_example.json
|
||||
|
||||
# Exclude unnecessary files
|
||||
global-exclude *.pyc
|
||||
global-exclude __pycache__
|
||||
global-exclude .git*
|
||||
global-exclude .DS_Store
|
||||
prune docs
|
||||
241
README.md
Normal file
241
README.md
Normal file
@ -0,0 +1,241 @@
|
||||
# ILSpy MCP Server
|
||||
|
||||
A Model Context Protocol (MCP) server that provides .NET assembly decompilation capabilities using ILSpy.
|
||||
|
||||
## Features
|
||||
|
||||
- **Decompile Assemblies**: Convert .NET assemblies back to readable C# source code
|
||||
- **List Types**: Enumerate classes, interfaces, structs, delegates, and enums in assemblies
|
||||
- **Generate Diagrammer**: Create interactive HTML visualizations of assembly structure
|
||||
- **Assembly Information**: Get metadata about .NET assemblies
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. **ILSpy Command Line Tool**: Install the global dotnet tool:
|
||||
```bash
|
||||
dotnet tool install --global ilspycmd
|
||||
```
|
||||
|
||||
2. **Python 3.8+**: Required for running the MCP server
|
||||
|
||||
## Installation
|
||||
|
||||
Install from PyPI:
|
||||
|
||||
```bash
|
||||
pip install ilspy-mcp-server
|
||||
```
|
||||
|
||||
Or for development:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Borealin/ilspy-mcp-server.git
|
||||
cd ilspy-mcp-server
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### MCP Client Configuration
|
||||
|
||||
Configure your MCP client (e.g., Claude Desktop) to use the server:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ilspy_mcp_server.server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Available Tools
|
||||
|
||||
#### 1. `decompile_assembly`
|
||||
Decompile a .NET assembly to C# source code.
|
||||
|
||||
**Parameters:**
|
||||
- `assembly_path` (required): Path to the .NET assembly file
|
||||
- `output_dir` (optional): Output directory for decompiled files
|
||||
- `type_name` (optional): Specific type to decompile
|
||||
- `language_version` (optional): C# language version (default: "Latest")
|
||||
- `create_project` (optional): Create a compilable project structure
|
||||
- `show_il_code` (optional): Show IL code instead of C#
|
||||
- `remove_dead_code` (optional): Remove dead code during decompilation
|
||||
- `nested_directories` (optional): Use nested directories for namespaces
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "decompile_assembly",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"type_name": "MyNamespace.MyClass",
|
||||
"language_version": "CSharp10_0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. `list_types`
|
||||
List types in a .NET assembly.
|
||||
|
||||
**Parameters:**
|
||||
- `assembly_path` (required): Path to the .NET assembly file
|
||||
- `entity_types` (optional): Array of entity types to list ("c", "i", "s", "d", "e")
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "list_types",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"entity_types": ["c", "i"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. `generate_diagrammer`
|
||||
Generate an interactive HTML diagrammer.
|
||||
|
||||
**Parameters:**
|
||||
- `assembly_path` (required): Path to the .NET assembly file
|
||||
- `output_dir` (optional): Output directory for the diagrammer
|
||||
- `include_pattern` (optional): Regex pattern for types to include
|
||||
- `exclude_pattern` (optional): Regex pattern for types to exclude
|
||||
|
||||
#### 4. `get_assembly_info`
|
||||
Get basic information about an assembly.
|
||||
|
||||
**Parameters:**
|
||||
- `assembly_path` (required): Path to the .NET assembly file
|
||||
|
||||
### Available Prompts
|
||||
|
||||
#### 1. `analyze_assembly`
|
||||
Analyze a .NET assembly and provide insights about its structure.
|
||||
|
||||
#### 2. `decompile_and_explain`
|
||||
Decompile a specific type and provide explanation of its functionality.
|
||||
|
||||
## Supported Assembly Types
|
||||
|
||||
- .NET Framework assemblies (.dll, .exe)
|
||||
- .NET Core/.NET 5+ assemblies
|
||||
- Portable Executable (PE) files with .NET metadata
|
||||
|
||||
## Supported C# Language Versions
|
||||
|
||||
- CSharp1 through CSharp12_0
|
||||
- Preview
|
||||
- Latest (default)
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Install the package**:
|
||||
```bash
|
||||
pip install ilspy-mcp-server
|
||||
```
|
||||
|
||||
2. **Configure your MCP client** (Claude Desktop example):
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ilspy_mcp_server.server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Use the tools** in your MCP client:
|
||||
- Ask to decompile a .NET assembly
|
||||
- List types in an assembly
|
||||
- Generate interactive diagrams
|
||||
- Get assembly information
|
||||
|
||||
## Error Handling
|
||||
|
||||
The server provides detailed error messages for common issues:
|
||||
- Assembly file not found
|
||||
- Invalid assembly format
|
||||
- ILSpyCmd not installed or not in PATH
|
||||
- Permission issues
|
||||
- Decompilation failures
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `LOGLEVEL`: Set logging level (DEBUG, INFO, WARNING, ERROR). Default: INFO
|
||||
|
||||
### MCP Client Examples
|
||||
|
||||
**Claude Desktop** (`config.json`):
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ilspy_mcp_server.server"],
|
||||
"env": {
|
||||
"LOGLEVEL": "INFO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Development/Testing**:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ilspy_mcp_server.server"],
|
||||
"env": {
|
||||
"LOGLEVEL": "DEBUG"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **"ILSpyCmd not found"**:
|
||||
```bash
|
||||
dotnet tool install --global ilspycmd
|
||||
```
|
||||
|
||||
2. **"Assembly file not found"**:
|
||||
- Check the file path is correct
|
||||
- Ensure the file has .dll or .exe extension
|
||||
|
||||
3. **Permission errors**:
|
||||
- Ensure read access to assembly files
|
||||
- Check output directory permissions
|
||||
|
||||
### Debug Mode
|
||||
|
||||
Enable debug logging to see detailed operation info:
|
||||
```json
|
||||
{
|
||||
"env": {
|
||||
"LOGLEVEL": "DEBUG"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
- Built on top of the excellent [ILSpy](https://github.com/icsharpcode/ILSpy) decompiler
|
||||
- Uses the [Model Context Protocol](https://modelcontextprotocol.io/) for integration
|
||||
118
RELEASE.md
Normal file
118
RELEASE.md
Normal file
@ -0,0 +1,118 @@
|
||||
# Release Guide
|
||||
|
||||
This document describes how to release the package to PyPI.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Install build tools:
|
||||
```bash
|
||||
pip install build twine
|
||||
```
|
||||
|
||||
2. Make sure you have PyPI credentials configured:
|
||||
```bash
|
||||
# Create ~/.pypirc or use environment variables
|
||||
# TWINE_USERNAME and TWINE_PASSWORD
|
||||
```
|
||||
|
||||
## Release Process
|
||||
|
||||
### 1. Update Version
|
||||
|
||||
Update the version in `pyproject.toml`:
|
||||
```toml
|
||||
version = "0.1.1" # Increment as needed
|
||||
```
|
||||
|
||||
### 2. Build the Package
|
||||
|
||||
```bash
|
||||
# Clean previous builds
|
||||
rm -rf dist/ build/
|
||||
|
||||
# Build the package
|
||||
python -m build
|
||||
```
|
||||
|
||||
This creates:
|
||||
- `dist/ilspy_mcp_server-X.X.X.tar.gz` (source distribution)
|
||||
- `dist/ilspy_mcp_server-X.X.X-py3-none-any.whl` (wheel)
|
||||
|
||||
### 3. Test the Build
|
||||
|
||||
Test the package locally:
|
||||
```bash
|
||||
pip install dist/ilspy_mcp_server-*.whl
|
||||
```
|
||||
|
||||
### 4. Check the Package
|
||||
|
||||
```bash
|
||||
twine check dist/*
|
||||
```
|
||||
|
||||
### 5. Upload to Test PyPI (Optional)
|
||||
|
||||
```bash
|
||||
twine upload --repository testpypi dist/*
|
||||
```
|
||||
|
||||
Test installation from Test PyPI:
|
||||
```bash
|
||||
pip install --index-url https://test.pypi.org/simple/ ilspy-mcp-server
|
||||
```
|
||||
|
||||
### 6. Upload to PyPI
|
||||
|
||||
```bash
|
||||
twine upload dist/*
|
||||
```
|
||||
|
||||
### 7. Verify Installation
|
||||
|
||||
```bash
|
||||
pip install ilspy-mcp-server
|
||||
```
|
||||
|
||||
## Version Numbering
|
||||
|
||||
Follow [Semantic Versioning](https://semver.org/):
|
||||
- `MAJOR.MINOR.PATCH` (e.g., 1.0.0)
|
||||
- Increment MAJOR for breaking changes
|
||||
- Increment MINOR for new features
|
||||
- Increment PATCH for bug fixes
|
||||
|
||||
## Automation (Optional)
|
||||
|
||||
You can automate releases using GitHub Actions. Create `.github/workflows/release.yml`:
|
||||
|
||||
```yaml
|
||||
name: Release to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine
|
||||
- name: Build package
|
||||
run: python -m build
|
||||
- name: Publish to PyPI
|
||||
env:
|
||||
TWINE_USERNAME: __token__
|
||||
TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
|
||||
run: twine upload dist/*
|
||||
```
|
||||
|
||||
Then add your PyPI API token as a GitHub secret named `PYPI_API_TOKEN`.
|
||||
14
claude_desktop_config.json
Normal file
14
claude_desktop_config.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"-m",
|
||||
"ilspy_mcp_server.server"
|
||||
],
|
||||
"env": {
|
||||
"LOGLEVEL": "INFO"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
327
docs/API.md
Normal file
327
docs/API.md
Normal file
@ -0,0 +1,327 @@
|
||||
# API Documentation
|
||||
|
||||
This document provides detailed API documentation for the ILSpy MCP Server.
|
||||
|
||||
## Overview
|
||||
|
||||
The ILSpy MCP Server provides a Model Context Protocol (MCP) interface to the ILSpy .NET decompiler. It exposes four main tools and two prompts for interacting with .NET assemblies.
|
||||
|
||||
## Tools
|
||||
|
||||
### 1. decompile_assembly
|
||||
|
||||
Decompiles a .NET assembly to C# source code.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) |
|
||||
| `output_dir` | string | ✗ | null | Output directory for decompiled files |
|
||||
| `type_name` | string | ✗ | null | Fully qualified name of specific type to decompile |
|
||||
| `language_version` | string | ✗ | "Latest" | C# language version to use |
|
||||
| `create_project` | boolean | ✗ | false | Create a compilable project with multiple files |
|
||||
| `show_il_code` | boolean | ✗ | false | Show IL code instead of C# |
|
||||
| `remove_dead_code` | boolean | ✗ | false | Remove dead code during decompilation |
|
||||
| `nested_directories` | boolean | ✗ | false | Use nested directories for namespaces |
|
||||
|
||||
**Language Versions:**
|
||||
- `CSharp1` through `CSharp12_0`
|
||||
- `Preview` (latest preview features)
|
||||
- `Latest` (default, most recent stable)
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "decompile_assembly",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"type_name": "MyNamespace.MyClass",
|
||||
"language_version": "CSharp10_0",
|
||||
"remove_dead_code": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
Returns decompiled C# source code as text, or information about saved files if `output_dir` is specified.
|
||||
|
||||
### 2. list_types
|
||||
|
||||
Lists types (classes, interfaces, structs, etc.) in a .NET assembly.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) |
|
||||
| `entity_types` | array[string] | ✗ | ["c"] | Types of entities to list |
|
||||
|
||||
**Entity Types:**
|
||||
- `c` - Classes
|
||||
- `i` - Interfaces
|
||||
- `s` - Structs
|
||||
- `d` - Delegates
|
||||
- `e` - Enums
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "list_types",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"entity_types": ["c", "i", "s"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
Returns a formatted list of types organized by namespace, including:
|
||||
- Type name
|
||||
- Full qualified name
|
||||
- Type kind (Class, Interface, etc.)
|
||||
- Namespace
|
||||
|
||||
### 3. generate_diagrammer
|
||||
|
||||
Generates an interactive HTML diagrammer for visualizing assembly structure.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) |
|
||||
| `output_dir` | string | ✗ | null | Output directory for the diagrammer |
|
||||
| `include_pattern` | string | ✗ | null | Regex pattern for types to include |
|
||||
| `exclude_pattern` | string | ✗ | null | Regex pattern for types to exclude |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "generate_diagrammer",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"output_dir": "./diagrams",
|
||||
"include_pattern": "MyNamespace\\..+"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
Returns success status and output directory path. The HTML file can be opened in a web browser to view the interactive diagram.
|
||||
|
||||
### 4. get_assembly_info
|
||||
|
||||
Gets basic information about a .NET assembly.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| `assembly_path` | string | ✓ | - | Path to the .NET assembly file (.dll or .exe) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "get_assembly_info",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
Returns assembly metadata including:
|
||||
- Assembly name
|
||||
- Version
|
||||
- Full name
|
||||
- Location
|
||||
- Target framework (if available)
|
||||
- Runtime version (if available)
|
||||
- Whether the assembly is signed
|
||||
- Whether debug information is available
|
||||
|
||||
## Prompts
|
||||
|
||||
### 1. analyze_assembly
|
||||
|
||||
Provides a structured prompt for analyzing a .NET assembly and understanding its structure.
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `assembly_path` | string | ✓ | Path to the .NET assembly file |
|
||||
| `focus_area` | string | ✗ | Specific area to focus on (types, namespaces, dependencies) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "analyze_assembly",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"focus_area": "types"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. decompile_and_explain
|
||||
|
||||
Provides a structured prompt for decompiling a specific type and explaining its functionality.
|
||||
|
||||
**Arguments:**
|
||||
|
||||
| Argument | Type | Required | Description |
|
||||
|----------|------|----------|-------------|
|
||||
| `assembly_path` | string | ✓ | Path to the .NET assembly file |
|
||||
| `type_name` | string | ✓ | Fully qualified name of the type to analyze |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "decompile_and_explain",
|
||||
"arguments": {
|
||||
"assembly_path": "/path/to/MyAssembly.dll",
|
||||
"type_name": "MyNamespace.ImportantClass"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
The server provides detailed error messages for common issues:
|
||||
|
||||
### Validation Errors
|
||||
- Empty or invalid assembly paths
|
||||
- Non-existent files
|
||||
- Invalid file extensions (must be .dll or .exe)
|
||||
- Invalid reference paths
|
||||
|
||||
### Runtime Errors
|
||||
- ILSpyCmd not found or not installed
|
||||
- Permission issues accessing files
|
||||
- Decompilation failures
|
||||
- Invalid assembly format
|
||||
|
||||
### Error Response Format
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "Validation Error: Assembly file not found: /invalid/path.dll"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Data Models
|
||||
|
||||
### DecompileRequest
|
||||
```python
|
||||
class DecompileRequest(BaseModel):
|
||||
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
|
||||
remove_dead_code: bool = False
|
||||
nested_directories: bool = False
|
||||
# ... additional fields
|
||||
```
|
||||
|
||||
### TypeInfo
|
||||
```python
|
||||
class TypeInfo(BaseModel):
|
||||
name: str
|
||||
full_name: str
|
||||
kind: str
|
||||
namespace: Optional[str] = None
|
||||
```
|
||||
|
||||
### AssemblyInfo
|
||||
```python
|
||||
class AssemblyInfo(BaseModel):
|
||||
name: str
|
||||
version: str
|
||||
full_name: str
|
||||
location: str
|
||||
target_framework: Optional[str] = None
|
||||
runtime_version: Optional[str] = None
|
||||
is_signed: bool = False
|
||||
has_debug_info: bool = False
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Decompilation
|
||||
```python
|
||||
# Using the MCP client
|
||||
result = await session.call_tool(
|
||||
"decompile_assembly",
|
||||
{"assembly_path": "MyApp.dll"}
|
||||
)
|
||||
```
|
||||
|
||||
### Filtered Type Listing
|
||||
```python
|
||||
# List only classes and interfaces
|
||||
result = await session.call_tool(
|
||||
"list_types",
|
||||
{
|
||||
"assembly_path": "MyApp.dll",
|
||||
"entity_types": ["c", "i"]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Targeted Decompilation
|
||||
```python
|
||||
# Decompile specific type with optimizations
|
||||
result = await session.call_tool(
|
||||
"decompile_assembly",
|
||||
{
|
||||
"assembly_path": "MyApp.dll",
|
||||
"type_name": "MyApp.Core.Engine",
|
||||
"language_version": "CSharp11_0",
|
||||
"remove_dead_code": true
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `LOGLEVEL` | Logging level (DEBUG, INFO, WARNING, ERROR) | INFO |
|
||||
|
||||
### MCP Client Configuration
|
||||
|
||||
For Claude Desktop:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "ilspy-mcp-server"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
For development:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": ["-m", "ilspy_mcp_server.server"],
|
||||
"env": {
|
||||
"LOGLEVEL": "DEBUG"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
47
mcp_config_example.json
Normal file
47
mcp_config_example.json
Normal file
@ -0,0 +1,47 @@
|
||||
{
|
||||
"description": "Configuration example for Claude Desktop or other MCP clients",
|
||||
"mcpServers": {
|
||||
"ilspy": {
|
||||
"command": "python",
|
||||
"args": [
|
||||
"-m",
|
||||
"ilspy_mcp_server.server"
|
||||
],
|
||||
"env": {
|
||||
"LOGLEVEL": "INFO"
|
||||
}
|
||||
}
|
||||
},
|
||||
"instructions": [
|
||||
"1. Install the package: pip install ilspy-mcp-server",
|
||||
"2. Make sure ilspycmd is installed: dotnet tool install --global ilspycmd",
|
||||
"3. Add this configuration to your MCP client's config file",
|
||||
"4. Set LOGLEVEL to DEBUG for more verbose output if needed"
|
||||
],
|
||||
"example_usage": {
|
||||
"decompile_assembly": {
|
||||
"description": "Decompile a .NET assembly",
|
||||
"parameters": {
|
||||
"assembly_path": "C:\\path\\to\\your\\assembly.dll",
|
||||
"type_name": "MyNamespace.MyClass",
|
||||
"language_version": "Latest",
|
||||
"remove_dead_code": true
|
||||
}
|
||||
},
|
||||
"list_types": {
|
||||
"description": "List types in an assembly",
|
||||
"parameters": {
|
||||
"assembly_path": "C:\\path\\to\\your\\assembly.dll",
|
||||
"entity_types": ["c", "i", "s"]
|
||||
}
|
||||
},
|
||||
"generate_diagrammer": {
|
||||
"description": "Generate HTML diagrammer",
|
||||
"parameters": {
|
||||
"assembly_path": "C:\\path\\to\\your\\assembly.dll",
|
||||
"output_dir": "C:\\path\\to\\output",
|
||||
"include_pattern": "MyNamespace\\..+"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
publish.py
Normal file
63
publish.py
Normal file
@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple script to build and publish the package to PyPI.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def run_command(cmd: str) -> bool:
|
||||
"""Run a command and return success status."""
|
||||
print(f"🔧 Running: {cmd}")
|
||||
result = subprocess.run(cmd, shell=True)
|
||||
return result.returncode == 0
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to build and publish package."""
|
||||
|
||||
# Check if we're in the right directory
|
||||
if not Path("pyproject.toml").exists():
|
||||
print("❌ pyproject.toml not found. Run this script from the project root.")
|
||||
sys.exit(1)
|
||||
|
||||
print("📦 Building and publishing ilspy-mcp-server...")
|
||||
|
||||
# Clean previous builds
|
||||
print("\n1. Cleaning previous builds...")
|
||||
run_command("rm -rf dist/ build/")
|
||||
|
||||
# Build the package
|
||||
print("\n2. Building package...")
|
||||
if not run_command("python -m build"):
|
||||
print("❌ Build failed!")
|
||||
sys.exit(1)
|
||||
|
||||
# Check the package
|
||||
print("\n3. Checking package...")
|
||||
if not run_command("twine check dist/*"):
|
||||
print("❌ Package check failed!")
|
||||
sys.exit(1)
|
||||
|
||||
# Ask for confirmation
|
||||
print("\n4. Ready to upload to PyPI")
|
||||
response = input("Do you want to upload to PyPI? (y/N): ")
|
||||
|
||||
if response.lower() != 'y':
|
||||
print("Upload cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
# Upload to PyPI
|
||||
print("\n5. Uploading to PyPI...")
|
||||
if not run_command("twine upload dist/*"):
|
||||
print("❌ Upload failed!")
|
||||
sys.exit(1)
|
||||
|
||||
print("\n✅ Package successfully published to PyPI!")
|
||||
print("📦 Install with: pip install ilspy-mcp-server")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
51
pyproject.toml
Normal file
51
pyproject.toml
Normal file
@ -0,0 +1,51 @@
|
||||
[project]
|
||||
name = "ilspy-mcp-server"
|
||||
version = "0.1.0"
|
||||
description = "MCP Server for ILSpy .NET Decompiler"
|
||||
authors = [
|
||||
{name = "Borealin", email = "me@borealin.cn"}
|
||||
]
|
||||
dependencies = [
|
||||
"mcp>=0.1.0",
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
requires-python = ">=3.8"
|
||||
readme = "README.md"
|
||||
license = {text = "MIT"}
|
||||
keywords = ["mcp", "ilspy", "decompiler", "dotnet", "csharp", "reverse-engineering"]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
"Topic :: Software Development :: Debuggers",
|
||||
"Topic :: System :: Software Distribution",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
Homepage = "https://github.com/Borealin/ilspy-mcp-server"
|
||||
Repository = "https://github.com/Borealin/ilspy-mcp-server.git"
|
||||
Issues = "https://github.com/Borealin/ilspy-mcp-server/issues"
|
||||
|
||||
[project.scripts]
|
||||
ilspy-mcp-server = "ilspy_mcp_server.server:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["src/ilspy_mcp_server"]
|
||||
|
||||
[tool.hatch.build.targets.sdist]
|
||||
include = [
|
||||
"/src",
|
||||
"/README.md",
|
||||
"/LICENSE",
|
||||
]
|
||||
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
mcp>=0.1.0
|
||||
pydantic>=2.0.0
|
||||
3
src/ilspy_mcp_server/__init__.py
Normal file
3
src/ilspy_mcp_server/__init__.py
Normal file
@ -0,0 +1,3 @@
|
||||
"""ILSpy MCP Server - Model Context Protocol server for .NET decompilation."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
389
src/ilspy_mcp_server/ilspy_wrapper.py
Normal file
389
src/ilspy_mcp_server/ilspy_wrapper.py
Normal file
@ -0,0 +1,389 @@
|
||||
"""Wrapper for ICSharpCode.ILSpyCmd command line tool."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Tuple, Dict, Any
|
||||
import re
|
||||
import logging
|
||||
|
||||
from .models import (
|
||||
DecompileRequest, DecompileResponse,
|
||||
ListTypesRequest, ListTypesResponse, TypeInfo,
|
||||
GenerateDiagrammerRequest, AssemblyInfoRequest, AssemblyInfo,
|
||||
EntityType
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ILSpyWrapper:
|
||||
"""Wrapper class for ILSpy command line tool."""
|
||||
|
||||
def __init__(self, ilspycmd_path: Optional[str] = None):
|
||||
"""Initialize the wrapper.
|
||||
|
||||
Args:
|
||||
ilspycmd_path: Path to ilspycmd executable. If None, will try to find it in PATH.
|
||||
"""
|
||||
self.ilspycmd_path = ilspycmd_path or self._find_ilspycmd()
|
||||
if not self.ilspycmd_path:
|
||||
raise RuntimeError("ILSpyCmd not found. Please install it with: dotnet tool install --global ilspycmd")
|
||||
|
||||
def _find_ilspycmd(self) -> Optional[str]:
|
||||
"""Find ilspycmd executable in PATH."""
|
||||
# Try common names
|
||||
for cmd_name in ["ilspycmd", "ilspycmd.exe"]:
|
||||
path = shutil.which(cmd_name)
|
||||
if path:
|
||||
return path
|
||||
return None
|
||||
|
||||
async def _run_command(self, args: List[str], input_data: Optional[str] = None) -> Tuple[int, str, str]:
|
||||
"""Run ilspycmd with given arguments.
|
||||
|
||||
Args:
|
||||
args: Command line arguments
|
||||
input_data: Optional input data to pass to stdin
|
||||
|
||||
Returns:
|
||||
Tuple of (return_code, stdout, stderr)
|
||||
"""
|
||||
cmd = [self.ilspycmd_path] + args
|
||||
logger.debug(f"Running command: {' '.join(cmd)}")
|
||||
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
*cmd,
|
||||
stdin=asyncio.subprocess.PIPE if input_data else None,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
input_bytes = input_data.encode('utf-8') if input_data else None
|
||||
stdout_bytes, stderr_bytes = await process.communicate(input=input_bytes)
|
||||
|
||||
stdout = stdout_bytes.decode('utf-8', errors='replace') if stdout_bytes else ""
|
||||
stderr = stderr_bytes.decode('utf-8', errors='replace') if stderr_bytes else ""
|
||||
|
||||
return process.returncode, stdout, stderr
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error running command: {e}")
|
||||
return -1, "", str(e)
|
||||
|
||||
async def decompile(self, request: DecompileRequest) -> DecompileResponse:
|
||||
"""Decompile a .NET assembly.
|
||||
|
||||
Args:
|
||||
request: Decompilation request
|
||||
|
||||
Returns:
|
||||
Decompilation response
|
||||
"""
|
||||
if not os.path.exists(request.assembly_path):
|
||||
return DecompileResponse(
|
||||
success=False,
|
||||
error_message=f"Assembly file not found: {request.assembly_path}",
|
||||
assembly_name=os.path.basename(request.assembly_path)
|
||||
)
|
||||
|
||||
args = [request.assembly_path]
|
||||
|
||||
# Add language version
|
||||
args.extend(["-lv", request.language_version.value])
|
||||
|
||||
# Add type filter if specified
|
||||
if request.type_name:
|
||||
args.extend(["-t", request.type_name])
|
||||
|
||||
# Add output directory if specified
|
||||
temp_dir = None
|
||||
output_dir = request.output_dir
|
||||
if not output_dir:
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
output_dir = temp_dir
|
||||
|
||||
args.extend(["-o", output_dir])
|
||||
|
||||
# Add project creation flag
|
||||
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:
|
||||
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
|
||||
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:
|
||||
args.append("--nested-directories")
|
||||
|
||||
# Disable update check for automation
|
||||
args.append("--disable-updatecheck")
|
||||
|
||||
try:
|
||||
return_code, stdout, stderr = await self._run_command(args)
|
||||
|
||||
assembly_name = os.path.splitext(os.path.basename(request.assembly_path))[0]
|
||||
|
||||
if return_code == 0:
|
||||
# If no output directory was specified, return stdout as source code
|
||||
source_code = None
|
||||
output_path = None
|
||||
|
||||
if request.output_dir is None:
|
||||
source_code = stdout
|
||||
else:
|
||||
output_path = output_dir
|
||||
# Try to read the main generated file if it exists
|
||||
if request.type_name:
|
||||
# Single type decompilation
|
||||
type_file = os.path.join(output_dir, f"{request.type_name.split('.')[-1]}.cs")
|
||||
if os.path.exists(type_file):
|
||||
with open(type_file, 'r', encoding='utf-8') as f:
|
||||
source_code = f.read()
|
||||
elif not request.create_project:
|
||||
# Single file decompilation
|
||||
cs_file = os.path.join(output_dir, f"{assembly_name}.cs")
|
||||
if os.path.exists(cs_file):
|
||||
with open(cs_file, 'r', encoding='utf-8') as f:
|
||||
source_code = f.read()
|
||||
|
||||
return DecompileResponse(
|
||||
success=True,
|
||||
source_code=source_code,
|
||||
output_path=output_path,
|
||||
assembly_name=assembly_name,
|
||||
type_name=request.type_name
|
||||
)
|
||||
else:
|
||||
error_msg = stderr or stdout or "Unknown error occurred"
|
||||
return DecompileResponse(
|
||||
success=False,
|
||||
error_message=error_msg,
|
||||
assembly_name=assembly_name,
|
||||
type_name=request.type_name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return DecompileResponse(
|
||||
success=False,
|
||||
error_message=str(e),
|
||||
assembly_name=os.path.basename(request.assembly_path),
|
||||
type_name=request.type_name
|
||||
)
|
||||
finally:
|
||||
# Clean up temporary directory
|
||||
if temp_dir and os.path.exists(temp_dir):
|
||||
shutil.rmtree(temp_dir, ignore_errors=True)
|
||||
|
||||
async def list_types(self, request: ListTypesRequest) -> ListTypesResponse:
|
||||
"""List types in a .NET assembly.
|
||||
|
||||
Args:
|
||||
request: List types request
|
||||
|
||||
Returns:
|
||||
List types response
|
||||
"""
|
||||
if not os.path.exists(request.assembly_path):
|
||||
return ListTypesResponse(
|
||||
success=False,
|
||||
error_message=f"Assembly file not found: {request.assembly_path}"
|
||||
)
|
||||
|
||||
args = [request.assembly_path]
|
||||
|
||||
# Add entity types to list
|
||||
entity_types_str = "".join([et.value for et in request.entity_types])
|
||||
args.extend(["-l", entity_types_str])
|
||||
|
||||
# Add reference paths
|
||||
for ref_path in request.reference_paths:
|
||||
args.extend(["-r", ref_path])
|
||||
|
||||
# Disable update check
|
||||
args.append("--disable-updatecheck")
|
||||
|
||||
try:
|
||||
return_code, stdout, stderr = await self._run_command(args)
|
||||
|
||||
if return_code == 0:
|
||||
types = self._parse_types_output(stdout)
|
||||
return ListTypesResponse(
|
||||
success=True,
|
||||
types=types,
|
||||
total_count=len(types)
|
||||
)
|
||||
else:
|
||||
error_msg = stderr or stdout or "Unknown error occurred"
|
||||
return ListTypesResponse(
|
||||
success=False,
|
||||
error_message=error_msg
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return ListTypesResponse(
|
||||
success=False,
|
||||
error_message=str(e)
|
||||
)
|
||||
|
||||
def _parse_types_output(self, output: str) -> List[TypeInfo]:
|
||||
"""Parse the output from list types command.
|
||||
|
||||
Args:
|
||||
output: Raw output from ilspycmd
|
||||
|
||||
Returns:
|
||||
List of TypeInfo objects
|
||||
"""
|
||||
types = []
|
||||
lines = output.strip().split('\n')
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Parse the line format: "TypeKind: FullTypeName"
|
||||
match = re.match(r'^(\w+):\s*(.+)$', line)
|
||||
if match:
|
||||
kind = match.group(1)
|
||||
full_name = match.group(2)
|
||||
|
||||
# Extract namespace and name
|
||||
parts = full_name.split('.')
|
||||
if len(parts) > 1:
|
||||
namespace = '.'.join(parts[:-1])
|
||||
name = parts[-1]
|
||||
else:
|
||||
namespace = None
|
||||
name = full_name
|
||||
|
||||
types.append(TypeInfo(
|
||||
name=name,
|
||||
full_name=full_name,
|
||||
kind=kind,
|
||||
namespace=namespace
|
||||
))
|
||||
|
||||
return types
|
||||
|
||||
async def generate_diagrammer(self, request: GenerateDiagrammerRequest) -> Dict[str, Any]:
|
||||
"""Generate HTML diagrammer for an assembly.
|
||||
|
||||
Args:
|
||||
request: Generate diagrammer request
|
||||
|
||||
Returns:
|
||||
Dictionary with success status and details
|
||||
"""
|
||||
if not os.path.exists(request.assembly_path):
|
||||
return {
|
||||
"success": False,
|
||||
"error_message": f"Assembly file not found: {request.assembly_path}"
|
||||
}
|
||||
|
||||
args = [request.assembly_path, "--generate-diagrammer"]
|
||||
|
||||
# Add output directory
|
||||
output_dir = request.output_dir
|
||||
if not output_dir:
|
||||
# Generate next to assembly
|
||||
assembly_dir = os.path.dirname(request.assembly_path)
|
||||
output_dir = os.path.join(assembly_dir, "diagrammer")
|
||||
|
||||
args.extend(["-o", output_dir])
|
||||
|
||||
# Add include/exclude patterns
|
||||
if request.include_pattern:
|
||||
args.extend(["--generate-diagrammer-include", request.include_pattern])
|
||||
if request.exclude_pattern:
|
||||
args.extend(["--generate-diagrammer-exclude", request.exclude_pattern])
|
||||
|
||||
# Add documentation file
|
||||
if request.docs_path:
|
||||
args.extend(["--generate-diagrammer-docs", request.docs_path])
|
||||
|
||||
# Add namespace stripping
|
||||
if request.strip_namespaces:
|
||||
args.extend(["--generate-diagrammer-strip-namespaces"] + request.strip_namespaces)
|
||||
|
||||
# Add report excluded flag
|
||||
if request.report_excluded:
|
||||
args.append("--generate-diagrammer-report-excluded")
|
||||
|
||||
# Disable update check
|
||||
args.append("--disable-updatecheck")
|
||||
|
||||
try:
|
||||
return_code, stdout, stderr = await self._run_command(args)
|
||||
|
||||
if return_code == 0:
|
||||
return {
|
||||
"success": True,
|
||||
"output_directory": output_dir,
|
||||
"message": "HTML diagrammer generated successfully"
|
||||
}
|
||||
else:
|
||||
error_msg = stderr or stdout or "Unknown error occurred"
|
||||
return {
|
||||
"success": False,
|
||||
"error_message": error_msg
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error_message": str(e)
|
||||
}
|
||||
|
||||
async def get_assembly_info(self, request: AssemblyInfoRequest) -> AssemblyInfo:
|
||||
"""Get basic information about an assembly.
|
||||
|
||||
Args:
|
||||
request: Assembly info request
|
||||
|
||||
Returns:
|
||||
Assembly information
|
||||
"""
|
||||
if not os.path.exists(request.assembly_path):
|
||||
raise FileNotFoundError(f"Assembly file not found: {request.assembly_path}")
|
||||
|
||||
# For now, we'll extract basic info from the file path
|
||||
# In a more complete implementation, we could use reflection or metadata reading
|
||||
assembly_path = Path(request.assembly_path)
|
||||
|
||||
return AssemblyInfo(
|
||||
name=assembly_path.stem,
|
||||
version="Unknown",
|
||||
full_name=assembly_path.name,
|
||||
location=str(assembly_path.absolute()),
|
||||
target_framework=None,
|
||||
runtime_version=None,
|
||||
is_signed=False,
|
||||
has_debug_info=os.path.exists(assembly_path.with_suffix('.pdb'))
|
||||
)
|
||||
211
src/ilspy_mcp_server/models.py
Normal file
211
src/ilspy_mcp_server/models.py
Normal file
@ -0,0 +1,211 @@
|
||||
"""Data models for ILSpy MCP Server."""
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pydantic import BaseModel, Field, validator
|
||||
from enum import Enum
|
||||
import os
|
||||
|
||||
|
||||
class LanguageVersion(str, Enum):
|
||||
"""C# Language versions supported by ILSpy."""
|
||||
CSHARP1 = "CSharp1"
|
||||
CSHARP2 = "CSharp2"
|
||||
CSHARP3 = "CSharp3"
|
||||
CSHARP4 = "CSharp4"
|
||||
CSHARP5 = "CSharp5"
|
||||
CSHARP6 = "CSharp6"
|
||||
CSHARP7 = "CSharp7"
|
||||
CSHARP7_1 = "CSharp7_1"
|
||||
CSHARP7_2 = "CSharp7_2"
|
||||
CSHARP7_3 = "CSharp7_3"
|
||||
CSHARP8_0 = "CSharp8_0"
|
||||
CSHARP9_0 = "CSharp9_0"
|
||||
CSHARP10_0 = "CSharp10_0"
|
||||
CSHARP11_0 = "CSharp11_0"
|
||||
CSHARP12_0 = "CSharp12_0"
|
||||
PREVIEW = "Preview"
|
||||
LATEST = "Latest"
|
||||
|
||||
|
||||
class EntityType(str, Enum):
|
||||
"""Entity types that can be listed."""
|
||||
CLASS = "c"
|
||||
INTERFACE = "i"
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class TypeInfo(BaseModel):
|
||||
"""Information about a type in an assembly."""
|
||||
name: str
|
||||
full_name: str
|
||||
kind: str
|
||||
namespace: Optional[str] = None
|
||||
|
||||
|
||||
class DecompileResponse(BaseModel):
|
||||
"""Response from decompilation operation."""
|
||||
success: bool
|
||||
source_code: Optional[str] = None
|
||||
output_path: Optional[str] = None
|
||||
error_message: Optional[str] = None
|
||||
assembly_name: str
|
||||
type_name: Optional[str] = None
|
||||
|
||||
|
||||
class ListTypesResponse(BaseModel):
|
||||
"""Response from list types operation."""
|
||||
success: bool
|
||||
types: List[TypeInfo] = Field(default_factory=list)
|
||||
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
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
class AssemblyInfo(BaseModel):
|
||||
"""Information about an assembly."""
|
||||
name: str
|
||||
version: str
|
||||
full_name: str
|
||||
location: str
|
||||
target_framework: Optional[str] = None
|
||||
runtime_version: Optional[str] = None
|
||||
is_signed: bool = False
|
||||
has_debug_info: bool = False
|
||||
416
src/ilspy_mcp_server/server.py
Normal file
416
src/ilspy_mcp_server/server.py
Normal file
@ -0,0 +1,416 @@
|
||||
"""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 .ilspy_wrapper import ILSpyWrapper
|
||||
from .models import (
|
||||
DecompileRequest, ListTypesRequest, GenerateDiagrammerRequest,
|
||||
AssemblyInfoRequest, LanguageVersion, EntityType
|
||||
)
|
||||
|
||||
# Set up logging
|
||||
log_level = os.getenv('LOGLEVEL', 'INFO').upper()
|
||||
numeric_level = getattr(logging, log_level, logging.INFO)
|
||||
logging.basicConfig(
|
||||
level=numeric_level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create the MCP server
|
||||
server = Server("ilspy-mcp-server")
|
||||
|
||||
# Global ILSpy wrapper instance
|
||||
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."""
|
||||
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)}")]
|
||||
)
|
||||
|
||||
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}"
|
||||
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)]
|
||||
)
|
||||
|
||||
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:
|
||||
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.")]
|
||||
)
|
||||
|
||||
|
||||
@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")
|
||||
|
||||
prompt_text = f"""I need to analyze the .NET assembly at "{assembly_path}".
|
||||
|
||||
Please help me understand:
|
||||
1. The overall structure and organization of the assembly
|
||||
2. Key types and their relationships
|
||||
3. Main namespaces and their purposes
|
||||
4. Any notable patterns or architectural decisions
|
||||
|
||||
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}".
|
||||
|
||||
Please:
|
||||
1. Decompile this specific type
|
||||
2. Explain what this type does and its purpose
|
||||
3. Highlight any interesting patterns, design decisions, or potential issues
|
||||
4. Suggest how this type fits into the overall architecture
|
||||
|
||||
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())
|
||||
Loading…
x
Reference in New Issue
Block a user