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>
137 lines
4.8 KiB
Python
137 lines
4.8 KiB
Python
"""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()
|