This commit is contained in:
pporcheret
2026-10-26 00:06:24 +01:00
parent c9c3d5f223
commit cd37ac3c0a
207 changed files with 19205 additions and 54 deletions
+11
View File
@@ -0,0 +1,11 @@
PROXMOX_HOST=192.168.1.20
PROXMOX_USER=root@pam
PROXMOX_TOKEN_NAME=hermes
PROXMOX_TOKEN_VALUE=91ac0c88-c744-4b97-8a7d-a5e05c783354
PROXMOX_VERIFY_SSL=false
# Sécurité
ALLOW_POWER_ACTIONS=true
ALLOW_DELETE=false
ALLOW_ROLLBACK=false
+353
View File
@@ -0,0 +1,353 @@
#!/usr/bin/env python3
import os
from typing import Optional
from dotenv import load_dotenv
from proxmoxer import ProxmoxAPI
from mcp.server.fastmcp import FastMCP
load_dotenv("/opt/hermes/proxmox-mcp/.env")
mcp = FastMCP("proxmox-mcp")
PROXMOX_HOST = os.getenv("PROXMOX_HOST")
PROXMOX_USER = os.getenv("PROXMOX_USER")
PROXMOX_TOKEN_NAME = os.getenv("PROXMOX_TOKEN_NAME")
PROXMOX_TOKEN_VALUE = os.getenv("PROXMOX_TOKEN_VALUE")
VERIFY_SSL = (
os.getenv("PROXMOX_VERIFY_SSL", "true").lower() == "true"
)
ALLOW_POWER_ACTIONS = (
os.getenv("ALLOW_POWER_ACTIONS", "false").lower() == "true"
)
ALLOW_DELETE = (
os.getenv("ALLOW_DELETE", "false").lower() == "true"
)
ALLOW_ROLLBACK = (
os.getenv("ALLOW_ROLLBACK", "false").lower() == "true"
)
def get_proxmox():
return ProxmoxAPI(
PROXMOX_HOST,
user=PROXMOX_USER,
token_name=PROXMOX_TOKEN_NAME,
token_value=PROXMOX_TOKEN_VALUE,
verify_ssl=VERIFY_SSL,
backend="https",
)
def power_allowed():
if not ALLOW_POWER_ACTIONS:
raise PermissionError(
"Les actions d'alimentation sont désactivées."
)
@mcp.tool()
def list_nodes():
"""
Liste tous les nodes du cluster.
"""
proxmox = get_proxmox()
return proxmox.nodes.get()
@mcp.tool()
def cluster_status():
"""
Retourne l'état général du cluster.
"""
proxmox = get_proxmox()
return proxmox.cluster.status.get()
@mcp.tool()
def list_vms(node: Optional[str] = None):
"""
Liste les VMs QEMU.
"""
proxmox = get_proxmox()
result = []
if node:
nodes = [node]
else:
nodes = [
n["node"]
for n in proxmox.nodes.get()
]
for current_node in nodes:
try:
vms = proxmox.nodes(current_node).qemu.get()
for vm in vms:
result.append({
"node": current_node,
"vmid": vm.get("vmid"),
"name": vm.get("name"),
"status": vm.get("status"),
"cpus": vm.get("cpus"),
"mem": vm.get("mem"),
"maxmem": vm.get("maxmem"),
"uptime": vm.get("uptime")
})
except Exception as e:
result.append({
"node": current_node,
"error": str(e)
})
return result
@mcp.tool()
def list_lxc(node: Optional[str] = None):
"""
Liste les conteneurs LXC.
"""
proxmox = get_proxmox()
result = []
if node:
nodes = [node]
else:
nodes = [
n["node"]
for n in proxmox.nodes.get()
]
for current_node in nodes:
try:
lxcs = proxmox.nodes(current_node).lxc.get()
for ct in lxcs:
result.append({
"node": current_node,
"vmid": ct.get("vmid"),
"name": ct.get("name"),
"status": ct.get("status"),
"cpus": ct.get("cpus"),
"mem": ct.get("mem"),
"maxmem": ct.get("maxmem"),
})
except Exception as e:
result.append({
"node": current_node,
"error": str(e)
})
return result
@mcp.tool()
def vm_status(node: str, vmid: int):
"""
Statut détaillé d'une VM.
"""
proxmox = get_proxmox()
return proxmox.nodes(node).qemu(vmid).status.current.get()
@mcp.tool()
def lxc_status(node: str, vmid: int):
"""
Statut détaillé d'un LXC.
"""
proxmox = get_proxmox()
return proxmox.nodes(node).lxc(vmid).status.current.get()
@mcp.tool()
def node_resources(node: str):
"""
CPU / RAM / stockage du node.
"""
proxmox = get_proxmox()
return proxmox.nodes(node).status.get()
@mcp.tool()
def start_vm(node: str, vmid: int):
power_allowed()
proxmox = get_proxmox()
task = (
proxmox.nodes(node)
.qemu(vmid)
.status.start.post()
)
return {
"action": "start",
"vmid": vmid,
"task": task
}
@mcp.tool()
def stop_vm(node: str, vmid: int):
power_allowed()
proxmox = get_proxmox()
task = (
proxmox.nodes(node)
.qemu(vmid)
.status.stop.post()
)
return {
"action": "stop",
"vmid": vmid,
"task": task
}
@mcp.tool()
def shutdown_vm(node: str, vmid: int):
power_allowed()
proxmox = get_proxmox()
task = (
proxmox.nodes(node)
.qemu(vmid)
.status.shutdown.post()
)
return {
"action": "shutdown",
"vmid": vmid,
"task": task
}
@mcp.tool()
def reboot_vm(node: str, vmid: int):
power_allowed()
proxmox = get_proxmox()
task = (
proxmox.nodes(node)
.qemu(vmid)
.status.reboot.post()
)
return {
"action": "reboot",
"vmid": vmid,
"task": task
}
@mcp.tool()
def create_snapshot(
node: str,
vmid: int,
snapshot_name: str,
description: str = ""
):
"""
Création snapshot VM.
"""
proxmox = get_proxmox()
task = (
proxmox.nodes(node)
.qemu(vmid)
.snapshot.post(
snapname=snapshot_name,
description=description
)
)
return {
"action": "snapshot",
"vmid": vmid,
"snapshot": snapshot_name,
"task": task
}
@mcp.tool()
def list_snapshots(node: str, vmid: int):
proxmox = get_proxmox()
return (
proxmox.nodes(node)
.qemu(vmid)
.snapshot.get()
)
@mcp.tool()
def rollback_snapshot(
node: str,
vmid: int,
snapshot_name: str
):
"""
Rollback snapshot.
Désactivé par défaut.
"""
if not ALLOW_ROLLBACK:
raise PermissionError(
"Rollback désactivé."
)
proxmox = get_proxmox()
task = (
proxmox.nodes(node)
.qemu(vmid)
.snapshot(snapshot_name)
.rollback.post()
)
return {
"action": "rollback",
"vmid": vmid,
"snapshot": snapshot_name,
"task": task
}
if __name__ == "__main__":
mcp.run()
+16
View File
@@ -0,0 +1,16 @@
from dotenv import load_dotenv
from proxmoxer import ProxmoxAPI
import os
load_dotenv("/opt/mcp-proxmox/.env")
p = ProxmoxAPI(
os.getenv("PROXMOX_HOST"),
user=os.getenv("PROXMOX_USER"),
token_name=os.getenv("PROXMOX_TOKEN_NAME"),
token_value=os.getenv("PROXMOX_TOKEN_VALUE"),
verify_ssl=False,
backend="https",
)
print(p.nodes.get())
+6
View File
@@ -0,0 +1,6 @@
WAZUH_URL=https://192.168.1.52:55000
WAZUH_USER=wazuh-wui
WAZUH_PASSWORD=y+tx9RA3cYiBxqpnoE2+138pV08Jhnwa
INDEXER_URL=https://192.168.1.52:9200
INDEXER_USER=kibanaserver
INDEXER_PASSWORD=cIh?n7Y1FeACDZRb063r0+REWoI3dQAA
+3
View File
@@ -0,0 +1,3 @@
mcp
httpx
python-dotenv
+327
View File
@@ -0,0 +1,327 @@
#!/usr/bin/env python3
import os
from pathlib import Path
import httpx
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
# =============================================================================
# Configuration
# =============================================================================
BASE_DIR = Path(__file__).resolve().parent
load_dotenv(BASE_DIR / ".env")
VIKUNJA_URL = os.getenv("VIKUNJA_URL", "").rstrip("/")
VIKUNJA_TOKEN = os.getenv("VIKUNJA_TOKEN", "")
VIKUNJA_VERIFY_SSL = (
os.getenv("VIKUNJA_VERIFY_SSL", "true").lower()
in ("true", "1", "yes")
)
if not VIKUNJA_URL:
raise RuntimeError("VIKUNJA_URL n'est pas défini dans .env")
if not VIKUNJA_TOKEN:
raise RuntimeError("VIKUNJA_TOKEN n'est pas défini dans .env")
API_URL = f"{VIKUNJA_URL}/api/v2"
HEADERS = {
"Authorization": f"Bearer {VIKUNJA_TOKEN}",
"Accept": "application/json",
}
# =============================================================================
# MCP
# =============================================================================
mcp = FastMCP("vikunja")
# =============================================================================
# Client Vikunja
# =============================================================================
async def vikunja_request(
method: str,
endpoint: str,
params: dict | None = None,
json: dict | None = None,
):
"""
Effectue une requête vers l'API v2 de Vikunja.
"""
url = f"{API_URL}{endpoint}"
async with httpx.AsyncClient(
headers=HEADERS,
verify=VIKUNJA_VERIFY_SSL,
timeout=30.0,
) as client:
response = await client.request(
method,
url,
params=params,
json=json,
)
response.raise_for_status()
if not response.content:
return None
return response.json()
async def vikunja_get(
endpoint: str,
params: dict | None = None,
):
return await vikunja_request(
"GET",
endpoint,
params=params,
)
async def vikunja_post(
endpoint: str,
data: dict,
):
return await vikunja_request(
"POST",
endpoint,
json=data,
)
# =============================================================================
# MCP Tools
# =============================================================================
@mcp.tool()
async def vikunja_get_projects() -> dict:
"""
Retourne les projets Vikunja disponibles pour l'utilisateur.
À utiliser notamment avant de créer une tâche lorsque l'utilisateur
ne donne pas directement l'identifiant du projet.
"""
projects = await vikunja_get(
"/projects",
params={
"format": "markdown",
},
)
result = []
for project in projects or []:
result.append(
{
"id": project.get("id"),
"title": project.get("title"),
"description": project.get("description"),
"parent_project_id": project.get("parent_project_id"),
"is_archived": project.get("is_archived"),
"created": project.get("created"),
"updated": project.get("updated"),
}
)
return {
"count": len(result),
"projects": result,
}
@mcp.tool()
async def vikunja_get_project(
project_id: int,
) -> dict:
"""
Retourne les informations détaillées d'un projet Vikunja.
Args:
project_id:
ID du projet.
"""
project = await vikunja_get(
f"/projects/{project_id}",
params={
"format": "markdown",
},
)
return {
"id": project.get("id"),
"title": project.get("title"),
"description": project.get("description"),
"parent_project_id": project.get("parent_project_id"),
"is_archived": project.get("is_archived"),
"created": project.get("created"),
"updated": project.get("updated"),
}
@mcp.tool()
async def vikunja_get_tasks(
project_id: int,
) -> dict:
"""
Retourne les tâches d'un projet Vikunja.
Args:
project_id:
ID du projet Vikunja.
"""
tasks = await vikunja_get(
f"/projects/{project_id}/tasks",
params={
"format": "markdown",
},
)
result = []
for task in tasks or []:
result.append(
{
"id": task.get("id"),
"title": task.get("title"),
"description": task.get("description"),
"done": task.get("done"),
"priority": task.get("priority"),
"due_date": task.get("due_date"),
"start_date": task.get("start_date"),
"end_date": task.get("end_date"),
"project_id": task.get("project_id"),
"labels": task.get("labels", []),
"created": task.get("created"),
"updated": task.get("updated"),
}
)
return {
"project_id": project_id,
"count": len(result),
"tasks": result,
}
@mcp.tool()
async def vikunja_get_task(
task_id: int,
) -> dict:
"""
Retourne le détail d'une tâche Vikunja.
Args:
task_id:
ID de la tâche.
"""
task = await vikunja_get(
f"/tasks/{task_id}",
params={
"format": "markdown",
},
)
return {
"id": task.get("id"),
"title": task.get("title"),
"description": task.get("description"),
"done": task.get("done"),
"priority": task.get("priority"),
"due_date": task.get("due_date"),
"start_date": task.get("start_date"),
"end_date": task.get("end_date"),
"project_id": task.get("project_id"),
"labels": task.get("labels", []),
"assignees": task.get("assignees", []),
"created": task.get("created"),
"updated": task.get("updated"),
}
@mcp.tool()
async def vikunja_create_task(
project_id: int,
title: str,
description: str = "",
due_date: str | None = None,
priority: int = 0,
) -> dict:
"""
Crée une nouvelle tâche dans Vikunja.
Args:
project_id:
ID du projet dans lequel créer la tâche.
title:
Titre de la tâche.
description:
Description facultative.
due_date:
Date d'échéance facultative au format ISO 8601.
Exemple : 2026-09-15T18:00:00+02:00
priority:
Priorité Vikunja.
0 signifie aucune priorité.
"""
data = {
"title": title,
"description": description,
"priority": priority,
}
if due_date:
data["due_date"] = due_date
task = await vikunja_request(
"POST",
f"/projects/{project_id}/tasks",
params={
"format": "markdown",
},
json=data,
)
return {
"success": True,
"task": {
"id": task.get("id"),
"title": task.get("title"),
"description": task.get("description"),
"project_id": task.get("project_id"),
"priority": task.get("priority"),
"due_date": task.get("due_date"),
"done": task.get("done"),
},
}
# =============================================================================
# Main
# =============================================================================
if __name__ == "__main__":
mcp.run()