Ziel: Ein lokales LLM in Unsloth Studio ruft kontrollierte MCP-Befehle auf. Der MCP-Server führt diese Befehle über SSH in der Rocky-Linux-VM aus.
Dieses Beispiel ist auf das rudimentärste begrenzt und überlässt dem LLM vollen root Zugriff auf die VM.
Ein praktisches Beispiel ist am Ende des Artikels eingefügt.
Vorbereitungen
Benötigt wird:- eine Linux-VM, auf die von lokal per SSH (Key) zugegriffen werden kann
- ein Linuxbenutzer mit root-Rechten (ohne Passwort)
- Unsloth Studio (mit heruntergeladenem LLM, ich verwende unsloth/Qwen3.8-27B-GGUF:UD-Q5_K_M)
Der MCP-Server läuft unter Windows und benutzt den dort vorhandenen OpenSSH-Client. Alle folgenden Befehle werden in PowerShell ausgeführt.
Python-Umgebung unter Windows
Schritt 1 – Projektordner erstellen
New-Item -ItemType Directory -Path "C:\VM-MCP" -Force
Set-Location "C:\VM-MCP"
Schritt 2 – Virtuelle Python-Umgebung
py --version
py -m venv .venv
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\.venv\Scripts\Activate.ps1
Nach erfolgreicher Aktivierung beginnt die PowerShell-Zeile mit (.venv).
(.venv) PS C:\VM-MCP>
Schritt 3 – MCP-SDK installieren
python -m pip install --upgrade pip
pip install "mcp[cli]" pyyaml
python -c "import mcp; print('MCP-Paket erfolgreich geladen')"
MCP-Server einrichten
Speicherort des Servers:
C:\VM-MCP\server.py
Das folgende Skript verwendet absolute Windows-Pfade. Dadurch ist es unabhängig davon, ob Unsloth die Datei C:\Users\<Benutzer>\.ssh\config lädt.
Die Befehle werden direkt mit root-Rechten entsprechend umgesetzt.
import logging
import shlex
import socket
import sys
import paramiko
from mcp.server import MCPServer
logging.getLogger("paramiko").setLevel(logging.WARNING)
mcp = MCPServer("Rocky Linux VM Root")
# ============================================================
# SSH-Konfiguration
# ============================================================
SSH_HOST = "127.0.0.1"
SSH_PORT = 2222
SSH_USER = "r"
SSH_KEY = r"C:\Users\r\.ssh\rocky_mcp_ed25519"
SSH_KNOWN_HOSTS = r"C:\Users\r\.ssh\known_hosts"
SSH_TIMEOUT = 600
MAX_SSH_TIMEOUT = 1800
# ============================================================
# Interne SSH-Ausführung
# ============================================================
def run_ssh(command: str, timeout: int = SSH_TIMEOUT) -> str:
"""Führt einen Linux-Befehl über eine geprüfte SSH-Verbindung aus."""
client = paramiko.SSHClient()
try:
client.load_host_keys(SSH_KNOWN_HOSTS)
client.connect(
SSH_HOST,
port=SSH_PORT,
username=SSH_USER,
key_filename=SSH_KEY,
timeout=10,
banner_timeout=10,
auth_timeout=10,
allow_agent=False,
look_for_keys=False,
)
_, stdout, stderr = client.exec_command(
command,
timeout=timeout,
)
stdout_text = stdout.read().decode("utf-8", errors="replace")
stderr_text = stderr.read().decode("utf-8", errors="replace")
exit_code = stdout.channel.recv_exit_status()
except (TimeoutError, socket.timeout):
return "FEHLER: Zeitüberschreitung bei der SSH-Ausführung."
except FileNotFoundError as exc:
return f"FEHLER: SSH-Datei wurde nicht gefunden.\n{exc}"
except paramiko.AuthenticationException:
return "FEHLER: SSH-Authentifizierung fehlgeschlagen."
except paramiko.BadHostKeyException as exc:
return f"FEHLER: SSH-Hostschlüssel stimmt nicht überein: {exc}"
except (PermissionError, paramiko.SSHException) as exc:
return f"FEHLER: SSH-Verbindung fehlgeschlagen.\n{exc}"
except Exception as exc:
return f"FEHLER: {type(exc).__name__}: {exc}"
finally:
client.close()
output_parts = []
if stdout_text.strip():
output_parts.append(stdout_text.strip())
if stderr_text.strip():
output_parts.append("STDERR:\n" + stderr_text.strip())
if not output_parts:
output_parts.append("(Keine Ausgabe)")
output_parts.append(f"Exit-Code: {exit_code}")
return "\n\n".join(output_parts)
# ============================================================
# Einziges MCP-Werkzeug
# ============================================================
@mcp.tool()
def run_root_command(command: str, timeout: int = SSH_TIMEOUT) -> str:
"""
Führt einen beliebigen Linux-Shellbefehl als Root auf der VM aus.
Verwende dieses Werkzeug für alle Aktionen auf der Rocky-Linux-VM,
einschließlich Paketverwaltung, Systemupdates, Dienste, Dateien,
Logs und Diagnose. Der Befehl läuft über sudo -n bash -lc.
command:
Vollständiger Linux-Shellbefehl für die VM.
timeout:
Maximale Laufzeit in Sekunden, begrenzt auf 1 bis 1800.
"""
command = str(command).strip()
if not command:
return "FEHLER: Der Befehl darf nicht leer sein."
if "\x00" in command:
return "FEHLER: Der Befehl enthält ein ungültiges Nullzeichen."
try:
timeout = int(timeout)
except (TypeError, ValueError):
return "FEHLER: 'timeout' muss eine ganze Zahl sein."
timeout = max(1, min(timeout, MAX_SSH_TIMEOUT))
return run_ssh(
"sudo -n bash -lc " + shlex.quote(command),
timeout=timeout,
)
def self_test() -> int:
"""Prüft SSH-Verbindung und Root-Rechte ohne MCP-Client."""
print(run_root_command("id; hostname; uptime"))
return 0
if __name__ == "__main__":
if "--self-test" in sys.argv:
raise SystemExit(self_test())
mcp.run(transport="stdio")
MCP-Server lokal prüfen
Set-Location C:\VM-MCP
.\.venv\Scripts\Activate.ps1
python -m py_compile .\server.py
python .\server.py --self-test
Erwartung: Hostname, Benutzer r und uptime erscheinen mit Exit-Code 0. Ein normaler Start wartet ohne sichtbare Ausgabe auf MCP-Nachrichten:
python .\server.py
Beenden mit Strg+C
Verbindung zu Unsloth Studio
In Unsloth Studio den Dialog MCP Servers öffnen und einen lokalen stdio-Server anlegen:
Display name: Rocky Linux
URL or command: C:\VM-MCP\.venv\Scripts\python.exe C:\VM-MCP\server.py
Environment variables leer
Danach Test connection ausführen und bei grünem Test Add server wählen. Das Modell muss ein Instruct-/Chat-Modell mit Tool-Calling-Unterstützung sein.
Erste Prompts
Verbindungstest:
1. das LLM laden und Context Size vergrößern
2. prüfen ob der MCP-Server grün ist

3. der Prompt (Thinking schaltet man am beten erst einmal auf none:
Prüfe über vm_connection_test die Verbindung zu meiner Rocky-Linux-VM und welche Rechte du dort hast

Damit lässt sich die VM komplett administrieren (im Rahmen der Möglichkeiten des LLM):
