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.
Files changed (47) hide show
  1. mcp_server/__init__.py +1 -0
  2. mcp_server/_shared.py +101 -0
  3. mcp_server/server.py +36 -0
  4. mcp_server/tools/__init__.py +1 -0
  5. mcp_server/tools/alerts.py +35 -0
  6. mcp_server/tools/prtg.py +95 -0
  7. mcp_server/tools/prtg_write.py +67 -0
  8. mcp_server/tools/sw_health.py +97 -0
  9. mcp_server/tools/sw_write.py +189 -0
  10. mcp_server/tools/swql.py +52 -0
  11. monitoring_aiops/__init__.py +9 -0
  12. monitoring_aiops/cli/__init__.py +9 -0
  13. monitoring_aiops/cli/_common.py +78 -0
  14. monitoring_aiops/cli/_root.py +57 -0
  15. monitoring_aiops/cli/alert.py +51 -0
  16. monitoring_aiops/cli/doctor.py +21 -0
  17. monitoring_aiops/cli/init.py +118 -0
  18. monitoring_aiops/cli/overview.py +16 -0
  19. monitoring_aiops/cli/secret.py +105 -0
  20. monitoring_aiops/cli/swql.py +51 -0
  21. monitoring_aiops/config.py +148 -0
  22. monitoring_aiops/connection.py +188 -0
  23. monitoring_aiops/doctor.py +91 -0
  24. monitoring_aiops/governance/__init__.py +40 -0
  25. monitoring_aiops/governance/audit.py +377 -0
  26. monitoring_aiops/governance/budget.py +225 -0
  27. monitoring_aiops/governance/decorators.py +474 -0
  28. monitoring_aiops/governance/paths.py +23 -0
  29. monitoring_aiops/governance/patterns.py +378 -0
  30. monitoring_aiops/governance/policy.py +411 -0
  31. monitoring_aiops/governance/sanitize.py +39 -0
  32. monitoring_aiops/governance/undo.py +218 -0
  33. monitoring_aiops/ops/__init__.py +1 -0
  34. monitoring_aiops/ops/_util.py +32 -0
  35. monitoring_aiops/ops/alerts.py +79 -0
  36. monitoring_aiops/ops/overview.py +29 -0
  37. monitoring_aiops/ops/prtg.py +145 -0
  38. monitoring_aiops/ops/prtg_write.py +59 -0
  39. monitoring_aiops/ops/sw_health.py +199 -0
  40. monitoring_aiops/ops/sw_write.py +179 -0
  41. monitoring_aiops/ops/swql.py +95 -0
  42. monitoring_aiops/secretstore.py +302 -0
  43. monitoring_aiops-0.1.0.dist-info/METADATA +114 -0
  44. monitoring_aiops-0.1.0.dist-info/RECORD +47 -0
  45. monitoring_aiops-0.1.0.dist-info/WHEEL +4 -0
  46. monitoring_aiops-0.1.0.dist-info/entry_points.txt +3 -0
  47. 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)
@@ -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)
@@ -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)