monitoring-aiops 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcp_server/__init__.py +1 -0
- mcp_server/_shared.py +101 -0
- mcp_server/server.py +36 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/alerts.py +35 -0
- mcp_server/tools/prtg.py +95 -0
- mcp_server/tools/prtg_write.py +67 -0
- mcp_server/tools/sw_health.py +97 -0
- mcp_server/tools/sw_write.py +189 -0
- mcp_server/tools/swql.py +52 -0
- monitoring_aiops/__init__.py +9 -0
- monitoring_aiops/cli/__init__.py +9 -0
- monitoring_aiops/cli/_common.py +78 -0
- monitoring_aiops/cli/_root.py +57 -0
- monitoring_aiops/cli/alert.py +51 -0
- monitoring_aiops/cli/doctor.py +21 -0
- monitoring_aiops/cli/init.py +118 -0
- monitoring_aiops/cli/overview.py +16 -0
- monitoring_aiops/cli/secret.py +105 -0
- monitoring_aiops/cli/swql.py +51 -0
- monitoring_aiops/config.py +148 -0
- monitoring_aiops/connection.py +188 -0
- monitoring_aiops/doctor.py +91 -0
- monitoring_aiops/governance/__init__.py +40 -0
- monitoring_aiops/governance/audit.py +377 -0
- monitoring_aiops/governance/budget.py +225 -0
- monitoring_aiops/governance/decorators.py +474 -0
- monitoring_aiops/governance/paths.py +23 -0
- monitoring_aiops/governance/patterns.py +378 -0
- monitoring_aiops/governance/policy.py +411 -0
- monitoring_aiops/governance/sanitize.py +39 -0
- monitoring_aiops/governance/undo.py +218 -0
- monitoring_aiops/ops/__init__.py +1 -0
- monitoring_aiops/ops/_util.py +32 -0
- monitoring_aiops/ops/alerts.py +79 -0
- monitoring_aiops/ops/overview.py +29 -0
- monitoring_aiops/ops/prtg.py +145 -0
- monitoring_aiops/ops/prtg_write.py +59 -0
- monitoring_aiops/ops/sw_health.py +199 -0
- monitoring_aiops/ops/sw_write.py +179 -0
- monitoring_aiops/ops/swql.py +95 -0
- monitoring_aiops/secretstore.py +302 -0
- monitoring_aiops-0.1.0.dist-info/METADATA +114 -0
- monitoring_aiops-0.1.0.dist-info/RECORD +47 -0
- monitoring_aiops-0.1.0.dist-info/WHEEL +4 -0
- monitoring_aiops-0.1.0.dist-info/entry_points.txt +3 -0
- monitoring_aiops-0.1.0.dist-info/licenses/LICENSE +21 -0
mcp_server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP server package for monitoring-aiops."""
|
mcp_server/_shared.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Shared MCP server primitives: the FastMCP instance, connection helper,
|
|
2
|
+
error sanitisation, and the ``@tool_errors`` decorator.
|
|
3
|
+
|
|
4
|
+
Tool modules under ``mcp_server/tools/`` import ``mcp`` from here and register
|
|
5
|
+
their ``@mcp.tool()`` functions onto it. ``mcp_server/server.py`` then imports
|
|
6
|
+
those modules and runs the server.
|
|
7
|
+
|
|
8
|
+
Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any FastMCP-reflected
|
|
9
|
+
tool signature — on older mcp/pydantic the union eval'd to ``types.UnionType``
|
|
10
|
+
crashes FastMCP's ``issubclass`` check.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import functools
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
from collections.abc import Callable
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Optional
|
|
19
|
+
|
|
20
|
+
from mcp.server.fastmcp import FastMCP
|
|
21
|
+
|
|
22
|
+
from monitoring_aiops.config import load_config
|
|
23
|
+
from monitoring_aiops.connection import ConnectionManager, MonitoringApiError
|
|
24
|
+
from monitoring_aiops.governance import sanitize
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
_DOCTOR_HINT = "Run 'monitoring-aiops doctor' to verify connectivity and credentials."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _safe_error(exc: Exception, tool: str) -> str:
|
|
32
|
+
"""Return an agent-safe error string; log full detail server-side only."""
|
|
33
|
+
logger.error("Tool %s failed", tool, exc_info=True)
|
|
34
|
+
_passthrough = (
|
|
35
|
+
ValueError,
|
|
36
|
+
FileNotFoundError,
|
|
37
|
+
KeyError,
|
|
38
|
+
PermissionError,
|
|
39
|
+
TimeoutError,
|
|
40
|
+
ConnectionError,
|
|
41
|
+
MonitoringApiError,
|
|
42
|
+
)
|
|
43
|
+
if isinstance(exc, _passthrough):
|
|
44
|
+
return sanitize(str(exc), 300)
|
|
45
|
+
return f"{type(exc).__name__}: operation failed."
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def tool_errors(shape: str = "dict") -> Callable:
|
|
49
|
+
"""Wrap a tool body in the canonical try/except → ``_safe_error`` pattern.
|
|
50
|
+
|
|
51
|
+
Place this *between* ``@governed_tool`` and the function so the audit
|
|
52
|
+
decorator and FastMCP still see the original signature.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def decorator(func: Callable) -> Callable:
|
|
56
|
+
name = func.__name__
|
|
57
|
+
|
|
58
|
+
@functools.wraps(func)
|
|
59
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
60
|
+
try:
|
|
61
|
+
return func(*args, **kwargs)
|
|
62
|
+
except Exception as e: # noqa: BLE001 — sanitised below
|
|
63
|
+
msg = _safe_error(e, name)
|
|
64
|
+
if shape == "list":
|
|
65
|
+
return [{"error": msg, "hint": _DOCTOR_HINT}]
|
|
66
|
+
if shape == "str":
|
|
67
|
+
return f"Error: {msg} {_DOCTOR_HINT}"
|
|
68
|
+
return {"error": msg, "hint": _DOCTOR_HINT}
|
|
69
|
+
|
|
70
|
+
return wrapper
|
|
71
|
+
|
|
72
|
+
return decorator
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
mcp = FastMCP(
|
|
76
|
+
"monitoring-aiops",
|
|
77
|
+
instructions=(
|
|
78
|
+
"Network/infra monitoring operations (preview) over SolarWinds Orion "
|
|
79
|
+
"(SWIS/SWQL) and Paessler PRTG: a canned-SWQL library + validated "
|
|
80
|
+
"read-only SWQL passthrough; node/interface/volume/application health and "
|
|
81
|
+
"top-N; active alerts with dedup/rollup; events and historical metrics; "
|
|
82
|
+
"PRTG sensors/alarms/history; and governed light writes — acknowledge "
|
|
83
|
+
"alerts, mute/pause (time-boxed), schedule maintenance, and "
|
|
84
|
+
"unmanage/add/remove a node. Destructive writes (unmanage / remove node) "
|
|
85
|
+
"are risk=high with a dry_run preview and require an approver. Every tool "
|
|
86
|
+
"runs through the monitoring-aiops governance harness (audit / budget / "
|
|
87
|
+
"risk-tier / undo)."
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
_conn_mgr: Optional[ConnectionManager] = None
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _get_connection(target: Optional[str] = None) -> Any:
|
|
95
|
+
"""Return a Monitoring connection, lazily initialising the manager."""
|
|
96
|
+
global _conn_mgr # noqa: PLW0603
|
|
97
|
+
if _conn_mgr is None:
|
|
98
|
+
config_path_str = os.environ.get("MONITORING_AIOPS_CONFIG")
|
|
99
|
+
config_path = Path(config_path_str) if config_path_str else None
|
|
100
|
+
_conn_mgr = ConnectionManager(load_config(config_path))
|
|
101
|
+
return _conn_mgr.connect(target)
|
mcp_server/server.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""MCP server wrapping monitoring-aiops operations (stdio transport).
|
|
2
|
+
|
|
3
|
+
Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
|
|
4
|
+
delegates to the ``monitoring_aiops`` ops package and is wrapped with the
|
|
5
|
+
monitoring-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
|
|
6
|
+
|
|
7
|
+
Standalone, self-governed network/infra monitoring operations (preview) over
|
|
8
|
+
SolarWinds Orion (SWIS/SWQL) and Paessler PRTG: SWQL, alert rollup, health, and
|
|
9
|
+
governed maintenance writes.
|
|
10
|
+
|
|
11
|
+
Source: https://github.com/AIops-tools/Monitoring-AIops
|
|
12
|
+
License: MIT
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
from mcp_server._shared import _safe_error, mcp, tool_errors
|
|
18
|
+
|
|
19
|
+
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
20
|
+
# `mcp` instance. Order does not matter; each module is self-contained.
|
|
21
|
+
from mcp_server.tools import ( # noqa: F401 — side effects
|
|
22
|
+
alerts,
|
|
23
|
+
prtg,
|
|
24
|
+
prtg_write,
|
|
25
|
+
sw_health,
|
|
26
|
+
sw_write,
|
|
27
|
+
swql,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = ["mcp", "main", "_safe_error", "tool_errors"]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def main() -> None:
|
|
34
|
+
"""Run the MCP server over stdio."""
|
|
35
|
+
logging.basicConfig(level=logging.INFO)
|
|
36
|
+
mcp.run(transport="stdio")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tool modules. Importing each registers its @mcp.tool() functions."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Active-alert MCP tools (SolarWinds + PRTG): rollup read + acknowledge."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from monitoring_aiops.governance import governed_tool
|
|
7
|
+
from monitoring_aiops.ops import alerts as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def active_alerts(target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] Active alerts with dedup/rollup by message (SolarWinds or PRTG).
|
|
15
|
+
|
|
16
|
+
Rolls up storms (e.g. interface-flap repeats) into one counted entry so the
|
|
17
|
+
signal isn't buried under duplicates.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
target: Monitoring target name from config; omit for the default.
|
|
21
|
+
"""
|
|
22
|
+
return ops.active_alerts(_get_connection(target))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
@governed_tool(risk_level="low")
|
|
27
|
+
@tool_errors("dict")
|
|
28
|
+
def alert_acknowledge(alert_id: str, target: Optional[str] = None) -> dict:
|
|
29
|
+
"""[WRITE][risk=low] Acknowledge one active alert (reversible triage action).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
alert_id: Alert id (AlertActiveID on SolarWinds, sensor objid on PRTG).
|
|
33
|
+
target: Monitoring target name from config; omit for the default.
|
|
34
|
+
"""
|
|
35
|
+
return ops.acknowledge_alert(_get_connection(target), alert_id)
|
mcp_server/tools/prtg.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""PRTG monitoring MCP tools (Paessler PRTG, read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from monitoring_aiops.governance import governed_tool
|
|
7
|
+
from monitoring_aiops.ops import prtg as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def prtg_sensors(status: Optional[str] = None, target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] List PRTG sensors (objid, sensor, device, status, message, lastValue).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
status: Optional PRTG status code to filter by (e.g. "5" for down).
|
|
18
|
+
target: PRTG target name from config; omit for the default.
|
|
19
|
+
"""
|
|
20
|
+
return ops.list_sensors(_get_connection(target), status)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
@governed_tool(risk_level="low")
|
|
25
|
+
@tool_errors("dict")
|
|
26
|
+
def prtg_sensor_details(sensor_id: str, target: Optional[str] = None) -> dict:
|
|
27
|
+
"""[READ] Detailed status for one PRTG sensor (name, status, lastvalue, ...).
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
sensor_id: PRTG sensor object id (objid).
|
|
31
|
+
target: PRTG target name from config; omit for the default.
|
|
32
|
+
"""
|
|
33
|
+
return ops.sensor_details(_get_connection(target), sensor_id)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
@governed_tool(risk_level="low")
|
|
38
|
+
@tool_errors("dict")
|
|
39
|
+
def prtg_devices(target: Optional[str] = None) -> dict:
|
|
40
|
+
"""[READ] List monitored PRTG devices (objid, device, host, status, group).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
target: PRTG target name from config; omit for the default.
|
|
44
|
+
"""
|
|
45
|
+
return ops.list_devices(_get_connection(target))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@mcp.tool()
|
|
49
|
+
@governed_tool(risk_level="low")
|
|
50
|
+
@tool_errors("dict")
|
|
51
|
+
def prtg_groups(target: Optional[str] = None) -> dict:
|
|
52
|
+
"""[READ] List PRTG device groups with their rollup status.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
target: PRTG target name from config; omit for the default.
|
|
56
|
+
"""
|
|
57
|
+
return ops.list_groups(_get_connection(target))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@mcp.tool()
|
|
61
|
+
@governed_tool(risk_level="low")
|
|
62
|
+
@tool_errors("dict")
|
|
63
|
+
def prtg_history(sensor_id: str, hours: int = 24, target: Optional[str] = None) -> dict:
|
|
64
|
+
"""[READ] Historic data points for one PRTG sensor over the recent window.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
sensor_id: PRTG sensor object id (objid).
|
|
68
|
+
hours: Look-back window in hours (default 24).
|
|
69
|
+
target: PRTG target name from config; omit for the default.
|
|
70
|
+
"""
|
|
71
|
+
return ops.sensor_history(_get_connection(target), sensor_id, hours)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
@governed_tool(risk_level="low")
|
|
76
|
+
@tool_errors("dict")
|
|
77
|
+
def prtg_system_status(target: Optional[str] = None) -> dict:
|
|
78
|
+
"""[READ] System-wide PRTG sensor counters (up/down/warning/paused/alarms).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
target: PRTG target name from config; omit for the default.
|
|
82
|
+
"""
|
|
83
|
+
return ops.system_status(_get_connection(target))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@mcp.tool()
|
|
87
|
+
@governed_tool(risk_level="low")
|
|
88
|
+
@tool_errors("dict")
|
|
89
|
+
def prtg_alarms(target: Optional[str] = None) -> dict:
|
|
90
|
+
"""[READ] Active PRTG alarms — sensors in a down state (status 5).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
target: PRTG target name from config; omit for the default.
|
|
94
|
+
"""
|
|
95
|
+
return ops.list_alarms(_get_connection(target))
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""PRTG governed-write MCP tools: pause / resume / maintenance window."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from monitoring_aiops.governance import governed_tool
|
|
7
|
+
from monitoring_aiops.ops import prtg_write as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _pause_undo(params: dict[str, Any], result: dict[str, Any]) -> dict:
|
|
11
|
+
"""Inverse of pause_sensor: resume the same object (reads result['priorState'])."""
|
|
12
|
+
return {
|
|
13
|
+
"tool": "resume_sensor",
|
|
14
|
+
"params": {"object_id": params.get("object_id")},
|
|
15
|
+
"note": f"resume paused object (priorState={result.get('priorState')})",
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@mcp.tool()
|
|
20
|
+
@governed_tool(risk_level="medium", undo=_pause_undo)
|
|
21
|
+
@tool_errors("dict")
|
|
22
|
+
def pause_sensor(
|
|
23
|
+
object_id: str,
|
|
24
|
+
minutes: int = 0,
|
|
25
|
+
message: str = "paused",
|
|
26
|
+
target: Optional[str] = None,
|
|
27
|
+
) -> dict:
|
|
28
|
+
"""[WRITE][risk=medium] Pause a PRTG object; reversible via resume_sensor.
|
|
29
|
+
|
|
30
|
+
Open-ended when minutes is 0, otherwise auto-resumes after ``minutes``.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
object_id: PRTG object id (sensor/device/group) to pause.
|
|
34
|
+
minutes: Auto-resume after this many minutes; 0 pauses indefinitely.
|
|
35
|
+
message: Pause message shown in PRTG.
|
|
36
|
+
target: Monitoring target name from config; omit for the default.
|
|
37
|
+
"""
|
|
38
|
+
return ops.pause_sensor(_get_connection(target), object_id, minutes, message)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
@governed_tool(risk_level="medium")
|
|
43
|
+
@tool_errors("dict")
|
|
44
|
+
def resume_sensor(object_id: str, target: Optional[str] = None) -> dict:
|
|
45
|
+
"""[WRITE][risk=medium] Resume a paused PRTG object (inverse of pause_sensor).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
object_id: PRTG object id to resume.
|
|
49
|
+
target: Monitoring target name from config; omit for the default.
|
|
50
|
+
"""
|
|
51
|
+
return ops.resume_sensor(_get_connection(target), object_id)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@mcp.tool()
|
|
55
|
+
@governed_tool(risk_level="medium")
|
|
56
|
+
@tool_errors("dict")
|
|
57
|
+
def schedule_maintenance_prtg(object_id: str, minutes: int, target: Optional[str] = None) -> dict:
|
|
58
|
+
"""[WRITE][risk=medium] Time-boxed maintenance window (a pause of ``minutes``).
|
|
59
|
+
|
|
60
|
+
Requires minutes > 0 — maintenance windows are never open-ended.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
object_id: PRTG object id to place in maintenance.
|
|
64
|
+
minutes: Maintenance duration in minutes (must be > 0).
|
|
65
|
+
target: Monitoring target name from config; omit for the default.
|
|
66
|
+
"""
|
|
67
|
+
return ops.schedule_maintenance(_get_connection(target), object_id, minutes)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""SolarWinds (Orion) health MCP tools — all read-only SWQL."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from monitoring_aiops.governance import governed_tool
|
|
7
|
+
from monitoring_aiops.ops import sw_health as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def node_status(name_or_ip: str, target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] One SolarWinds node's health by caption or IP (Orion.Nodes).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name_or_ip: Node caption or IP address to look up.
|
|
18
|
+
target: SolarWinds target name from config; omit for the default.
|
|
19
|
+
"""
|
|
20
|
+
return ops.node_status(_get_connection(target), name_or_ip)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@mcp.tool()
|
|
24
|
+
@governed_tool(risk_level="low")
|
|
25
|
+
@tool_errors("dict")
|
|
26
|
+
def nodes_list(status: Optional[int] = None, target: Optional[str] = None) -> dict:
|
|
27
|
+
"""[READ] List SolarWinds nodes, optionally filtered by status.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
status: Optional Orion status filter (1=Up, 2=Down, 3=Warning).
|
|
31
|
+
target: SolarWinds target name from config; omit for the default.
|
|
32
|
+
"""
|
|
33
|
+
return ops.nodes_list(_get_connection(target), status)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
@governed_tool(risk_level="low")
|
|
38
|
+
@tool_errors("dict")
|
|
39
|
+
def interface_status(top: Optional[int] = None, target: Optional[str] = None) -> dict:
|
|
40
|
+
"""[READ] Interface status + utilization (Orion.NPM.Interfaces).
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
top: If given, return only the top-N interfaces by peak utilization.
|
|
44
|
+
target: SolarWinds target name from config; omit for the default.
|
|
45
|
+
"""
|
|
46
|
+
return ops.interface_status(_get_connection(target), top)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@mcp.tool()
|
|
50
|
+
@governed_tool(risk_level="low")
|
|
51
|
+
@tool_errors("dict")
|
|
52
|
+
def volume_status(min_percent: float = 0, target: Optional[str] = None) -> dict:
|
|
53
|
+
"""[READ] Volumes at/above a fill threshold (Orion.Volumes), worst first.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
min_percent: Minimum percent-used to include (default 0 = all).
|
|
57
|
+
target: SolarWinds target name from config; omit for the default.
|
|
58
|
+
"""
|
|
59
|
+
return ops.volume_status(_get_connection(target), min_percent)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@mcp.tool()
|
|
63
|
+
@governed_tool(risk_level="low")
|
|
64
|
+
@tool_errors("dict")
|
|
65
|
+
def application_status(target: Optional[str] = None) -> dict:
|
|
66
|
+
"""[READ] SAM application status (Orion.APM.Application); resilient if absent.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
target: SolarWinds target name from config; omit for the default.
|
|
70
|
+
"""
|
|
71
|
+
return ops.application_status(_get_connection(target))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@mcp.tool()
|
|
75
|
+
@governed_tool(risk_level="low")
|
|
76
|
+
@tool_errors("dict")
|
|
77
|
+
def topn(metric: str = "cpu", n: int = 10, target: Optional[str] = None) -> list:
|
|
78
|
+
"""[READ] Top-N SolarWinds nodes by a health metric.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
metric: One of cpu, memory, latency, packetloss.
|
|
82
|
+
n: How many nodes to return (default 10).
|
|
83
|
+
target: SolarWinds target name from config; omit for the default.
|
|
84
|
+
"""
|
|
85
|
+
return ops.topn(_get_connection(target), metric, n)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@mcp.tool()
|
|
89
|
+
@governed_tool(risk_level="low")
|
|
90
|
+
@tool_errors("dict")
|
|
91
|
+
def noc_rollup(target: Optional[str] = None) -> dict:
|
|
92
|
+
"""[READ] One-shot NOC glance: down/warning counts + top-3 worst CPU nodes.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
target: SolarWinds target name from config; omit for the default.
|
|
96
|
+
"""
|
|
97
|
+
return ops.noc_rollup(_get_connection(target))
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""SolarWinds suppression / unmanage / maintenance MCP tools (reads + writes).
|
|
2
|
+
|
|
3
|
+
Reversible writes (mute, unmanage) pass an ``undo=`` callback so the harness
|
|
4
|
+
records an inverse descriptor. The two footgun writes — ``unmanage_node`` (masks
|
|
5
|
+
monitoring) and ``remove_node`` (deletes the node) — are risk=high with a
|
|
6
|
+
``dry_run`` preview and require an approver under the graduated-autonomy policy.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
12
|
+
from monitoring_aiops.governance import governed_tool
|
|
13
|
+
from monitoring_aiops.ops import sw_write as ops
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _mute_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
|
|
17
|
+
"""Inverse of mute_alerts: resume alerts for the same entity."""
|
|
18
|
+
if not isinstance(result, dict):
|
|
19
|
+
return None
|
|
20
|
+
return {
|
|
21
|
+
"tool": "unmute_alerts",
|
|
22
|
+
"params": {"entity_uri": params.get("entity_uri")},
|
|
23
|
+
"skill": "monitoring-aiops",
|
|
24
|
+
"note": "Inverse of mute_alerts: resume alerts for the entity.",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _unmanage_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
|
|
29
|
+
"""Inverse of unmanage_node: return the node to managed state."""
|
|
30
|
+
if not isinstance(result, dict):
|
|
31
|
+
return None
|
|
32
|
+
return {
|
|
33
|
+
"tool": "remanage_node",
|
|
34
|
+
"params": {"node_id": params.get("node_id")},
|
|
35
|
+
"skill": "monitoring-aiops",
|
|
36
|
+
"note": "Inverse of unmanage_node: return the node to managed.",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── reads ──────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
@governed_tool(risk_level="low")
|
|
45
|
+
@tool_errors("dict")
|
|
46
|
+
def list_events(top: int = 50, target: Optional[str] = None) -> dict:
|
|
47
|
+
"""[READ] Most recent SolarWinds Orion events (newest first).
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
top: Number of events to return (newest first).
|
|
51
|
+
target: SolarWinds target name from config; omit for the default.
|
|
52
|
+
"""
|
|
53
|
+
return ops.list_events(_get_connection(target), top)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@mcp.tool()
|
|
57
|
+
@governed_tool(risk_level="low")
|
|
58
|
+
@tool_errors("dict")
|
|
59
|
+
def list_unmanaged(target: Optional[str] = None) -> dict:
|
|
60
|
+
"""[READ] Nodes unmanaged now or with a scheduled future unmanage window.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
target: SolarWinds target name from config; omit for the default.
|
|
64
|
+
"""
|
|
65
|
+
return ops.list_unmanaged(_get_connection(target))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@mcp.tool()
|
|
69
|
+
@governed_tool(risk_level="low")
|
|
70
|
+
@tool_errors("dict")
|
|
71
|
+
def list_muted(target: Optional[str] = None) -> dict:
|
|
72
|
+
"""[READ] Objects currently in an alert-suppression (muted) window.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
target: SolarWinds target name from config; omit for the default.
|
|
76
|
+
"""
|
|
77
|
+
return ops.list_muted(_get_connection(target))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── writes ─────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@mcp.tool()
|
|
84
|
+
@governed_tool(risk_level="medium", undo=_mute_undo)
|
|
85
|
+
@tool_errors("dict")
|
|
86
|
+
def mute_alerts(entity_uri: str, minutes: int = 60, target: Optional[str] = None) -> dict:
|
|
87
|
+
"""[WRITE][risk=medium] Time-boxed alert suppression for an entity. Inverse: unmute_alerts.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
entity_uri: SWIS entity URI to suppress (e.g. from list_muted / SWQL).
|
|
91
|
+
minutes: Suppression window length in minutes.
|
|
92
|
+
target: SolarWinds target name from config; omit for the default.
|
|
93
|
+
"""
|
|
94
|
+
return ops.mute_alerts(_get_connection(target), entity_uri, minutes)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@mcp.tool()
|
|
98
|
+
@governed_tool(risk_level="medium")
|
|
99
|
+
@tool_errors("dict")
|
|
100
|
+
def unmute_alerts(entity_uri: str, target: Optional[str] = None) -> dict:
|
|
101
|
+
"""[WRITE][risk=medium] Resume alerts for an entity (lift suppression).
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
entity_uri: SWIS entity URI to resume (from list_muted).
|
|
105
|
+
target: SolarWinds target name from config; omit for the default.
|
|
106
|
+
"""
|
|
107
|
+
return ops.unmute_alerts(_get_connection(target), entity_uri)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@mcp.tool()
|
|
111
|
+
@governed_tool(risk_level="medium")
|
|
112
|
+
@tool_errors("dict")
|
|
113
|
+
def schedule_maintenance(
|
|
114
|
+
node_id: int, start_iso: str, end_iso: str, target: Optional[str] = None
|
|
115
|
+
) -> dict:
|
|
116
|
+
"""[WRITE][risk=medium] Schedule a bounded maintenance window for a node.
|
|
117
|
+
|
|
118
|
+
An end time is REQUIRED — no open-ended maintenance.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
node_id: Numeric Orion NodeID.
|
|
122
|
+
start_iso: Window start (ISO-8601 UTC timestamp).
|
|
123
|
+
end_iso: Window end (ISO-8601 UTC timestamp) — required.
|
|
124
|
+
target: SolarWinds target name from config; omit for the default.
|
|
125
|
+
"""
|
|
126
|
+
return ops.schedule_maintenance(_get_connection(target), node_id, start_iso, end_iso)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@mcp.tool()
|
|
130
|
+
@governed_tool(risk_level="high", undo=_unmanage_undo)
|
|
131
|
+
@tool_errors("dict")
|
|
132
|
+
def unmanage_node(
|
|
133
|
+
node_id: int,
|
|
134
|
+
start_iso: str,
|
|
135
|
+
end_iso: str,
|
|
136
|
+
dry_run: bool = False,
|
|
137
|
+
target: Optional[str] = None,
|
|
138
|
+
) -> dict:
|
|
139
|
+
"""[WRITE][risk=high] Unmanage a node — masks its monitoring. Inverse: remanage_node.
|
|
140
|
+
|
|
141
|
+
Pass dry_run=True to preview. Requires an approver (set
|
|
142
|
+
MONITORING_AUDIT_APPROVED_BY) under the graduated-autonomy policy. Reversible
|
|
143
|
+
→ remanage. An end time is required (no open-ended unmanage).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
node_id: Numeric Orion NodeID.
|
|
147
|
+
start_iso: Unmanage window start (ISO-8601 UTC timestamp).
|
|
148
|
+
end_iso: Unmanage window end (ISO-8601 UTC timestamp) — required.
|
|
149
|
+
dry_run: If True, preview without unmanaging.
|
|
150
|
+
target: SolarWinds target name from config; omit for the default.
|
|
151
|
+
"""
|
|
152
|
+
conn = _get_connection(target)
|
|
153
|
+
if dry_run:
|
|
154
|
+
return {"dryRun": True, "wouldUnmanage": {"nodeId": node_id}}
|
|
155
|
+
return ops.unmanage_node(conn, node_id, start_iso, end_iso)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@mcp.tool()
|
|
159
|
+
@governed_tool(risk_level="medium")
|
|
160
|
+
@tool_errors("dict")
|
|
161
|
+
def remanage_node(node_id: int, target: Optional[str] = None) -> dict:
|
|
162
|
+
"""[WRITE][risk=medium] Return a node to managed state (lift unmanage).
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
node_id: Numeric Orion NodeID.
|
|
166
|
+
target: SolarWinds target name from config; omit for the default.
|
|
167
|
+
"""
|
|
168
|
+
return ops.remanage_node(_get_connection(target), node_id)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@mcp.tool()
|
|
172
|
+
@governed_tool(risk_level="high")
|
|
173
|
+
@tool_errors("dict")
|
|
174
|
+
def remove_node(node_id: int, dry_run: bool = False, target: Optional[str] = None) -> dict:
|
|
175
|
+
"""[WRITE][risk=high] Permanently delete a node from Orion. IRREVERSIBLE — no undo.
|
|
176
|
+
|
|
177
|
+
Pass dry_run=True to preview (reports the node's caption). Requires an
|
|
178
|
+
approver (MONITORING_AUDIT_APPROVED_BY) under the graduated-autonomy policy.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
node_id: Numeric Orion NodeID to delete.
|
|
182
|
+
dry_run: If True, preview without deleting.
|
|
183
|
+
target: SolarWinds target name from config; omit for the default.
|
|
184
|
+
"""
|
|
185
|
+
conn = _get_connection(target)
|
|
186
|
+
if dry_run:
|
|
187
|
+
caption = ops.node_caption(conn, node_id)
|
|
188
|
+
return {"dryRun": True, "wouldRemove": {"nodeId": node_id, "caption": caption}}
|
|
189
|
+
return ops.remove_node(conn, node_id)
|
mcp_server/tools/swql.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""SWQL query MCP tools (SolarWinds, read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from monitoring_aiops.governance import governed_tool
|
|
7
|
+
from monitoring_aiops.ops import swql as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def swql_library(target: Optional[str] = None) -> list:
|
|
14
|
+
"""[READ] List the canned-SWQL library (name + description).
|
|
15
|
+
|
|
16
|
+
These answer the most-repeated SolarWinds SWQL questions directly — run one
|
|
17
|
+
with swql_canned instead of hand-writing the query.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
target: Monitoring target name from config; omit for the default.
|
|
21
|
+
"""
|
|
22
|
+
return ops.list_canned()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@mcp.tool()
|
|
26
|
+
@governed_tool(risk_level="low")
|
|
27
|
+
@tool_errors("dict")
|
|
28
|
+
def swql_canned(name: str, params: Optional[dict] = None, target: Optional[str] = None) -> dict:
|
|
29
|
+
"""[READ] Run a named canned SWQL query (e.g. nodes_down, flapping_interfaces).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
name: Canned query name (from swql_library).
|
|
33
|
+
params: Optional query params (e.g. {"min": 90} for a threshold).
|
|
34
|
+
target: SolarWinds target name from config; omit for the default.
|
|
35
|
+
"""
|
|
36
|
+
return ops.run_canned(_get_connection(target), name, params)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@mcp.tool()
|
|
40
|
+
@governed_tool(risk_level="low")
|
|
41
|
+
@tool_errors("dict")
|
|
42
|
+
def swql_query(query: str, params: Optional[dict] = None, target: Optional[str] = None) -> dict:
|
|
43
|
+
"""[READ] Run a validated read-only SWQL SELECT (row-capped).
|
|
44
|
+
|
|
45
|
+
Only SELECT is permitted — state changes go through the governed write tools.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
query: A SWQL SELECT statement.
|
|
49
|
+
params: Optional named parameters referenced as @name in the query.
|
|
50
|
+
target: SolarWinds target name from config; omit for the default.
|
|
51
|
+
"""
|
|
52
|
+
return ops.run_query(_get_connection(target), query, params)
|