init: init version

This commit is contained in:
Borealin 2025-08-03 23:31:39 +08:00
commit b6a09eabfe
15 changed files with 1946 additions and 0 deletions

31
.gitignore vendored Normal file
View 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
View 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
View 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
View 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
View 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`.

View File

@ -0,0 +1,14 @@
{
"mcpServers": {
"ilspy": {
"command": "python",
"args": [
"-m",
"ilspy_mcp_server.server"
],
"env": {
"LOGLEVEL": "INFO"
}
}
}
}

327
docs/API.md Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
mcp>=0.1.0
pydantic>=2.0.0

View File

@ -0,0 +1,3 @@
"""ILSpy MCP Server - Model Context Protocol server for .NET decompilation."""
__version__ = "0.1.0"

View 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'))
)

View 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

View 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())