Initial commit: Agentic OS troubleshooting platform

Self-hosted, Docker-based agentic troubleshooting platform: FastAPI backend +
LangGraph agent, Next.js UI, tiered LLM routing (local Ollama -> Gemini ->
DeepSeek -> OpenRouter), MCP server manager, encrypted device credentials,
RBAC, audit log, project-memory + Obsidian integrations, and editable
troubleshooting decision rules tuned for the GeneseasX vessel stack.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-14 22:11:07 +03:00
commit 6185b9b85a
126 changed files with 14565 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
[project]
name = "fortigate-mcp"
version = "0.1.0"
description = "FortiGate (FortiOS REST API) MCP server"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.2.0",
"httpx>=0.27.0",
]
[tool.uv]
package = false

View File

@@ -0,0 +1,108 @@
"""FortiGate MCP server (FortiOS REST API).
Read-only diagnostics by default. Token (API key) auth. Self-signed certs are
accepted (typical for standalone FortiGates). Writes require FORTIGATE_ALLOW_WRITES=true.
"""
from __future__ import annotations
import json
import os
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("fortigate")
ALLOW_WRITES = os.environ.get("FORTIGATE_ALLOW_WRITES", "false").lower() == "true"
_active: dict | None = None # {"host","api_key","port"}
def _resolve(host, api_key, port) -> dict:
if host:
return {"host": host, "api_key": api_key, "port": port or 443}
if _active:
return _active
raise RuntimeError("No FortiGate target. Call fortigate_connect first.")
def _base(target: dict) -> str:
return f"https://{target['host']}:{target.get('port', 443)}"
def _get(target: dict, path: str, params: dict | None = None) -> dict:
url = f"{_base(target)}{path}"
headers = {"Authorization": f"Bearer {target.get('api_key')}"}
with httpx.Client(verify=False, timeout=20) as client: # noqa: S501 (self-signed device)
r = client.get(url, headers=headers, params=params)
r.raise_for_status()
return r.json()
@mcp.tool()
def fortigate_connect(host: str, api_key: str, port: int = 443, username: str | None = None) -> str:
"""Set the active FortiGate target (token/API-key auth) and verify connectivity."""
global _active
target = {"host": host, "api_key": api_key, "port": port}
try:
_get(target, "/api/v2/monitor/system/status")
except Exception as exc: # noqa: BLE001
return f"connect failed: {exc}"
_active = target
return f"connected to FortiGate {host}"
@mcp.tool()
def fortigate_test_connection(host: str | None = None, api_key: str | None = None, port: int = 443) -> str:
"""Test connectivity to a FortiGate."""
target = _resolve(host, api_key, port)
try:
_get(target, "/api/v2/monitor/system/status")
return "ok"
except Exception as exc: # noqa: BLE001
return f"failed: {exc}"
@mcp.tool()
def fortigate_get_system_status(host: str | None = None, api_key: str | None = None, port: int = 443) -> str:
"""System status: model, version, serial, uptime."""
target = _resolve(host, api_key, port)
return json.dumps(_get(target, "/api/v2/monitor/system/status"), indent=2)[:6000]
@mcp.tool()
def fortigate_list_interfaces(host: str | None = None, api_key: str | None = None, port: int = 443) -> str:
"""List interfaces and their link/IP state."""
target = _resolve(host, api_key, port)
data = _get(target, "/api/v2/monitor/system/interface")
return json.dumps(data, indent=2)[:8000]
@mcp.tool()
def fortigate_list_policies(host: str | None = None, api_key: str | None = None, port: int = 443) -> str:
"""List firewall policies (cmdb)."""
target = _resolve(host, api_key, port)
data = _get(target, "/api/v2/cmdb/firewall/policy")
return json.dumps(data, indent=2)[:8000]
@mcp.tool()
def fortigate_get_routing(host: str | None = None, api_key: str | None = None, port: int = 443) -> str:
"""Active routing table."""
target = _resolve(host, api_key, port)
return json.dumps(_get(target, "/api/v2/monitor/router/ipv4"), indent=2)[:8000]
@mcp.tool()
def fortigate_get_logs(lines: int = 50, host: str | None = None, api_key: str | None = None, port: int = 443) -> str:
"""Recent event log (best-effort; depends on logging config)."""
target = _resolve(host, api_key, port)
try:
data = _get(target, "/api/v2/log/memory/event", params={"rows": lines})
return json.dumps(data, indent=2)[:8000]
except Exception as exc: # noqa: BLE001
return f"log fetch failed (logging may be disk/forticloud): {exc}"
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,12 @@
[project]
name = "fortiswitch-mcp"
version = "0.1.0"
description = "FortiSwitch (FortiSwitchOS REST API) MCP server"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.2.0",
"httpx>=0.27.0",
]
[tool.uv]
package = false

View File

@@ -0,0 +1,136 @@
"""FortiSwitch MCP server (standalone FortiSwitchOS REST API).
Read-only diagnostics. Supports token (API key) auth, with optional
username/password session login as a fallback. Writes require
FORTISWITCH_ALLOW_WRITES=true. Endpoints are best-effort and may vary by
FortiSwitchOS version.
"""
from __future__ import annotations
import json
import os
import httpx
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("fortiswitch")
ALLOW_WRITES = os.environ.get("FORTISWITCH_ALLOW_WRITES", "false").lower() == "true"
_active: dict | None = None # {"host","api_key","username","password","port"}
def _resolve(host, api_key, username, password, port) -> dict:
if host:
return {
"host": host,
"api_key": api_key,
"username": username,
"password": password,
"port": port or 443,
}
if _active:
return _active
raise RuntimeError("No FortiSwitch target. Call fortiswitch_connect first.")
def _base(target: dict) -> str:
return f"https://{target['host']}:{target.get('port', 443)}"
def _client(target: dict) -> httpx.Client:
client = httpx.Client(verify=False, timeout=20) # noqa: S501 (self-signed device)
if not target.get("api_key") and target.get("username"):
# Session login fallback
try:
client.post(
f"{_base(target)}/logincheck",
data={"username": target["username"], "secretkey": target.get("password", "")},
)
except Exception: # noqa: BLE001
pass
return client
def _headers(target: dict) -> dict:
if target.get("api_key"):
return {"Authorization": f"Bearer {target['api_key']}"}
return {}
def _get(target: dict, path: str, params: dict | None = None) -> dict:
client = _client(target)
try:
r = client.get(f"{_base(target)}{path}", headers=_headers(target), params=params)
r.raise_for_status()
return r.json()
finally:
client.close()
@mcp.tool()
def fortiswitch_connect(host: str, api_key: str | None = None, username: str | None = None,
password: str | None = None, port: int = 443) -> str:
"""Set the active FortiSwitch target and verify connectivity."""
global _active
target = {"host": host, "api_key": api_key, "username": username, "password": password, "port": port}
try:
_get(target, "/api/v2/monitor/system/status")
except Exception as exc: # noqa: BLE001
return f"connect failed: {exc}"
_active = target
return f"connected to FortiSwitch {host}"
@mcp.tool()
def fortiswitch_test_connection(host: str | None = None, api_key: str | None = None,
username: str | None = None, password: str | None = None,
port: int = 443) -> str:
"""Test connectivity to a FortiSwitch."""
target = _resolve(host, api_key, username, password, port)
try:
_get(target, "/api/v2/monitor/system/status")
return "ok"
except Exception as exc: # noqa: BLE001
return f"failed: {exc}"
@mcp.tool()
def fortiswitch_get_system_status(host: str | None = None, api_key: str | None = None,
username: str | None = None, password: str | None = None,
port: int = 443) -> str:
"""System status: model, version, serial, uptime."""
target = _resolve(host, api_key, username, password, port)
return json.dumps(_get(target, "/api/v2/monitor/system/status"), indent=2)[:6000]
@mcp.tool()
def fortiswitch_list_ports(host: str | None = None, api_key: str | None = None,
username: str | None = None, password: str | None = None,
port: int = 443) -> str:
"""List switch ports and their status."""
target = _resolve(host, api_key, username, password, port)
for path in ("/api/v2/monitor/switch/port", "/api/v2/cmdb/switch/interface"):
try:
return json.dumps(_get(target, path), indent=2)[:8000]
except Exception: # noqa: BLE001
continue
return "could not read switch ports on known endpoints"
@mcp.tool()
def fortiswitch_get_port_stats(host: str | None = None, api_key: str | None = None,
username: str | None = None, password: str | None = None,
port: int = 443) -> str:
"""Per-port statistics / counters (errors, traffic)."""
target = _resolve(host, api_key, username, password, port)
for path in ("/api/v2/monitor/switch/port-statistics", "/api/v2/monitor/switch/port"):
try:
return json.dumps(_get(target, path), indent=2)[:8000]
except Exception: # noqa: BLE001
continue
return "could not read port statistics on known endpoints"
if __name__ == "__main__":
mcp.run()

View File

@@ -0,0 +1,12 @@
[project]
name = "ssh-generic-mcp"
version = "0.1.0"
description = "Generic SSH MCP server for Debian/Linux + GeneseasX hosts"
requires-python = ">=3.10"
dependencies = [
"mcp>=1.2.0",
"paramiko>=3.4.0",
]
[tool.uv]
package = false

View File

@@ -0,0 +1,135 @@
"""Generic SSH MCP server.
Read-only by default. Mutating commands are blocked unless SSH_ALLOW_WRITES=true.
Targets can be set with ssh_connect and reused, or passed per-call.
Conventions mirror the other device MCPs in this stack (saved active target,
*_ALLOW_WRITES gate for writes).
"""
from __future__ import annotations
import os
import re
import shlex
import paramiko
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("ssh-generic")
ALLOW_WRITES = os.environ.get("SSH_ALLOW_WRITES", "false").lower() == "true"
# Heuristic denylist for mutating operations when writes are not allowed.
_WRITE_TOKENS = (
"rm ", "mv ", "dd ", "mkfs", "systemctl start", "systemctl stop", "systemctl restart",
"systemctl disable", "systemctl enable", "iptables", "nft ", "ip route add",
"ip route del", "ip addr add", "ip link set", "apt", "yum", "dnf", "tee ",
"chmod", "chown", "passwd", "useradd", "userdel", "mount", "umount",
)
# Whole-word matches only (avoid false positives e.g. "halt" inside "head").
_WRITE_WORDS = re.compile(r"\b(reboot|shutdown|poweroff|halt|kill|pkill)\b")
# Shell output redirect to a file (allow stderr redirects like 2>/dev/null).
_WRITE_REDIRECT = re.compile(r"(?<![0-9])>(?!>)|>>")
# Active target: {"host","port","username","password"}
_active: dict | None = None
def _resolve(host, port, username, password) -> dict:
if host:
return {
"host": host,
"port": port or 22,
"username": username or "root",
"password": password,
}
if _active:
return _active
raise RuntimeError("No SSH target. Call ssh_connect first or pass host/username.")
def _client(target: dict) -> paramiko.SSHClient:
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(
hostname=target["host"],
port=int(target.get("port", 22)),
username=target.get("username"),
password=target.get("password"),
timeout=15,
allow_agent=False,
look_for_keys=bool(not target.get("password")),
)
return client
def _is_write(command: str) -> bool:
low = command.lower()
if _WRITE_WORDS.search(low):
return True
if _WRITE_REDIRECT.search(command):
return True
return any(tok in low for tok in _WRITE_TOKENS)
@mcp.tool()
def ssh_connect(host: str, username: str = "root", password: str | None = None, port: int = 22) -> str:
"""Set the active SSH target and verify connectivity."""
global _active
target = {"host": host, "port": port, "username": username, "password": password}
try:
c = _client(target)
c.close()
except Exception as exc: # noqa: BLE001
return f"connect failed: {exc}"
_active = target
return f"connected to {username}@{host}:{port}"
@mcp.tool()
def ssh_test_connection(host: str | None = None, username: str | None = None,
password: str | None = None, port: int = 22) -> str:
"""Test SSH connectivity to a host."""
target = _resolve(host, port, username, password)
try:
c = _client(target)
c.close()
return "ok"
except Exception as exc: # noqa: BLE001
return f"failed: {exc}"
@mcp.tool()
def ssh_run(command: str, host: str | None = None, username: str | None = None,
password: str | None = None, port: int = 22, timeout: int = 30) -> str:
"""Run a shell command over SSH. Mutating commands require SSH_ALLOW_WRITES=true."""
if _is_write(command) and not ALLOW_WRITES:
return (
"blocked: this looks like a mutating command and SSH_ALLOW_WRITES is false. "
"Use the approval flow / enable writes to proceed."
)
target = _resolve(host, port, username, password)
client = _client(target)
try:
_, stdout, stderr = client.exec_command(command, timeout=timeout)
out = stdout.read().decode(errors="replace")
err = stderr.read().decode(errors="replace")
code = stdout.channel.recv_exit_status()
result = out
if err:
result += f"\n[stderr]\n{err}"
return f"[exit {code}]\n{result}".strip()
finally:
client.close()
@mcp.tool()
def ssh_service_status(service: str, host: str | None = None, username: str | None = None,
password: str | None = None, port: int = 22) -> str:
"""Read-only systemd service status."""
return ssh_run(f"systemctl status {shlex.quote(service)} --no-pager",
host=host, username=username, password=password, port=port)
if __name__ == "__main__":
mcp.run()