add console and serial port mixins, expand to 94 tools

New features:
- ConsoleMixin: vm_screenshot, wait_for_vm_tools, get_vm_tools_status
- SerialPortMixin: setup_serial_port, get_serial_port, connect_serial_port,
  clear_serial_port, remove_serial_port

Enables:
- VM console screenshots via vSphere HTTP API
- VMware Tools status monitoring and wait utilities
- Network serial ports for headless VMs and network appliances

README rewritten with comprehensive documentation of all 94 tools.
This commit is contained in:
Ryan Malloy 2025-12-26 18:44:09 -07:00
parent d771db2e1d
commit 8e28b84e59
5 changed files with 802 additions and 147 deletions

437
README.md
View File

@ -1,188 +1,345 @@
# ESXi MCP Server
A VMware ESXi/vCenter management server based on MCP (Model Control Protocol), providing simple REST API interfaces for virtual machine management.
A comprehensive VMware vSphere management server implementing the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/), enabling AI assistants like Claude to manage virtual infrastructure through natural language.
## Why ESXi MCP Server?
Traditional VMware management requires navigating complex UIs or writing scripts. With ESXi MCP Server, you can simply ask:
> "Create a new VM with 4 CPUs and 8GB RAM, then take a snapshot before installing the OS"
And watch it happen. The server exposes **94 tools** covering every aspect of vSphere management.
## Features
- Support for ESXi and vCenter Server connections
- Real-time communication based on SSE (Server-Sent Events)
- RESTful API interface with JSON-RPC support
- API key authentication
- Complete virtual machine lifecycle management
- Real-time performance monitoring
- SSL/TLS secure connection support
- Flexible configuration options (YAML/JSON/Environment Variables)
## Core Functions
- Virtual Machine Management
- Create VM
- Clone VM
- Delete VM
- Power On/Off operations
- List all VMs
- Performance Monitoring
- CPU usage
- Memory usage
- Storage usage
- Network traffic statistics
## Requirements
- Python 3.7+
- pyVmomi
- PyYAML
- uvicorn
- mcp-core (Machine Control Protocol core library)
- **94 MCP Tools** - Complete vSphere management capabilities
- **6 MCP Resources** - Real-time access to VMs, hosts, datastores, networks, and clusters
- **Modular Architecture** - 13 specialized mixins organized by function
- **Full vCenter & ESXi Support** - Works with standalone hosts or vCenter Server
- **Guest Operations** - Execute commands, transfer files inside VMs via VMware Tools
- **Serial Console Access** - Network serial ports for headless VMs and network appliances
- **VM Screenshots** - Capture console screenshots for monitoring or documentation
## Quick Start
1. Install dependencies:
### Installation
```bash
pip install pyvmomi pyyaml uvicorn mcp-core
# Install with uv (recommended)
uvx esxi-mcp-server
# Or install with pip
pip install esxi-mcp-server
```
2. Create configuration file `config.yaml`:
### Configuration
```yaml
vcenter_host: "your-vcenter-ip"
vcenter_user: "administrator@vsphere.local"
vcenter_password: "your-password"
datacenter: "your-datacenter" # Optional
cluster: "your-cluster" # Optional
datastore: "your-datastore" # Optional
network: "VM Network" # Optional
insecure: true # Skip SSL certificate verification
api_key: "your-api-key" # API access key
log_file: "./logs/vmware_mcp.log" # Log file path
log_level: "INFO" # Log level
```
3. Run the server:
Create a `.env` file or set environment variables:
```bash
python server.py -c config.yaml
VCENTER_HOST=vcenter.example.com
VCENTER_USER=administrator@vsphere.local
VCENTER_PASSWORD=your-password
VCENTER_INSECURE=true # Skip SSL verification (dev only)
```
## API Interface
### Run the Server
### Authentication
```bash
# Using uvx
uvx esxi-mcp-server
All privileged operations require authentication first:
```http
POST /sse/messages
Authorization: Bearer your-api-key
# Or if installed
esxi-mcp-server
```
### Main Tool Interfaces
### Add to Claude Code
1. Create VM
```json
{
"name": "vm-name",
"cpu": 2,
"memory": 4096,
"datastore": "datastore-name",
"network": "network-name"
}
```bash
claude mcp add esxi "uvx esxi-mcp-server"
```
2. Clone VM
```json
{
"template_name": "source-vm",
"new_name": "new-vm-name"
}
## Available Tools (94 Total)
### VM Lifecycle (6 tools)
| Tool | Description |
|------|-------------|
| `list_vms` | List all virtual machines |
| `get_vm_info` | Get detailed VM information |
| `create_vm` | Create a new virtual machine |
| `clone_vm` | Clone from template or existing VM |
| `delete_vm` | Delete a virtual machine |
| `reconfigure_vm` | Modify CPU, memory, annotation |
| `rename_vm` | Rename a virtual machine |
### Power Operations (6 tools)
| Tool | Description |
|------|-------------|
| `power_on_vm` | Power on a VM |
| `power_off_vm` | Power off a VM (hard) |
| `shutdown_guest` | Graceful guest OS shutdown |
| `reboot_guest` | Graceful guest OS reboot |
| `suspend_vm` | Suspend a VM |
| `reset_vm` | Hard reset a VM |
### Snapshots (5 tools)
| Tool | Description |
|------|-------------|
| `list_snapshots` | List all snapshots |
| `create_snapshot` | Create a new snapshot |
| `revert_to_snapshot` | Revert to a snapshot |
| `delete_snapshot` | Delete a snapshot |
| `delete_all_snapshots` | Remove all snapshots |
### Guest Operations (7 tools)
*Requires VMware Tools running in the guest*
| Tool | Description |
|------|-------------|
| `list_guest_processes` | List processes in guest OS |
| `run_command_in_guest` | Execute command in guest |
| `read_guest_file` | Read file from guest OS |
| `write_guest_file` | Write file to guest OS |
| `list_guest_directory` | List directory contents |
| `create_guest_directory` | Create directory in guest |
| `delete_guest_file` | Delete file or directory |
### Console & Monitoring (5 tools)
| Tool | Description |
|------|-------------|
| `vm_screenshot` | Capture VM console screenshot |
| `wait_for_vm_tools` | Wait for VMware Tools to be ready |
| `get_vm_tools_status` | Get VMware Tools status |
| `get_vm_stats` | Get VM performance statistics |
| `get_host_stats` | Get host performance statistics |
### Serial Port Management (5 tools)
*For network appliances and headless VMs*
| Tool | Description |
|------|-------------|
| `get_serial_port` | Get serial port configuration |
| `setup_serial_port` | Configure network serial port |
| `connect_serial_port` | Connect/disconnect serial port |
| `clear_serial_port` | Reset serial port connection |
| `remove_serial_port` | Remove serial port from VM |
### Disk Management (5 tools)
| Tool | Description |
|------|-------------|
| `list_disks` | List VM disks |
| `add_disk` | Add a new disk |
| `remove_disk` | Remove a disk |
| `resize_disk` | Expand disk size |
| `get_disk_info` | Get disk details |
### NIC Management (6 tools)
| Tool | Description |
|------|-------------|
| `list_nics` | List VM network adapters |
| `add_nic` | Add a network adapter |
| `remove_nic` | Remove a network adapter |
| `connect_nic` | Connect/disconnect NIC |
| `change_nic_network` | Change NIC network |
| `get_nic_info` | Get NIC details |
### OVF/OVA Management (5 tools)
| Tool | Description |
|------|-------------|
| `deploy_ovf` | Deploy VM from OVF template |
| `export_ovf` | Export VM to OVF |
| `list_ovf_networks` | List OVF network mappings |
| `upload_to_datastore` | Upload file to datastore |
| `download_from_datastore` | Download file from datastore |
### Host Management (10 tools)
| Tool | Description |
|------|-------------|
| `list_hosts` | List ESXi hosts |
| `get_host_info` | Get host details |
| `get_host_hardware` | Get hardware information |
| `get_host_networking` | Get network configuration |
| `list_services` | List host services |
| `get_service_status` | Get service status |
| `start_service` | Start a host service |
| `stop_service` | Stop a host service |
| `restart_service` | Restart a host service |
| `get_ntp_config` | Get NTP configuration |
### Datastore & Resources (8 tools)
| Tool | Description |
|------|-------------|
| `get_datastore_info` | Get datastore details |
| `browse_datastore` | Browse datastore files |
| `get_vcenter_info` | Get vCenter information |
| `get_resource_pool_info` | Get resource pool details |
| `get_network_info` | Get network details |
| `list_templates` | List VM templates |
| `get_alarms` | Get active alarms |
| `get_recent_events` | Get recent events |
### vCenter Operations (18 tools)
*Available when connected to vCenter Server*
| Tool | Description |
|------|-------------|
| `list_folders` | List VM folders |
| `create_folder` | Create a folder |
| `delete_folder` | Delete a folder |
| `move_vm_to_folder` | Move VM to folder |
| `list_clusters` | List clusters |
| `get_cluster_info` | Get cluster details |
| `list_resource_pools` | List resource pools |
| `create_resource_pool` | Create resource pool |
| `delete_resource_pool` | Delete resource pool |
| `move_vm_to_resource_pool` | Move VM to resource pool |
| `list_tags` | List tags |
| `get_vm_tags` | Get tags on a VM |
| `apply_tag_to_vm` | Apply tag to VM |
| `remove_tag_from_vm` | Remove tag from VM |
| `migrate_vm` | Migrate VM to another host |
| `list_recent_tasks` | List recent tasks |
| `list_recent_events` | List recent events |
| `cancel_task` | Cancel a running task |
## MCP Resources
Access real-time data through MCP resources:
| Resource URI | Description |
|--------------|-------------|
| `esxi://vms` | All virtual machines |
| `esxi://hosts` | All ESXi hosts |
| `esxi://datastores` | All datastores |
| `esxi://networks` | All networks |
| `esxi://clusters` | All clusters |
| `esxi://resource-pools` | All resource pools |
## Architecture
The server uses a modular mixin architecture:
```
esxi_mcp_server/
├── server.py # FastMCP server setup
├── connection.py # VMware connection management
├── config.py # Settings and configuration
└── mixins/
├── vm_lifecycle.py # VM CRUD operations
├── power_ops.py # Power management
├── snapshots.py # Snapshot management
├── guest_ops.py # Guest OS operations
├── console.py # Screenshots & Tools monitoring
├── serial_port.py # Serial console access
├── disk_management.py # Disk operations
├── nic_management.py # Network adapter operations
├── ovf_management.py # OVF/OVA handling
├── host_management.py # Host operations
├── monitoring.py # Performance monitoring
├── resources.py # MCP resources
└── vcenter_ops.py # vCenter-specific operations
```
3. Delete VM
```json
{
"name": "vm-name"
}
## Configuration Options
| Variable | Description | Default |
|----------|-------------|---------|
| `VCENTER_HOST` | vCenter/ESXi hostname or IP | *required* |
| `VCENTER_USER` | Username | *required* |
| `VCENTER_PASSWORD` | Password | *required* |
| `VCENTER_INSECURE` | Skip SSL verification | `false` |
| `VCENTER_DATACENTER` | Target datacenter | *auto-detect* |
| `VCENTER_CLUSTER` | Target cluster | *auto-detect* |
| `VCENTER_DATASTORE` | Default datastore | *auto-detect* |
| `VCENTER_NETWORK` | Default network | *auto-detect* |
| `MCP_TRANSPORT` | Transport mode (`stdio` or `sse`) | `stdio` |
| `LOG_LEVEL` | Logging level | `INFO` |
## Docker Support
```bash
# Build
docker build -t esxi-mcp-server .
# Run
docker run -d \
-e VCENTER_HOST=vcenter.example.com \
-e VCENTER_USER=admin@vsphere.local \
-e VCENTER_PASSWORD=secret \
esxi-mcp-server
```
4. Power Operations
```json
{
"name": "vm-name"
}
## Examples
### Create a VM and Install an OS
```
User: Create a new VM called "web-server" with 4 CPUs, 8GB RAM, and a 100GB disk
Claude: I'll create that VM for you.
[Calls create_vm with name="web-server", cpu=4, memory_mb=8192, disk_gb=100]
VM 'web-server' created successfully.
User: Power it on and take a screenshot
Claude: [Calls power_on_vm, then vm_screenshot]
The VM is now running. Here's the console screenshot showing the BIOS boot screen.
```
### Resource Monitoring Interface
### Guest Operations
Get VM performance data:
```http
GET vmstats://{vm_name}
```
User: Run "uname -a" on the linux-server VM
Claude: [Calls run_command_in_guest with command="/usr/bin/uname", arguments="-a"]
The command returned:
Linux linux-server 5.15.0-generic #1 SMP x86_64 GNU/Linux
```
## Configuration
### Serial Console for Network Appliances
| Parameter | Description | Required | Default |
|-----------|-------------|----------|---------|
| vcenter_host | vCenter/ESXi server address | Yes | - |
| vcenter_user | Login username | Yes | - |
| vcenter_password | Login password | Yes | - |
| datacenter | Datacenter name | No | Auto-select first |
| cluster | Cluster name | No | Auto-select first |
| datastore | Storage name | No | Auto-select largest available |
| network | Network name | No | VM Network |
| insecure | Skip SSL verification | No | false |
| api_key | API access key | No | - |
| log_file | Log file path | No | Console output |
| log_level | Log level | No | INFO |
```
User: Set up a serial console on my Cisco router VM
## Environment Variables
Claude: [Calls setup_serial_port with name="cisco-router", protocol="telnet"]
All configuration items support environment variable settings, following these naming rules:
- VCENTER_HOST
- VCENTER_USER
- VCENTER_PASSWORD
- VCENTER_DATACENTER
- VCENTER_CLUSTER
- VCENTER_DATASTORE
- VCENTER_NETWORK
- VCENTER_INSECURE
- MCP_API_KEY
- MCP_LOG_FILE
- MCP_LOG_LEVEL
Serial port configured. You can connect via:
telnet://10.20.0.22:4521
```
## Security Recommendations
## Requirements
1. Production Environment:
- Use valid SSL certificates
- Enable API key authentication
- Set appropriate log levels
- Restrict API access scope
- Python 3.11+
- VMware vSphere 7.0+ (ESXi or vCenter)
- VMware Tools (for guest operations)
2. Testing Environment:
- Set insecure: true to skip SSL verification
- Use more detailed log level (DEBUG)
## Development
```bash
# Clone the repo
git clone https://github.com/yourusername/esxi-mcp-server.git
cd esxi-mcp-server
# Install dependencies
uv sync
# Run tests
uv run python test_client.py
```
## License
MIT License
MIT License - See [LICENSE](LICENSE) for details.
## Contributing
Issues and Pull Requests are welcome!
## Changelog
### v0.0.1
- Initial release
- Basic VM management functionality
- SSE communication support
- API key authentication
- Performance monitoring
## Author
Bright8192
Contributions welcome! Please read the contributing guidelines and submit a PR.
## Acknowledgments
- VMware pyvmomi team
- MCP Protocol development team
- Built with [FastMCP](https://github.com/jlowin/fastmcp)
- Uses [pyVmomi](https://github.com/vmware/pyvmomi) for vSphere API
- Inspired by the Model Context Protocol specification

View File

@ -1,5 +1,6 @@
"""MCP Mixins for ESXi operations organized by category."""
from esxi_mcp_server.mixins.console import ConsoleMixin
from esxi_mcp_server.mixins.disk_management import DiskManagementMixin
from esxi_mcp_server.mixins.guest_ops import GuestOpsMixin
from esxi_mcp_server.mixins.host_management import HostManagementMixin
@ -8,20 +9,23 @@ from esxi_mcp_server.mixins.nic_management import NICManagementMixin
from esxi_mcp_server.mixins.ovf_management import OVFManagementMixin
from esxi_mcp_server.mixins.power_ops import PowerOpsMixin
from esxi_mcp_server.mixins.resources import ResourcesMixin
from esxi_mcp_server.mixins.serial_port import SerialPortMixin
from esxi_mcp_server.mixins.snapshots import SnapshotsMixin
from esxi_mcp_server.mixins.vcenter_ops import VCenterOpsMixin
from esxi_mcp_server.mixins.vm_lifecycle import VMLifecycleMixin
__all__ = [
"VMLifecycleMixin",
"PowerOpsMixin",
"SnapshotsMixin",
"MonitoringMixin",
"GuestOpsMixin",
"ResourcesMixin",
"ConsoleMixin",
"DiskManagementMixin",
"GuestOpsMixin",
"HostManagementMixin",
"MonitoringMixin",
"NICManagementMixin",
"OVFManagementMixin",
"HostManagementMixin",
"PowerOpsMixin",
"ResourcesMixin",
"SerialPortMixin",
"SnapshotsMixin",
"VCenterOpsMixin",
"VMLifecycleMixin",
]

View File

@ -0,0 +1,178 @@
"""VM Console operations - screenshots and tools monitoring."""
import base64
import time
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, Any
import requests
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from mcp.types import ToolAnnotations
from pyVmomi import vim
if TYPE_CHECKING:
from esxi_mcp_server.connection import VMwareConnection
class ConsoleMixin(MCPMixin):
"""VM console operations - screenshots and VMware Tools monitoring."""
def __init__(self, conn: "VMwareConnection"):
self.conn = conn
@mcp_tool(
name="wait_for_vm_tools",
description="Wait for VMware Tools to become available on a VM. Useful after powering on a VM.",
annotations=ToolAnnotations(readOnlyHint=True),
)
def wait_for_vm_tools(
self, name: str, timeout: int = 120, poll_interval: int = 5
) -> dict[str, Any]:
"""Wait for VMware Tools to become available.
Args:
name: VM name
timeout: Maximum seconds to wait (default: 120)
poll_interval: Seconds between status checks (default: 5)
Returns:
Dict with tools status, version, and guest info when ready
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
start_time = datetime.now()
end_time = start_time + timedelta(seconds=timeout)
while datetime.now() < end_time:
tools_status = vm.guest.toolsStatus if vm.guest else None
if tools_status == vim.vm.GuestInfo.ToolsStatus.toolsOk:
return {
"status": "ready",
"tools_status": str(tools_status),
"tools_version": vm.guest.toolsVersion if vm.guest else None,
"tools_running_status": (
vm.guest.toolsRunningStatus if vm.guest else None
),
"ip_address": vm.guest.ipAddress if vm.guest else None,
"hostname": vm.guest.hostName if vm.guest else None,
"guest_os": vm.guest.guestFullName if vm.guest else None,
"wait_time_seconds": (datetime.now() - start_time).total_seconds(),
}
time.sleep(poll_interval)
# Timeout reached
return {
"status": "timeout",
"tools_status": str(vm.guest.toolsStatus) if vm.guest else None,
"message": f"VMware Tools not ready after {timeout} seconds",
"wait_time_seconds": timeout,
}
@mcp_tool(
name="get_vm_tools_status",
description="Get current VMware Tools status for a VM",
annotations=ToolAnnotations(readOnlyHint=True),
)
def get_vm_tools_status(self, name: str) -> dict[str, Any]:
"""Get VMware Tools status without waiting.
Args:
name: VM name
Returns:
Dict with current tools status and guest info
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
return {
"tools_status": str(vm.guest.toolsStatus) if vm.guest else None,
"tools_version": vm.guest.toolsVersion if vm.guest else None,
"tools_running_status": (
vm.guest.toolsRunningStatus if vm.guest else None
),
"tools_version_status": (
str(vm.guest.toolsVersionStatus) if vm.guest else None
),
"ip_address": vm.guest.ipAddress if vm.guest else None,
"hostname": vm.guest.hostName if vm.guest else None,
"guest_os": vm.guest.guestFullName if vm.guest else None,
"guest_id": vm.guest.guestId if vm.guest else None,
"guest_state": vm.guest.guestState if vm.guest else None,
}
@mcp_tool(
name="vm_screenshot",
description="Capture a screenshot of the VM console. Returns base64-encoded PNG image.",
annotations=ToolAnnotations(readOnlyHint=True),
)
def vm_screenshot(
self,
name: str,
width: int | None = None,
height: int | None = None,
) -> dict[str, Any]:
"""Capture VM console screenshot via vSphere HTTP API.
Args:
name: VM name
width: Optional width to scale the image
height: Optional height to scale the image
Returns:
Dict with base64-encoded image data and metadata
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
# Build screenshot URL
# Format: https://{host}/screen?id={moid}
host = self.conn.settings.vcenter_host
moid = vm._moId
screenshot_url = f"https://{host}/screen?id={moid}"
# Add optional scaling parameters
params = []
if width:
params.append(f"w={width}")
if height:
params.append(f"h={height}")
if params:
screenshot_url += "&" + "&".join(params)
# Build auth header
username = self.conn.settings.vcenter_user
password = self.conn.settings.vcenter_password.get_secret_value()
auth = base64.b64encode(f"{username}:{password}".encode()).decode("ascii")
# Make request
try:
response = requests.get(
screenshot_url,
headers={"Authorization": f"Basic {auth}"},
verify=not self.conn.settings.vcenter_insecure,
timeout=30,
)
response.raise_for_status()
except requests.RequestException as e:
raise ValueError(f"Failed to capture screenshot: {e}") from e
# Encode image as base64
image_data = base64.b64encode(response.content).decode("ascii")
content_type = response.headers.get("Content-Type", "image/png")
return {
"vm_name": name,
"moid": moid,
"content_type": content_type,
"size_bytes": len(response.content),
"image_base64": image_data,
"width": width,
"height": height,
}

View File

@ -0,0 +1,312 @@
"""Serial Port Management - network console access for VMs."""
import random
import socket
import time
from typing import TYPE_CHECKING, Any
from fastmcp.contrib.mcp_mixin import MCPMixin, mcp_tool
from mcp.types import ToolAnnotations
from pyVmomi import vim
if TYPE_CHECKING:
from esxi_mcp_server.connection import VMwareConnection
class SerialPortMixin(MCPMixin):
"""Serial port management for VM network console access.
Network serial ports allow telnet/TCP connections to VM consoles,
useful for headless VMs, network appliances, or serial console access.
Supported protocols:
- telnet: Telnet over TCP (can negotiate SSL)
- telnets: Telnet over SSL over TCP
- tcp: Unencrypted TCP
- tcp+ssl: Encrypted SSL over TCP
"""
def __init__(self, conn: "VMwareConnection"):
self.conn = conn
def _get_serial_port(self, vm: vim.VirtualMachine) -> vim.vm.device.VirtualSerialPort | None:
"""Find existing serial port with URI backing on a VM."""
if not vm.config:
return None
for device in vm.config.hardware.device:
if (
isinstance(device, vim.vm.device.VirtualSerialPort)
and isinstance(device.backing, vim.vm.device.VirtualSerialPort.URIBackingInfo)
):
return device
return None
def _find_unused_port(self, host_ip: str, start: int = 2000, end: int = 9000) -> int:
"""Find an unused TCP port on the ESXi host."""
# Try random ports in range until we find one that's available
attempts = 0
max_attempts = 50
while attempts < max_attempts:
port = random.randint(start, end)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(1)
try:
result = sock.connect_ex((host_ip, port))
if result != 0: # Port not in use
return port
except (OSError, TimeoutError):
return port # Likely available
finally:
sock.close()
attempts += 1
raise ValueError(f"Could not find unused port in range {start}-{end}")
@mcp_tool(
name="get_serial_port",
description="Get current serial port configuration for a VM",
annotations=ToolAnnotations(readOnlyHint=True),
)
def get_serial_port(self, name: str) -> dict[str, Any]:
"""Get serial port configuration.
Args:
name: VM name
Returns:
Dict with serial port details or message if not configured
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
serial_port = self._get_serial_port(vm)
if not serial_port:
return {
"configured": False,
"message": "No network serial port configured",
}
backing = serial_port.backing
return {
"configured": True,
"label": serial_port.deviceInfo.label,
"connected": serial_port.connectable.connected if serial_port.connectable else None,
"start_connected": serial_port.connectable.startConnected if serial_port.connectable else None,
"direction": backing.direction if backing else None,
"service_uri": backing.serviceURI if backing else None,
"yield_on_poll": serial_port.yieldOnPoll,
}
@mcp_tool(
name="setup_serial_port",
description="Configure a network serial port on a VM for console access. VM must be powered off.",
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True),
)
def setup_serial_port(
self,
name: str,
protocol: str = "telnet",
port: int | None = None,
direction: str = "server",
yield_on_poll: bool = True,
) -> dict[str, Any]:
"""Setup or update network serial port.
Args:
name: VM name
protocol: Protocol to use (telnet, telnets, tcp, tcp+ssl). Default: telnet
port: TCP port number. If not specified, auto-assigns unused port.
direction: 'server' (VM listens) or 'client' (VM connects). Default: server
yield_on_poll: Enable CPU yield behavior. Default: True
Returns:
Dict with configured serial port URI and details
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
# Check VM is powered off
if vm.runtime.powerState != vim.VirtualMachine.PowerState.poweredOff:
raise ValueError(f"VM '{name}' must be powered off to configure serial port")
# Validate protocol
valid_protocols = ["telnet", "telnets", "tcp", "tcp+ssl", "tcp4", "tcp6"]
if protocol not in valid_protocols:
raise ValueError(f"Invalid protocol '{protocol}'. Must be one of: {valid_protocols}")
# Validate direction
if direction not in ["server", "client"]:
raise ValueError("Direction must be 'server' or 'client'")
# Find or assign port
if port is None:
host = vm.runtime.host
host_ip = host.name if host else self.conn.settings.vcenter_host
port = self._find_unused_port(host_ip)
# Build service URI
service_uri = f"{protocol}://:{port}"
# Build spec
serial_spec = vim.vm.device.VirtualDeviceSpec()
existing_port = self._get_serial_port(vm)
if existing_port:
# Edit existing
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
serial_spec.device = existing_port
else:
# Add new
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.add
serial_spec.device = vim.vm.device.VirtualSerialPort()
# Configure backing
serial_spec.device.yieldOnPoll = yield_on_poll
serial_spec.device.backing = vim.vm.device.VirtualSerialPort.URIBackingInfo()
serial_spec.device.backing.direction = direction
serial_spec.device.backing.serviceURI = service_uri
# Configure connectable
serial_spec.device.connectable = vim.vm.device.VirtualDevice.ConnectInfo()
serial_spec.device.connectable.startConnected = True
serial_spec.device.connectable.allowGuestControl = True
serial_spec.device.connectable.connected = False # Will connect on power on
# Apply config
spec = vim.vm.ConfigSpec()
spec.deviceChange = [serial_spec]
task = vm.ReconfigVM_Task(spec=spec)
self.conn.wait_for_task(task)
# Get ESXi host info for connection string
host = vm.runtime.host
host_ip = host.name if host else self.conn.settings.vcenter_host
return {
"vm_name": name,
"service_uri": service_uri,
"connection_string": f"{protocol}://{host_ip}:{port}",
"protocol": protocol,
"port": port,
"direction": direction,
"yield_on_poll": yield_on_poll,
"operation": "updated" if existing_port else "created",
}
@mcp_tool(
name="connect_serial_port",
description="Connect or disconnect an existing serial port on a VM",
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True),
)
def connect_serial_port(self, name: str, connected: bool = True) -> dict[str, Any]:
"""Connect or disconnect serial port.
Args:
name: VM name
connected: True to connect, False to disconnect. Default: True
Returns:
Dict with result
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
serial_port = self._get_serial_port(vm)
if not serial_port:
raise ValueError(f"No network serial port configured on VM '{name}'")
# Build edit spec
serial_spec = vim.vm.device.VirtualDeviceSpec()
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.edit
serial_spec.device = serial_port
serial_spec.device.connectable.connected = connected
spec = vim.vm.ConfigSpec()
spec.deviceChange = [serial_spec]
task = vm.ReconfigVM_Task(spec=spec)
self.conn.wait_for_task(task)
return {
"vm_name": name,
"connected": connected,
"service_uri": serial_port.backing.serviceURI if serial_port.backing else None,
}
@mcp_tool(
name="clear_serial_port",
description="Reset serial port by disconnecting and reconnecting (clears stuck connections)",
annotations=ToolAnnotations(destructiveHint=False, idempotentHint=True),
)
def clear_serial_port(self, name: str) -> dict[str, Any]:
"""Clear serial port by cycling connection state.
Useful for clearing stuck or stale connections.
Args:
name: VM name
Returns:
Dict with result
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
serial_port = self._get_serial_port(vm)
if not serial_port:
raise ValueError(f"No network serial port configured on VM '{name}'")
# Disconnect
self.connect_serial_port(name, connected=False)
time.sleep(1)
# Reconnect
self.connect_serial_port(name, connected=True)
return {
"vm_name": name,
"status": "cleared",
"service_uri": serial_port.backing.serviceURI if serial_port.backing else None,
"message": "Serial port disconnected and reconnected",
}
@mcp_tool(
name="remove_serial_port",
description="Remove the network serial port from a VM. VM must be powered off.",
annotations=ToolAnnotations(destructiveHint=True, idempotentHint=True),
)
def remove_serial_port(self, name: str) -> str:
"""Remove serial port from VM.
Args:
name: VM name
Returns:
Success message
"""
vm = self.conn.find_vm(name)
if not vm:
raise ValueError(f"VM '{name}' not found")
# Check VM is powered off
if vm.runtime.powerState != vim.VirtualMachine.PowerState.poweredOff:
raise ValueError(f"VM '{name}' must be powered off to remove serial port")
serial_port = self._get_serial_port(vm)
if not serial_port:
return f"No network serial port configured on VM '{name}'"
# Build remove spec
serial_spec = vim.vm.device.VirtualDeviceSpec()
serial_spec.operation = vim.vm.device.VirtualDeviceSpec.Operation.remove
serial_spec.device = serial_port
spec = vim.vm.ConfigSpec()
spec.deviceChange = [serial_spec]
task = vm.ReconfigVM_Task(spec=spec)
self.conn.wait_for_task(task)
return f"Serial port removed from VM '{name}'"

View File

@ -9,6 +9,7 @@ from fastmcp import FastMCP
from esxi_mcp_server.config import Settings, get_settings
from esxi_mcp_server.connection import VMwareConnection
from esxi_mcp_server.mixins import (
ConsoleMixin,
DiskManagementMixin,
GuestOpsMixin,
HostManagementMixin,
@ -17,6 +18,7 @@ from esxi_mcp_server.mixins import (
OVFManagementMixin,
PowerOpsMixin,
ResourcesMixin,
SerialPortMixin,
SnapshotsMixin,
VCenterOpsMixin,
VMLifecycleMixin,
@ -78,6 +80,8 @@ def create_server(settings: Settings | None = None) -> FastMCP:
OVFManagementMixin(conn),
HostManagementMixin(conn),
VCenterOpsMixin(conn),
ConsoleMixin(conn),
SerialPortMixin(conn),
]
tool_count = 0