Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 54 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,25 @@
[![License](https://img.shields.io/badge/license-MIT-purple)](LICENSE)
[![PyPI Version](https://img.shields.io/pypi/v/truenas-mcp-server)](https://pypi.org/project/truenas-mcp-server/)

A production-ready Model Context Protocol (MCP) server for TrueNAS Core systems. Control and manage your TrueNAS storage through natural language with Claude or other MCP-compatible clients.
A production-ready Model Context Protocol (MCP) server for **TrueNAS Core and SCALE** systems. Control and manage your TrueNAS storage and virtualization through natural language with Claude or other MCP-compatible clients.

**Automatic variant detection**: The server automatically detects whether you're connected to TrueNAS Core or SCALE and enables the appropriate features.

## 🚀 Features

### Core Capabilities
### Universal Features (Core & SCALE)
- **User Management** - Create, update, delete users and manage permissions
- **Storage Management** - Manage pools, datasets, volumes with full ZFS support
- **Storage Management** - Manage pools, datasets, volumes with full ZFS support
- **File Sharing** - Configure SMB, NFS, and iSCSI shares
- **Snapshot Management** - Create, delete, rollback snapshots with automation
- **System Monitoring** - Check system health, pool status, and resource usage

### TrueNAS SCALE Features (24.04+)
*Automatically enabled when connected to SCALE*
- **Apps** - Manage Docker Compose-based TrueNAS applications
- **Incus Instances** - Control Incus VMs and containers (SCALE 25.04+)
- **Legacy VMs** - Manage bhyve virtual machines

### Enterprise Features
- **Type-Safe Operations** - Full Pydantic models for request/response validation
- **Comprehensive Error Handling** - Detailed error messages and recovery guidance
Expand Down Expand Up @@ -123,6 +131,13 @@ Once configured, you can interact with TrueNAS using natural language:
"Set up an SMB share for the documents dataset"
"Create a snapshot of all datasets in the tank pool"
"Show me users who have sudo privileges"

# TrueNAS SCALE virtualization examples
"List all running apps and their status"
"Get the configuration for the sonarr app"
"Show me all Incus VMs and containers"
"Update the crypto-nodes VM to use 8 CPUs"
"Restart the plex app"
```

### As a Python Library
Expand Down Expand Up @@ -199,6 +214,34 @@ asyncio.run(main())
- `clone_snapshot` - Clone to new dataset
- `create_snapshot_task` - Setup automated snapshots

### App Management (TrueNAS SCALE)
- `list_apps` - Show all TrueNAS apps with status
- `get_app` - Get detailed app information
- `get_app_config` - Get full app configuration
- `start_app` - Start an app
- `stop_app` - Stop an app
- `restart_app` - Restart an app
- `redeploy_app` - Redeploy after config changes
- `update_app_config` - Update app configuration

### Incus Instance Management (TrueNAS SCALE)
- `list_instances` - Show VMs and containers
- `get_instance` - Get instance details
- `start_instance` - Start an instance
- `stop_instance` - Stop an instance
- `restart_instance` - Restart an instance
- `update_instance` - Update CPU/memory/autostart
- `list_instance_devices` - Show attached devices

### Legacy VM Management
- `list_legacy_vms` - Show bhyve VMs
- `get_legacy_vm` - Get VM details
- `start_legacy_vm` - Start a VM
- `stop_legacy_vm` - Stop a VM
- `restart_legacy_vm` - Restart a VM
- `update_legacy_vm` - Update VM configuration
- `get_legacy_vm_status` - Get VM status

### Debug Tools (Development Mode)
- `debug_connection` - Check connection settings
- `test_connection` - Verify API connectivity
Expand All @@ -221,14 +264,20 @@ truenas_mcp_server/
│ ├── base.py # Base models
│ ├── user.py # User models
│ ├── storage.py # Storage models
│ └── sharing.py # Share models
│ ├── sharing.py # Share models
│ ├── app.py # App models (SCALE)
│ ├── instance.py # Incus instance models (SCALE)
│ └── vm.py # Legacy VM models
├── tools/ # MCP tools
│ ├── __init__.py
│ ├── base.py # Base tool class
│ ├── users.py # User tools
│ ├── storage.py # Storage tools
│ ├── sharing.py # Share tools
│ └── snapshots.py # Snapshot tools
│ ├── snapshots.py # Snapshot tools
│ ├── apps.py # App tools (SCALE)
│ ├── instances.py # Incus instance tools (SCALE)
│ └── vms.py # Legacy VM tools
└── exceptions.py # Custom exceptions
```

Expand Down
243 changes: 243 additions & 0 deletions tests/test_virtualization_live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
"""
Live integration tests for TrueNAS virtualization tools

These tests run against actual TrueNAS servers and require:
- TRUENAS_URL and TRUENAS_API_KEY environment variables set
- Test resources created manually or by this test

Test resources use the 'mcp-test-' prefix to avoid conflicts with production.
"""

import asyncio
import os
import pytest
from typing import Optional

# Skip all tests if not configured
pytestmark = pytest.mark.skipif(
not os.environ.get("TRUENAS_URL"),
reason="TRUENAS_URL not set - skipping live tests"
)


class TestAppToolsLive:
"""Live tests for AppTools"""

@pytest.fixture
def app_tools(self):
from truenas_mcp_server.tools.apps import AppTools
return AppTools()

@pytest.mark.asyncio
async def test_list_apps(self, app_tools):
"""Test listing all apps (read-only, safe)"""
result = await app_tools.list_apps()
assert result["success"] is True
assert "apps" in result
assert "metadata" in result
print(f"\nFound {result['metadata']['total_apps']} apps")
for app in result["apps"]:
print(f" - {app['name']}: {app['state']}")

@pytest.mark.asyncio
async def test_get_app_existing(self, app_tools):
"""Test getting an existing app"""
# First list apps to find one
list_result = await app_tools.list_apps()
if not list_result["apps"]:
pytest.skip("No apps found to test")

app_name = list_result["apps"][0]["name"]
result = await app_tools.get_app(app_name)
assert result["success"] is True
assert result["app"]["name"] == app_name

@pytest.mark.asyncio
async def test_get_app_config_existing(self, app_tools):
"""Test getting app config (uses quirky plain string body!)"""
list_result = await app_tools.list_apps()
if not list_result["apps"]:
pytest.skip("No apps found to test")

app_name = list_result["apps"][0]["name"]
result = await app_tools.get_app_config(app_name)
assert result["success"] is True
assert result["app_name"] == app_name
assert "config" in result
print(f"\nApp {app_name} config keys: {list(result['config'].keys()) if result['config'] else 'empty'}")

@pytest.mark.asyncio
async def test_get_app_nonexistent(self, app_tools):
"""Test getting a non-existent app"""
result = await app_tools.get_app("nonexistent-app-xyz123")
assert result["success"] is False
assert "not found" in result["error"].lower()


class TestInstanceToolsLive:
"""Live tests for InstanceTools"""

@pytest.fixture
def instance_tools(self):
from truenas_mcp_server.tools.instances import InstanceTools
return InstanceTools()

@pytest.mark.asyncio
async def test_list_instances(self, instance_tools):
"""Test listing all Incus instances (read-only, safe)"""
result = await instance_tools.list_instances()
assert result["success"] is True
assert "instances" in result
assert "metadata" in result
print(f"\nFound {result['metadata']['total_instances']} instances")
for inst in result["instances"]:
print(f" - {inst['name']} ({inst['type']}): {inst['status']} - {inst['cpu']} CPU, {inst['memory_gb']}GB RAM")

@pytest.mark.asyncio
async def test_list_instances_filter_vm(self, instance_tools):
"""Test filtering instances by type"""
result = await instance_tools.list_instances(instance_type="VM")
assert result["success"] is True
for inst in result["instances"]:
assert inst["type"] == "VM"

@pytest.mark.asyncio
async def test_list_instances_filter_container(self, instance_tools):
"""Test filtering instances by type"""
result = await instance_tools.list_instances(instance_type="CONTAINER")
assert result["success"] is True
for inst in result["instances"]:
assert inst["type"] == "CONTAINER"

@pytest.mark.asyncio
async def test_get_instance_existing(self, instance_tools):
"""Test getting an existing instance"""
list_result = await instance_tools.list_instances()
if not list_result["instances"]:
pytest.skip("No instances found to test")

inst_name = list_result["instances"][0]["name"]
result = await instance_tools.get_instance(inst_name)
assert result["success"] is True
assert result["instance"]["name"] == inst_name

@pytest.mark.asyncio
async def test_get_instance_nonexistent(self, instance_tools):
"""Test getting a non-existent instance"""
result = await instance_tools.get_instance("nonexistent-instance-xyz123")
assert result["success"] is False
assert "not found" in result["error"].lower()

@pytest.mark.asyncio
async def test_list_instance_devices(self, instance_tools):
"""Test listing devices for an instance"""
list_result = await instance_tools.list_instances()
if not list_result["instances"]:
pytest.skip("No instances found to test")

inst_name = list_result["instances"][0]["name"]
result = await instance_tools.list_instance_devices(inst_name)
assert result["success"] is True
assert "devices" in result
print(f"\nInstance {inst_name} has {result['metadata']['device_count']} devices")


class TestLegacyVMToolsLive:
"""Live tests for LegacyVMTools"""

@pytest.fixture
def vm_tools(self):
from truenas_mcp_server.tools.vms import LegacyVMTools
return LegacyVMTools()

@pytest.mark.asyncio
async def test_list_legacy_vms(self, vm_tools):
"""Test listing all legacy VMs (read-only, safe)"""
result = await vm_tools.list_legacy_vms()
assert result["success"] is True
assert "vms" in result
assert "metadata" in result
print(f"\nFound {result['metadata']['total_vms']} legacy VMs")
for vm in result["vms"]:
print(f" - {vm['name']} (ID: {vm['id']}): {vm['status']} - {vm['vcpus']} vCPU, {vm['memory_mb']}MB RAM")

@pytest.mark.asyncio
async def test_get_legacy_vm_existing(self, vm_tools):
"""Test getting an existing legacy VM"""
list_result = await vm_tools.list_legacy_vms()
if not list_result["vms"]:
pytest.skip("No legacy VMs found to test")

vm_id = list_result["vms"][0]["id"]
result = await vm_tools.get_legacy_vm(vm_id)
assert result["success"] is True
assert result["vm"]["id"] == vm_id

@pytest.mark.asyncio
async def test_get_legacy_vm_nonexistent(self, vm_tools):
"""Test getting a non-existent legacy VM"""
result = await vm_tools.get_legacy_vm(99999)
assert result["success"] is False
assert "not found" in result["error"].lower()


class TestIntegration:
"""Integration tests that exercise multiple tools together"""

@pytest.mark.asyncio
async def test_full_workflow(self):
"""
Test a full workflow:
1. List resources
2. Get details of each type
3. Verify API quirks are handled correctly
"""
from truenas_mcp_server.tools.apps import AppTools
from truenas_mcp_server.tools.instances import InstanceTools
from truenas_mcp_server.tools.vms import LegacyVMTools

app_tools = AppTools()
instance_tools = InstanceTools()
vm_tools = LegacyVMTools()

# List all resource types
apps = await app_tools.list_apps()
instances = await instance_tools.list_instances()
vms = await vm_tools.list_legacy_vms()

print("\n=== TrueNAS Virtualization Summary ===")
print(f"Apps: {apps['metadata']['total_apps']}")
print(f"Incus Instances: {instances['metadata']['total_instances']}")
print(f"Legacy VMs: {vms['metadata']['total_vms']}")

# Verify all succeeded
assert apps["success"]
assert instances["success"]
assert vms["success"]


if __name__ == "__main__":
# Run quick smoke test
async def smoke_test():
from truenas_mcp_server.tools.apps import AppTools
from truenas_mcp_server.tools.instances import InstanceTools
from truenas_mcp_server.tools.vms import LegacyVMTools

print("Testing AppTools...")
app_tools = AppTools()
apps = await app_tools.list_apps()
print(f" Found {apps['metadata']['total_apps']} apps")

print("Testing InstanceTools...")
instance_tools = InstanceTools()
instances = await instance_tools.list_instances()
print(f" Found {instances['metadata']['total_instances']} instances")

print("Testing LegacyVMTools...")
vm_tools = LegacyVMTools()
vms = await vm_tools.list_legacy_vms()
print(f" Found {vms['metadata']['total_vms']} legacy VMs")

print("\nAll smoke tests passed!")

asyncio.run(smoke_test())
4 changes: 2 additions & 2 deletions truenas_mcp_server/client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
HTTP Client management for TrueNAS API interactions
"""

from .http_client import TrueNASClient, get_client, close_client
from .http_client import TrueNASClient, TrueNASVariant, get_client, close_client

__all__ = ["TrueNASClient", "get_client", "close_client"]
__all__ = ["TrueNASClient", "TrueNASVariant", "get_client", "close_client"]
Loading