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