postgres-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 (57) hide show
  1. mcp_server/__init__.py +1 -0
  2. mcp_server/_shared.py +101 -0
  3. mcp_server/server.py +37 -0
  4. mcp_server/tools/__init__.py +1 -0
  5. mcp_server/tools/activity.py +52 -0
  6. mcp_server/tools/analysis.py +89 -0
  7. mcp_server/tools/indexes.py +65 -0
  8. mcp_server/tools/queries.py +60 -0
  9. mcp_server/tools/remediation.py +281 -0
  10. mcp_server/tools/replication.py +43 -0
  11. mcp_server/tools/server.py +86 -0
  12. mcp_server/tools/tables.py +46 -0
  13. postgres_aiops/__init__.py +9 -0
  14. postgres_aiops/cli/__init__.py +9 -0
  15. postgres_aiops/cli/_common.py +78 -0
  16. postgres_aiops/cli/_root.py +68 -0
  17. postgres_aiops/cli/activity.py +52 -0
  18. postgres_aiops/cli/analyze.py +53 -0
  19. postgres_aiops/cli/doctor.py +21 -0
  20. postgres_aiops/cli/index.py +55 -0
  21. postgres_aiops/cli/init.py +112 -0
  22. postgres_aiops/cli/overview.py +16 -0
  23. postgres_aiops/cli/query.py +70 -0
  24. postgres_aiops/cli/remediate.py +186 -0
  25. postgres_aiops/cli/replication.py +45 -0
  26. postgres_aiops/cli/secret.py +103 -0
  27. postgres_aiops/cli/server.py +69 -0
  28. postgres_aiops/cli/table.py +45 -0
  29. postgres_aiops/config.py +154 -0
  30. postgres_aiops/connection.py +178 -0
  31. postgres_aiops/doctor.py +84 -0
  32. postgres_aiops/governance/__init__.py +40 -0
  33. postgres_aiops/governance/audit.py +377 -0
  34. postgres_aiops/governance/budget.py +225 -0
  35. postgres_aiops/governance/decorators.py +474 -0
  36. postgres_aiops/governance/paths.py +23 -0
  37. postgres_aiops/governance/patterns.py +378 -0
  38. postgres_aiops/governance/policy.py +411 -0
  39. postgres_aiops/governance/sanitize.py +39 -0
  40. postgres_aiops/governance/undo.py +218 -0
  41. postgres_aiops/ops/__init__.py +1 -0
  42. postgres_aiops/ops/_util.py +102 -0
  43. postgres_aiops/ops/activity.py +193 -0
  44. postgres_aiops/ops/analysis.py +263 -0
  45. postgres_aiops/ops/indexes.py +211 -0
  46. postgres_aiops/ops/overview.py +51 -0
  47. postgres_aiops/ops/queries.py +123 -0
  48. postgres_aiops/ops/remediation.py +237 -0
  49. postgres_aiops/ops/replication.py +144 -0
  50. postgres_aiops/ops/server.py +151 -0
  51. postgres_aiops/ops/tables.py +146 -0
  52. postgres_aiops/secretstore.py +302 -0
  53. postgres_aiops-0.1.0.dist-info/METADATA +119 -0
  54. postgres_aiops-0.1.0.dist-info/RECORD +57 -0
  55. postgres_aiops-0.1.0.dist-info/WHEEL +4 -0
  56. postgres_aiops-0.1.0.dist-info/entry_points.txt +3 -0
  57. postgres_aiops-0.1.0.dist-info/licenses/LICENSE +21 -0
mcp_server/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """MCP server package for postgres-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 postgres_aiops.config import load_config
23
+ from postgres_aiops.connection import ConnectionManager, PgError
24
+ from postgres_aiops.governance import sanitize
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ _DOCTOR_HINT = "Run 'postgres-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
+ PgError,
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
+ "postgres-aiops",
77
+ instructions=(
78
+ "Governed PostgreSQL DBA operations (preview): a one-shot cluster "
79
+ "'overview'; server reads (version/settings/extensions/databases/roles); "
80
+ "activity (sessions, long-running queries, locks); query stats "
81
+ "(pg_stat_statements top-N, EXPLAIN); index and table health (unused / "
82
+ "missing / bloat / autovacuum); replication (lag, slots, WAL); three "
83
+ "flagship analyses — 'slow_query_rca', 'bloat_and_vacuum_analysis', and "
84
+ "'blocking_lock_chain_rca'; and guarded writes (terminate/cancel, "
85
+ "vacuum/analyze, create/drop index, reindex, ALTER SYSTEM). Every tool "
86
+ "runs through the postgres-aiops governance harness (audit / budget / "
87
+ "risk-tier / undo). Do NOT use for OT/industrial edge — see industrial-aiops."
88
+ ),
89
+ )
90
+
91
+ _conn_mgr: Optional[ConnectionManager] = None
92
+
93
+
94
+ def _get_connection(target: Optional[str] = None) -> Any:
95
+ """Return a PostgreSQL connection, lazily initialising the manager."""
96
+ global _conn_mgr # noqa: PLW0603
97
+ if _conn_mgr is None:
98
+ config_path_str = os.environ.get("POSTGRES_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,37 @@
1
+ """MCP server wrapping postgres-aiops operations (stdio transport).
2
+
3
+ Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
4
+ delegates to the ``postgres_aiops`` ops package and is wrapped with the
5
+ postgres-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
6
+
7
+ Standalone, self-governed PostgreSQL DBA operations (preview).
8
+ For PostgreSQL servers/clusters via psycopg 3.
9
+
10
+ Source: https://github.com/AIops-tools/Postgres-AIops
11
+ License: MIT
12
+ """
13
+
14
+ import logging
15
+
16
+ from mcp_server._shared import _safe_error, mcp, tool_errors
17
+
18
+ # Importing the tool modules registers every @mcp.tool() onto the shared
19
+ # `mcp` instance. Order does not matter; each module is self-contained.
20
+ from mcp_server.tools import ( # noqa: F401 — side effects
21
+ activity,
22
+ analysis,
23
+ indexes,
24
+ queries,
25
+ remediation,
26
+ replication,
27
+ server,
28
+ tables,
29
+ )
30
+
31
+ __all__ = ["mcp", "main", "_safe_error", "tool_errors"]
32
+
33
+
34
+ def main() -> None:
35
+ """Run the MCP server over stdio."""
36
+ logging.basicConfig(level=logging.INFO)
37
+ mcp.run(transport="stdio")
@@ -0,0 +1 @@
1
+ """MCP tool modules. Importing each registers its @mcp.tool() functions."""
@@ -0,0 +1,52 @@
1
+ """Activity PostgreSQL MCP tools (read-only): sessions, long queries, locks."""
2
+
3
+ from typing import Optional
4
+
5
+ from mcp_server._shared import _get_connection, mcp, tool_errors
6
+ from postgres_aiops.governance import governed_tool
7
+ from postgres_aiops.ops import activity as ops
8
+
9
+
10
+ @mcp.tool()
11
+ @governed_tool(risk_level="low")
12
+ @tool_errors("dict")
13
+ def list_activity(
14
+ state: Optional[str] = None,
15
+ include_idle: bool = True,
16
+ target: Optional[str] = None,
17
+ ) -> dict:
18
+ """[READ] Current sessions (pg_stat_activity) with per-state counts.
19
+
20
+ Flags idle-in-transaction backends (open transactions holding resources).
21
+
22
+ Args:
23
+ state: Optional exact state filter (active, idle, 'idle in transaction').
24
+ include_idle: Include plain idle backends (default True).
25
+ target: Target name from config; omit for the default.
26
+ """
27
+ return ops.list_activity(_get_connection(target), state=state, include_idle=include_idle)
28
+
29
+
30
+ @mcp.tool()
31
+ @governed_tool(risk_level="low")
32
+ @tool_errors("dict")
33
+ def long_running_queries(min_seconds: int = 60, target: Optional[str] = None) -> dict:
34
+ """[READ] Active queries running at least ``min_seconds``, oldest first.
35
+
36
+ Args:
37
+ min_seconds: Minimum age in seconds (default 60).
38
+ target: Target name from config; omit for the default.
39
+ """
40
+ return ops.long_running_queries(_get_connection(target), min_seconds=min_seconds)
41
+
42
+
43
+ @mcp.tool()
44
+ @governed_tool(risk_level="low")
45
+ @tool_errors("dict")
46
+ def list_locks(target: Optional[str] = None) -> dict:
47
+ """[READ] Held/awaited locks joined to their owning backend and object.
48
+
49
+ Args:
50
+ target: Target name from config; omit for the default.
51
+ """
52
+ return ops.list_locks(_get_connection(target))
@@ -0,0 +1,89 @@
1
+ """Flagship PostgreSQL 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 postgres_aiops.governance import governed_tool
7
+ from postgres_aiops.ops import activity as activity_ops
8
+ from postgres_aiops.ops import analysis as ops
9
+ from postgres_aiops.ops import queries as query_ops
10
+ from postgres_aiops.ops import tables as table_ops
11
+
12
+
13
+ @mcp.tool()
14
+ @governed_tool(risk_level="low")
15
+ @tool_errors("dict")
16
+ def slow_query_rca(
17
+ statements: Optional[list[dict[str, Any]]] = None,
18
+ explain_sql: Optional[str] = None,
19
+ limit: int = 20,
20
+ target: Optional[str] = None,
21
+ ) -> dict:
22
+ """[READ] RCA for the worst pg_stat_statements entry, with cause + action.
23
+
24
+ Picks the statement with the greatest total execution time and maps its
25
+ numbers (mean time, cache-hit ratio, temp spill, calls) — plus an optional
26
+ EXPLAIN plan — to cited causes and concrete actions. Pass 'statements' for
27
+ pure/offline analysis, or omit to pull the top statements live.
28
+
29
+ Args:
30
+ statements: Injected pg_stat_statements rows (as from top_queries); if
31
+ omitted, the worst statements are pulled live.
32
+ explain_sql: Optional SQL to EXPLAIN so plan node types feed the RCA.
33
+ limit: How many statements to pull when not injected (default 20).
34
+ target: Target name from config; omit for the default.
35
+ """
36
+ conn = None
37
+ if statements is None:
38
+ conn = _get_connection(target)
39
+ statements = query_ops.top_queries(conn, order_by="total_time", limit=limit)["statements"]
40
+ explain = None
41
+ if explain_sql:
42
+ conn = conn or _get_connection(target)
43
+ explain = query_ops.explain_query(conn, explain_sql, analyze=False)
44
+ return ops.slow_query_rca(statements, explain=explain)
45
+
46
+
47
+ @mcp.tool()
48
+ @governed_tool(risk_level="low")
49
+ @tool_errors("dict")
50
+ def bloat_and_vacuum_analysis(
51
+ tables: Optional[list[dict[str, Any]]] = None,
52
+ limit: int = 50,
53
+ target: Optional[str] = None,
54
+ ) -> dict:
55
+ """[READ] Rank tables needing vacuum from dead-tuple ratio + autovacuum recency.
56
+
57
+ Pass 'tables' (as from table_bloat) for pure/offline analysis, or omit to
58
+ pull the worst dead-tuple tables live. Each recommendation cites its numbers.
59
+
60
+ Args:
61
+ tables: Injected table-bloat rows; if omitted, pulled live.
62
+ limit: How many tables to pull when not injected (default 50).
63
+ target: Target name from config; omit for the default.
64
+ """
65
+ if tables is None:
66
+ tables = table_ops.table_bloat(_get_connection(target), limit=limit)["tables"]
67
+ return ops.bloat_and_vacuum_analysis(tables)
68
+
69
+
70
+ @mcp.tool()
71
+ @governed_tool(risk_level="low")
72
+ @tool_errors("dict")
73
+ def blocking_lock_chain_rca(
74
+ pairs: Optional[list[dict[str, Any]]] = None,
75
+ target: Optional[str] = None,
76
+ ) -> dict:
77
+ """[READ] Build the wait-for tree from blocking pairs and name the root blocker.
78
+
79
+ Pass 'pairs' (as from the live blocking-pairs read) for pure/offline analysis,
80
+ or omit to pull the current blocking graph live.
81
+
82
+ Args:
83
+ pairs: Injected blocking pairs {blockedPid, blockingPid, ...}; if omitted,
84
+ pulled live from pg_blocking_pids.
85
+ target: Target name from config; omit for the default.
86
+ """
87
+ if pairs is None:
88
+ pairs = activity_ops.blocking_pairs(_get_connection(target))
89
+ return ops.blocking_lock_chain_rca(pairs)
@@ -0,0 +1,65 @@
1
+ """Index-health PostgreSQL MCP tools (read-only)."""
2
+
3
+ from typing import Optional
4
+
5
+ from mcp_server._shared import _get_connection, mcp, tool_errors
6
+ from postgres_aiops.governance import governed_tool
7
+ from postgres_aiops.ops import indexes as ops
8
+
9
+
10
+ @mcp.tool()
11
+ @governed_tool(risk_level="low")
12
+ @tool_errors("dict")
13
+ def unused_indexes(min_size_bytes: int = 0, target: Optional[str] = None) -> dict:
14
+ """[READ] Non-unique, non-primary indexes with zero scans (drop candidates).
15
+
16
+ Args:
17
+ min_size_bytes: Only report indexes at least this large (default 0).
18
+ target: Target name from config; omit for the default.
19
+ """
20
+ return ops.unused_indexes(_get_connection(target), min_size_bytes=min_size_bytes)
21
+
22
+
23
+ @mcp.tool()
24
+ @governed_tool(risk_level="low")
25
+ @tool_errors("dict")
26
+ def missing_index_hints(
27
+ min_seq_scan: int = 1000,
28
+ min_live_tup: int = 10000,
29
+ target: Optional[str] = None,
30
+ ) -> dict:
31
+ """[READ] Tables with heavy sequential scans and few index scans (index hints).
32
+
33
+ Args:
34
+ min_seq_scan: Minimum cumulative sequential scans to flag (default 1000).
35
+ min_live_tup: Minimum live tuples for a table to qualify (default 10000).
36
+ target: Target name from config; omit for the default.
37
+ """
38
+ return ops.missing_index_hints(
39
+ _get_connection(target), min_seq_scan=min_seq_scan, min_live_tup=min_live_tup
40
+ )
41
+
42
+
43
+ @mcp.tool()
44
+ @governed_tool(risk_level="low")
45
+ @tool_errors("dict")
46
+ def index_bloat(limit: int = 50, target: Optional[str] = None) -> dict:
47
+ """[READ] Coarse index-bloat estimate (all inputs returned for transparency).
48
+
49
+ Args:
50
+ limit: Number of indexes to inspect, largest first (default 50).
51
+ target: Target name from config; omit for the default.
52
+ """
53
+ return ops.index_bloat(_get_connection(target), limit=limit)
54
+
55
+
56
+ @mcp.tool()
57
+ @governed_tool(risk_level="low")
58
+ @tool_errors("dict")
59
+ def invalid_indexes(target: Optional[str] = None) -> dict:
60
+ """[READ] Invalid indexes (failed CONCURRENTLY builds) and duplicate indexes.
61
+
62
+ Args:
63
+ target: Target name from config; omit for the default.
64
+ """
65
+ return ops.invalid_indexes(_get_connection(target))
@@ -0,0 +1,60 @@
1
+ """Query-statistics PostgreSQL MCP tools: top-N, EXPLAIN (read) + stats reset (write)."""
2
+
3
+ from typing import Optional
4
+
5
+ from mcp_server._shared import _get_connection, mcp, tool_errors
6
+ from postgres_aiops.governance import governed_tool
7
+ from postgres_aiops.ops import queries as ops
8
+
9
+
10
+ @mcp.tool()
11
+ @governed_tool(risk_level="low")
12
+ @tool_errors("dict")
13
+ def top_queries(
14
+ order_by: str = "total_time",
15
+ limit: int = 20,
16
+ target: Optional[str] = None,
17
+ ) -> dict:
18
+ """[READ] Top statements from pg_stat_statements by a whitelisted metric.
19
+
20
+ Args:
21
+ order_by: One of total_time, mean_time, calls, rows, io.
22
+ limit: Number of statements to return (1..200, default 20).
23
+ target: Target name from config; omit for the default.
24
+ """
25
+ return ops.top_queries(_get_connection(target), order_by=order_by, limit=limit)
26
+
27
+
28
+ @mcp.tool()
29
+ @governed_tool(risk_level="low")
30
+ @tool_errors("dict")
31
+ def explain_query(sql: str, analyze: bool = False, target: Optional[str] = None) -> dict:
32
+ """[READ] Return the JSON execution plan for ``sql`` (EXPLAIN).
33
+
34
+ analyze=False (default) plans without executing; analyze=True runs the
35
+ statement to collect real timing — only use it for read-only SQL.
36
+
37
+ Args:
38
+ sql: A single SQL statement to EXPLAIN.
39
+ analyze: If True, execute the statement to gather real row counts/timing.
40
+ target: Target name from config; omit for the default.
41
+ """
42
+ return ops.explain_query(_get_connection(target), sql, analyze=analyze)
43
+
44
+
45
+ @mcp.tool()
46
+ @governed_tool(risk_level="medium")
47
+ @tool_errors("dict")
48
+ def reset_query_stats(dry_run: bool = False, target: Optional[str] = None) -> dict:
49
+ """[WRITE][risk=medium] Reset pg_stat_statements accumulators (irreversible).
50
+
51
+ The counters cannot be restored, so no undo is recorded. Pass dry_run=True
52
+ to preview.
53
+
54
+ Args:
55
+ dry_run: If True, preview without resetting.
56
+ target: Target name from config; omit for the default.
57
+ """
58
+ if dry_run:
59
+ return {"dryRun": True, "wouldReset": "pg_stat_statements"}
60
+ return ops.reset_query_stats(_get_connection(target))