proxy-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 +108 -0
- mcp_server/server.py +40 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/analysis.py +170 -0
- mcp_server/tools/certs.py +29 -0
- mcp_server/tools/configread.py +50 -0
- mcp_server/tools/routes.py +52 -0
- mcp_server/tools/services.py +74 -0
- mcp_server/tools/status.py +47 -0
- mcp_server/tools/traffic.py +36 -0
- mcp_server/tools/undo.py +121 -0
- mcp_server/tools/writes.py +265 -0
- proxy_aiops/__init__.py +14 -0
- proxy_aiops/cli/__init__.py +5 -0
- proxy_aiops/cli/_common.py +78 -0
- proxy_aiops/cli/_root.py +68 -0
- proxy_aiops/cli/analyze.py +81 -0
- proxy_aiops/cli/certs.py +43 -0
- proxy_aiops/cli/configcmd.py +107 -0
- proxy_aiops/cli/doctor.py +21 -0
- proxy_aiops/cli/init.py +166 -0
- proxy_aiops/cli/overview.py +16 -0
- proxy_aiops/cli/routes.py +59 -0
- proxy_aiops/cli/secret.py +105 -0
- proxy_aiops/cli/server.py +74 -0
- proxy_aiops/cli/services.py +54 -0
- proxy_aiops/cli/undo.py +62 -0
- proxy_aiops/config.py +174 -0
- proxy_aiops/connection.py +189 -0
- proxy_aiops/doctor.py +97 -0
- proxy_aiops/governance/__init__.py +40 -0
- proxy_aiops/governance/audit.py +377 -0
- proxy_aiops/governance/budget.py +225 -0
- proxy_aiops/governance/decorators.py +482 -0
- proxy_aiops/governance/paths.py +23 -0
- proxy_aiops/governance/patterns.py +378 -0
- proxy_aiops/governance/policy.py +430 -0
- proxy_aiops/governance/sanitize.py +45 -0
- proxy_aiops/governance/undo.py +218 -0
- proxy_aiops/ops/__init__.py +2 -0
- proxy_aiops/ops/_util.py +86 -0
- proxy_aiops/ops/analysis.py +469 -0
- proxy_aiops/ops/certs.py +146 -0
- proxy_aiops/ops/configread.py +94 -0
- proxy_aiops/ops/overview.py +55 -0
- proxy_aiops/ops/routes.py +209 -0
- proxy_aiops/ops/services.py +304 -0
- proxy_aiops/ops/status.py +73 -0
- proxy_aiops/ops/traffic.py +178 -0
- proxy_aiops/ops/writes.py +174 -0
- proxy_aiops/platform.py +371 -0
- proxy_aiops/secretstore.py +304 -0
- proxy_aiops-0.1.0.dist-info/METADATA +213 -0
- proxy_aiops-0.1.0.dist-info/RECORD +58 -0
- proxy_aiops-0.1.0.dist-info/WHEEL +4 -0
- proxy_aiops-0.1.0.dist-info/entry_points.txt +3 -0
- proxy_aiops-0.1.0.dist-info/licenses/LICENSE +21 -0
mcp_server/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP server package for proxy-aiops."""
|
mcp_server/_shared.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
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 proxy_aiops.config import load_config
|
|
23
|
+
from proxy_aiops.connection import ConnectionManager, ProxyApiError
|
|
24
|
+
from proxy_aiops.governance import sanitize
|
|
25
|
+
from proxy_aiops.platform import UnsupportedOperation
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
_DOCTOR_HINT = "Run 'proxy-aiops doctor' to verify connectivity and credentials."
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _safe_error(exc: Exception, tool: str) -> str:
|
|
33
|
+
"""Return an agent-safe error string; log full detail server-side only."""
|
|
34
|
+
logger.error("Tool %s failed", tool, exc_info=True)
|
|
35
|
+
_passthrough = (
|
|
36
|
+
ValueError,
|
|
37
|
+
FileNotFoundError,
|
|
38
|
+
KeyError,
|
|
39
|
+
PermissionError,
|
|
40
|
+
TimeoutError,
|
|
41
|
+
ConnectionError,
|
|
42
|
+
ProxyApiError,
|
|
43
|
+
UnsupportedOperation,
|
|
44
|
+
)
|
|
45
|
+
if isinstance(exc, _passthrough):
|
|
46
|
+
return sanitize(str(exc), 400)
|
|
47
|
+
return f"{type(exc).__name__}: operation failed."
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def tool_errors(shape: str = "dict") -> Callable:
|
|
51
|
+
"""Wrap a tool body in the canonical try/except → ``_safe_error`` pattern.
|
|
52
|
+
|
|
53
|
+
Place this *between* ``@governed_tool`` and the function so the audit
|
|
54
|
+
decorator and FastMCP still see the original signature.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def decorator(func: Callable) -> Callable:
|
|
58
|
+
name = func.__name__
|
|
59
|
+
|
|
60
|
+
@functools.wraps(func)
|
|
61
|
+
def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
62
|
+
try:
|
|
63
|
+
return func(*args, **kwargs)
|
|
64
|
+
except Exception as e: # noqa: BLE001 — sanitised below
|
|
65
|
+
msg = _safe_error(e, name)
|
|
66
|
+
if shape == "list":
|
|
67
|
+
return [{"error": msg, "hint": _DOCTOR_HINT}]
|
|
68
|
+
if shape == "str":
|
|
69
|
+
return f"Error: {msg} {_DOCTOR_HINT}"
|
|
70
|
+
return {"error": msg, "hint": _DOCTOR_HINT}
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
return decorator
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
mcp = FastMCP(
|
|
78
|
+
"proxy-aiops",
|
|
79
|
+
instructions=(
|
|
80
|
+
"Reverse-proxy / load-balancer operations (preview) over Traefik, Caddy "
|
|
81
|
+
"and HAProxy: version/entrypoints, routes (routers / caddy routes / "
|
|
82
|
+
"frontends), services and server-level upstream health, middlewares, "
|
|
83
|
+
"TLS certificate inventory, traffic/error counters, and config "
|
|
84
|
+
"snapshot/search. Flagship analyses: backend_health_rca, "
|
|
85
|
+
"cert_expiry_sweep, error_rate_rca, route_conflict_analysis — "
|
|
86
|
+
"transparent heuristics that show their numbers. Governed writes — "
|
|
87
|
+
"caddy config set/delete/load (prior-config captured, undo-recorded) "
|
|
88
|
+
"and haproxy runtime server state (ready/drain/maint) and weight "
|
|
89
|
+
"(reversible) — with a dry_run preview; delete/load are risk=high with "
|
|
90
|
+
"an approver. A per-target 'platform' field selects the API shape, and "
|
|
91
|
+
"an explicit support matrix raises teaching errors for ops a platform "
|
|
92
|
+
"cannot do (e.g. traefik writes belong to its providers). Every tool "
|
|
93
|
+
"runs through the proxy-aiops governance harness (audit / budget / "
|
|
94
|
+
"risk-tier / undo). Do NOT use for firewall rules — use firewall-aiops."
|
|
95
|
+
),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
_conn_mgr: Optional[ConnectionManager] = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _get_connection(target: Optional[str] = None) -> Any:
|
|
102
|
+
"""Return a proxy connection, lazily initialising the manager."""
|
|
103
|
+
global _conn_mgr # noqa: PLW0603
|
|
104
|
+
if _conn_mgr is None:
|
|
105
|
+
config_path_str = os.environ.get("PROXY_AIOPS_CONFIG")
|
|
106
|
+
config_path = Path(config_path_str) if config_path_str else None
|
|
107
|
+
_conn_mgr = ConnectionManager(load_config(config_path))
|
|
108
|
+
return _conn_mgr.connect(target)
|
mcp_server/server.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""MCP server wrapping proxy-aiops operations (stdio transport).
|
|
2
|
+
|
|
3
|
+
Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
|
|
4
|
+
delegates to the ``proxy_aiops`` ops package and is wrapped with the
|
|
5
|
+
proxy-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
|
|
6
|
+
|
|
7
|
+
Standalone, self-governed reverse-proxy operations (preview) over Traefik,
|
|
8
|
+
Caddy and HAProxy: status/routes/services/upstreams/certs/traffic/config
|
|
9
|
+
reads, four flagship analyses, and governed writes (caddy config set/delete/
|
|
10
|
+
load, haproxy server state/weight).
|
|
11
|
+
|
|
12
|
+
Source: https://github.com/AIops-tools/Proxy-AIops
|
|
13
|
+
License: MIT
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from mcp_server._shared import _safe_error, mcp, tool_errors
|
|
19
|
+
|
|
20
|
+
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
21
|
+
# `mcp` instance. Order does not matter; each module is self-contained.
|
|
22
|
+
from mcp_server.tools import ( # noqa: F401 — side effects
|
|
23
|
+
analysis,
|
|
24
|
+
certs,
|
|
25
|
+
configread,
|
|
26
|
+
routes,
|
|
27
|
+
services,
|
|
28
|
+
status,
|
|
29
|
+
traffic,
|
|
30
|
+
undo,
|
|
31
|
+
writes,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
__all__ = ["mcp", "main", "_safe_error", "tool_errors"]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
"""Run the MCP server over stdio."""
|
|
39
|
+
logging.basicConfig(level=logging.INFO)
|
|
40
|
+
mcp.run(transport="stdio")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""MCP tool modules — importing each registers its @mcp.tool() functions."""
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Flagship proxy-analysis MCP tools (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import analysis as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def backend_health_rca(
|
|
14
|
+
service: Optional[str] = None,
|
|
15
|
+
upstreams: Optional[list[dict[str, Any]]] = None,
|
|
16
|
+
target: Optional[str] = None,
|
|
17
|
+
) -> dict:
|
|
18
|
+
"""[READ] Down upstreams grouped per service, each mapped to cause + action.
|
|
19
|
+
|
|
20
|
+
The flagship availability RCA: pulls server-level upstream health, groups
|
|
21
|
+
per service, and classifies each outage/degradation (connection refused,
|
|
22
|
+
L4/L7 health-check failure class, DNS, admin maint/drain, all-servers-down)
|
|
23
|
+
with a recommended action. Every finding carries its numbers. Pass
|
|
24
|
+
'upstreams' for pure analysis, or a target to pull live.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
service: Optional service/backend filter when pulling live.
|
|
28
|
+
upstreams: Injected rows {service, server, address, status, checkInfo};
|
|
29
|
+
skips the live pull.
|
|
30
|
+
target: Proxy target name from config; omit for the default.
|
|
31
|
+
|
|
32
|
+
Returns dict: {servicesEvaluated, outages, degraded, findings:[{service,
|
|
33
|
+
serversTotal, up, down, maint, failingServers, cause, action}], note}.
|
|
34
|
+
"""
|
|
35
|
+
if upstreams is None:
|
|
36
|
+
from proxy_aiops.ops import services as svc_ops
|
|
37
|
+
|
|
38
|
+
pulled = svc_ops.list_upstreams(_get_connection(target), service)
|
|
39
|
+
if "error" in pulled:
|
|
40
|
+
return pulled
|
|
41
|
+
upstreams = pulled.get("upstreams", [])
|
|
42
|
+
return ops.backend_health_rca(upstreams)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@mcp.tool()
|
|
46
|
+
@governed_tool(risk_level="low")
|
|
47
|
+
@tool_errors("dict")
|
|
48
|
+
def cert_expiry_sweep(
|
|
49
|
+
warn_days: float = 30.0,
|
|
50
|
+
critical_days: float = 7.0,
|
|
51
|
+
port: int = 443,
|
|
52
|
+
certs: Optional[list[dict[str, Any]]] = None,
|
|
53
|
+
target: Optional[str] = None,
|
|
54
|
+
) -> dict:
|
|
55
|
+
"""[READ] TLS cert inventory bucketed by days-to-expiry + renewal hints.
|
|
56
|
+
|
|
57
|
+
The flagship cert sweep for traefik/caddy targets: collects the TLS domain
|
|
58
|
+
inventory, live-probes each domain's served leaf cert (bounded handshake),
|
|
59
|
+
and buckets by expiry: expired / critical / warning / ok — with a
|
|
60
|
+
platform-specific renewal hint (ACME resolver / storage checks). Pass
|
|
61
|
+
'certs' for pure analysis over {domain, daysToExpiry} rows. haproxy targets
|
|
62
|
+
return the support matrix's teaching note.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
warn_days: Days-to-expiry at/below which a cert is a warning (default 30).
|
|
66
|
+
critical_days: Days at/below which a cert is critical (default 7).
|
|
67
|
+
port: TLS port to probe (default 443).
|
|
68
|
+
certs: Injected rows {domain, daysToExpiry, notAfter?}; skips pull+probe.
|
|
69
|
+
target: Proxy target name from config; omit for the default.
|
|
70
|
+
|
|
71
|
+
Returns dict: {certsEvaluated, expired, critical, warning, ok, unknown,
|
|
72
|
+
certificates (soonest first), thresholds, renewalHint, note}.
|
|
73
|
+
"""
|
|
74
|
+
platform = ""
|
|
75
|
+
if certs is None:
|
|
76
|
+
from proxy_aiops.ops import certs as cert_ops
|
|
77
|
+
|
|
78
|
+
conn = _get_connection(target)
|
|
79
|
+
platform = conn.target.platform
|
|
80
|
+
pulled = cert_ops.list_certificates(conn, probe=True, port=port)
|
|
81
|
+
if "unsupported" in pulled or "error" in pulled:
|
|
82
|
+
return pulled
|
|
83
|
+
certs = pulled.get("certificates", [])
|
|
84
|
+
return ops.cert_expiry_sweep(
|
|
85
|
+
certs, warn_days=warn_days, critical_days=critical_days, platform=platform
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@mcp.tool()
|
|
90
|
+
@governed_tool(risk_level="low")
|
|
91
|
+
@tool_errors("dict")
|
|
92
|
+
def error_rate_rca(
|
|
93
|
+
error_rate_pct: float = 5.0,
|
|
94
|
+
min_requests: float = 30.0,
|
|
95
|
+
counters: Optional[list[dict[str, Any]]] = None,
|
|
96
|
+
target: Optional[str] = None,
|
|
97
|
+
) -> dict:
|
|
98
|
+
"""[READ] Rank services by 5xx share vs the fleet baseline, cause + action.
|
|
99
|
+
|
|
100
|
+
The flagship error RCA: reads per-service status-code counters (traefik
|
|
101
|
+
/metrics, haproxy stats), flags services whose 5xx rate crosses the
|
|
102
|
+
threshold with enough traffic, and maps the dominant code to a cause
|
|
103
|
+
(503 no-upstream / 502 conn-fail / 504 timeout / 500 app error). Every
|
|
104
|
+
entry carries its numbers and its multiple vs the fleet baseline. Pass
|
|
105
|
+
'counters' for pure analysis. caddy targets return the support matrix's
|
|
106
|
+
teaching note (no per-route counters).
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
error_rate_pct: 5xx %% at/above which a service is flagged (default 5.0).
|
|
110
|
+
min_requests: Minimum requests before a service can be flagged (default 30).
|
|
111
|
+
counters: Injected rows {service, total, codes:{...}} and/or
|
|
112
|
+
classes:{"5xx": n}; skips the live pull.
|
|
113
|
+
target: Proxy target name from config; omit for the default.
|
|
114
|
+
|
|
115
|
+
Returns dict: {servicesEvaluated, flaggedCount, fleetErrorRatePct,
|
|
116
|
+
thresholds, flagged:[{service, requestsTotal, errors5xx, errorRatePct,
|
|
117
|
+
dominantCode, vsBaselineX, severity, cause, action}], note}.
|
|
118
|
+
"""
|
|
119
|
+
if counters is None:
|
|
120
|
+
from proxy_aiops.ops import traffic as traffic_ops
|
|
121
|
+
|
|
122
|
+
pulled = traffic_ops.error_counters(_get_connection(target))
|
|
123
|
+
if "error" in pulled:
|
|
124
|
+
return pulled
|
|
125
|
+
counters = pulled.get("services", [])
|
|
126
|
+
return ops.error_rate_rca(
|
|
127
|
+
counters, error_rate_pct=error_rate_pct, min_requests=min_requests
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@mcp.tool()
|
|
132
|
+
@governed_tool(risk_level="low")
|
|
133
|
+
@tool_errors("dict")
|
|
134
|
+
def route_conflict_analysis(
|
|
135
|
+
routes: Optional[list[dict[str, Any]]] = None,
|
|
136
|
+
services: Optional[list[dict[str, Any]]] = None,
|
|
137
|
+
target: Optional[str] = None,
|
|
138
|
+
) -> dict:
|
|
139
|
+
"""[READ] Shadowed routes, dead routes, and redirect loops (static).
|
|
140
|
+
|
|
141
|
+
The flagship routing hygiene analysis: fetches the route table and service
|
|
142
|
+
list and statically finds (1) routes fully covered by an earlier/higher-
|
|
143
|
+
priority route (they can never match), (2) routes pointing at a missing
|
|
144
|
+
service or one with zero servers up, and (3) redirect chains that loop.
|
|
145
|
+
Every finding names the covering route / missing service. Pass 'routes'
|
|
146
|
+
(and optionally 'services') for pure analysis.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
routes: Injected rows {name, hosts, paths, priority, service, enabled,
|
|
150
|
+
redirectTo}; skips the live pull.
|
|
151
|
+
services: Injected rows {name, serversTotal, serversUp} for dead-route
|
|
152
|
+
detection.
|
|
153
|
+
target: Proxy target name from config; omit for the default.
|
|
154
|
+
|
|
155
|
+
Returns dict: {routesEvaluated, shadowedCount, deadCount,
|
|
156
|
+
redirectLoopCount, shadowedRoutes, deadRoutes, redirectLoops, note}.
|
|
157
|
+
"""
|
|
158
|
+
if routes is None:
|
|
159
|
+
from proxy_aiops.ops import routes as route_ops
|
|
160
|
+
from proxy_aiops.ops import services as svc_ops
|
|
161
|
+
|
|
162
|
+
conn = _get_connection(target)
|
|
163
|
+
pulled = route_ops.list_routes(conn)
|
|
164
|
+
if "error" in pulled:
|
|
165
|
+
return pulled
|
|
166
|
+
routes = pulled.get("routes", [])
|
|
167
|
+
if services is None:
|
|
168
|
+
pulled_svcs = svc_ops.list_services(conn)
|
|
169
|
+
services = pulled_svcs.get("services", []) if "error" not in pulled_svcs else None
|
|
170
|
+
return ops.route_conflict_analysis(routes, services)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""TLS certificate MCP tools (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import certs as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def list_certificates(
|
|
14
|
+
probe: bool = False,
|
|
15
|
+
port: int = 443,
|
|
16
|
+
target: Optional[str] = None,
|
|
17
|
+
) -> dict:
|
|
18
|
+
"""[READ] TLS domain inventory for a traefik/caddy target; optionally
|
|
19
|
+
handshake-probe each domain for its live expiry.
|
|
20
|
+
|
|
21
|
+
On haproxy this returns the support matrix's teaching note (certs are .pem
|
|
22
|
+
files in haproxy.cfg).
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
probe: If True, TLS-handshake each domain (bounded) to read expiry.
|
|
26
|
+
port: TLS port to probe (default 443).
|
|
27
|
+
target: Proxy target name from config; omit for the default.
|
|
28
|
+
"""
|
|
29
|
+
return ops.list_certificates(_get_connection(target), probe=probe, port=port)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Config-tree MCP tools (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import configread as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def config_snapshot(target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] The live config tree (caddy /config/) or merged dynamic state
|
|
15
|
+
(traefik /api/rawdata), sanitised and bounded. haproxy returns the support
|
|
16
|
+
matrix's teaching note.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
target: Proxy target name from config; omit for the default.
|
|
20
|
+
"""
|
|
21
|
+
return ops.config_snapshot(_get_connection(target))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@mcp.tool()
|
|
25
|
+
@governed_tool(risk_level="low")
|
|
26
|
+
@tool_errors("dict")
|
|
27
|
+
def search_config(query: str, target: Optional[str] = None) -> dict:
|
|
28
|
+
"""[READ] Search the config tree for a string; returns matching config
|
|
29
|
+
paths (on caddy, directly usable by get/set_config_value).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
query: Case-insensitive substring to find in keys and values.
|
|
33
|
+
target: Proxy target name from config; omit for the default.
|
|
34
|
+
"""
|
|
35
|
+
return ops.search_config(_get_connection(target), query)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@mcp.tool()
|
|
39
|
+
@governed_tool(risk_level="low")
|
|
40
|
+
@tool_errors("dict")
|
|
41
|
+
def get_config_value(path: str, target: Optional[str] = None) -> dict:
|
|
42
|
+
"""[READ] One value out of the caddy config tree by config path
|
|
43
|
+
(e.g. apps/http/servers/srv0/routes/0). Off-caddy platforms return the
|
|
44
|
+
support matrix's teaching note.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: Slash-separated config path (dot-segments rejected).
|
|
48
|
+
target: Proxy target name from config; omit for the default.
|
|
49
|
+
"""
|
|
50
|
+
return ops.get_config_value(_get_connection(target), path)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Route MCP tools — routers / caddy routes / frontends (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import routes as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def list_routes(host: Optional[str] = None, target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] Routes normalised across platforms: {name, hosts, paths,
|
|
15
|
+
priority, service, tls, enabled, redirectTo}.
|
|
16
|
+
|
|
17
|
+
Traefik router names and caddy config paths (apps/http/servers/...) come
|
|
18
|
+
back exactly as the other tools expect them.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
host: Optional hostname filter (keeps host-less catch-alls).
|
|
22
|
+
target: Proxy target name from config; omit for the default.
|
|
23
|
+
"""
|
|
24
|
+
return ops.list_routes(_get_connection(target), host)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@mcp.tool()
|
|
28
|
+
@governed_tool(risk_level="low")
|
|
29
|
+
@tool_errors("dict")
|
|
30
|
+
def route_detail(name: str, target: Optional[str] = None) -> dict:
|
|
31
|
+
"""[READ] One route's full detail by name (from list_routes).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Route name — traefik router name, caddy route config path, or
|
|
35
|
+
haproxy frontend name.
|
|
36
|
+
target: Proxy target name from config; omit for the default.
|
|
37
|
+
"""
|
|
38
|
+
return ops.route_detail(_get_connection(target), name)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@mcp.tool()
|
|
42
|
+
@governed_tool(risk_level="low")
|
|
43
|
+
@tool_errors("dict")
|
|
44
|
+
def find_route(host: str, path: str = "/", target: Optional[str] = None) -> dict:
|
|
45
|
+
"""[READ] Which routes would serve a host/path (static match, best first).
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
host: Hostname to match (e.g. app.example.com).
|
|
49
|
+
path: Request path to match (default /).
|
|
50
|
+
target: Proxy target name from config; omit for the default.
|
|
51
|
+
"""
|
|
52
|
+
return ops.find_route(_get_connection(target), host, path)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Service / upstream MCP tools (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import services as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def list_services(target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] Services / backends with per-service server-up counts.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
target: Proxy target name from config; omit for the default.
|
|
18
|
+
"""
|
|
19
|
+
return ops.list_services(_get_connection(target))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@mcp.tool()
|
|
23
|
+
@governed_tool(risk_level="low")
|
|
24
|
+
@tool_errors("dict")
|
|
25
|
+
def service_detail(name: str, target: Optional[str] = None) -> dict:
|
|
26
|
+
"""[READ] One service/backend's full detail by name (from list_services).
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
name: Service name — traefik service name, caddy route config path, or
|
|
30
|
+
haproxy backend name.
|
|
31
|
+
target: Proxy target name from config; omit for the default.
|
|
32
|
+
"""
|
|
33
|
+
return ops.service_detail(_get_connection(target), name)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@mcp.tool()
|
|
37
|
+
@governed_tool(risk_level="low")
|
|
38
|
+
@tool_errors("dict")
|
|
39
|
+
def list_upstreams(service: Optional[str] = None, target: Optional[str] = None) -> dict:
|
|
40
|
+
"""[READ] Server-level upstream health rows: {service, server, address,
|
|
41
|
+
status(up/down/maint/drain), checkInfo, weight}. Feeds backend_health_rca.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
service: Optional service/backend filter.
|
|
45
|
+
target: Proxy target name from config; omit for the default.
|
|
46
|
+
"""
|
|
47
|
+
return ops.list_upstreams(_get_connection(target), service)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@mcp.tool()
|
|
51
|
+
@governed_tool(risk_level="low")
|
|
52
|
+
@tool_errors("dict")
|
|
53
|
+
def upstream_detail(service: str, server: str, target: Optional[str] = None) -> dict:
|
|
54
|
+
"""[READ] One upstream server's health/state row.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
service: Service/backend name.
|
|
58
|
+
server: Server name or address (from list_upstreams).
|
|
59
|
+
target: Proxy target name from config; omit for the default.
|
|
60
|
+
"""
|
|
61
|
+
return ops.upstream_detail(_get_connection(target), service, server)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@mcp.tool()
|
|
65
|
+
@governed_tool(risk_level="low")
|
|
66
|
+
@tool_errors("dict")
|
|
67
|
+
def list_middlewares(target: Optional[str] = None) -> dict:
|
|
68
|
+
"""[READ] Middlewares (traefik). On caddy/haproxy this returns the support
|
|
69
|
+
matrix's teaching note (their equivalents live inside routes / haproxy.cfg).
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
target: Proxy target name from config; omit for the default.
|
|
73
|
+
"""
|
|
74
|
+
return ops.list_middlewares(_get_connection(target))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Status MCP tools — overview, version, entrypoints (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import overview as overview_ops
|
|
8
|
+
from proxy_aiops.ops import status as ops
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@mcp.tool()
|
|
12
|
+
@governed_tool(risk_level="low")
|
|
13
|
+
@tool_errors("dict")
|
|
14
|
+
def proxy_overview(target: Optional[str] = None) -> dict:
|
|
15
|
+
"""[READ] One-shot summary: platform/version + route/service counts +
|
|
16
|
+
upstream up/down health.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
target: Proxy target name from config; omit for the default.
|
|
20
|
+
"""
|
|
21
|
+
return overview_ops.proxy_overview(_get_connection(target))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@mcp.tool()
|
|
25
|
+
@governed_tool(risk_level="low")
|
|
26
|
+
@tool_errors("dict")
|
|
27
|
+
def version_info(target: Optional[str] = None) -> dict:
|
|
28
|
+
"""[READ] Version / build info (traefik /api/version, haproxy /v2/info;
|
|
29
|
+
caddy returns a teaching note — its admin API has no version endpoint).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
target: Proxy target name from config; omit for the default.
|
|
33
|
+
"""
|
|
34
|
+
return ops.version_info(_get_connection(target))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@mcp.tool()
|
|
38
|
+
@governed_tool(risk_level="low")
|
|
39
|
+
@tool_errors("dict")
|
|
40
|
+
def list_entrypoints(target: Optional[str] = None) -> dict:
|
|
41
|
+
"""[READ] Listeners: traefik entrypoints / caddy server listen addresses /
|
|
42
|
+
haproxy frontends — where traffic enters this proxy.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
target: Proxy target name from config; omit for the default.
|
|
46
|
+
"""
|
|
47
|
+
return ops.list_entrypoints(_get_connection(target))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Traffic / error-counter MCP tools (read-only)."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
6
|
+
from proxy_aiops.governance import governed_tool
|
|
7
|
+
from proxy_aiops.ops import traffic as ops
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@mcp.tool()
|
|
11
|
+
@governed_tool(risk_level="low")
|
|
12
|
+
@tool_errors("dict")
|
|
13
|
+
def traffic_stats(target: Optional[str] = None) -> dict:
|
|
14
|
+
"""[READ] Per-service traffic snapshot: requests, latency/rate/sessions
|
|
15
|
+
where the platform exposes it (traefik /metrics, haproxy stats; caddy
|
|
16
|
+
returns the support matrix's teaching note).
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
target: Proxy target name from config; omit for the default.
|
|
20
|
+
"""
|
|
21
|
+
return ops.traffic_stats(_get_connection(target))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@mcp.tool()
|
|
25
|
+
@governed_tool(risk_level="low")
|
|
26
|
+
@tool_errors("dict")
|
|
27
|
+
def error_counters(target: Optional[str] = None) -> dict:
|
|
28
|
+
"""[READ] Per-service request/status-code counters (feeds error_rate_rca).
|
|
29
|
+
|
|
30
|
+
traefik: parsed from the /metrics text endpoint (per-code); haproxy: Data Plane
|
|
31
|
+
stats (per-class hrsp_*); caddy: teaching note (no per-route counters).
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
target: Proxy target name from config; omit for the default.
|
|
35
|
+
"""
|
|
36
|
+
return ops.error_counters(_get_connection(target))
|