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.
- mcp_server/__init__.py +1 -0
- mcp_server/_shared.py +101 -0
- mcp_server/server.py +37 -0
- mcp_server/tools/__init__.py +1 -0
- mcp_server/tools/activity.py +52 -0
- mcp_server/tools/analysis.py +89 -0
- mcp_server/tools/indexes.py +65 -0
- mcp_server/tools/queries.py +60 -0
- mcp_server/tools/remediation.py +281 -0
- mcp_server/tools/replication.py +43 -0
- mcp_server/tools/server.py +86 -0
- mcp_server/tools/tables.py +46 -0
- postgres_aiops/__init__.py +9 -0
- postgres_aiops/cli/__init__.py +9 -0
- postgres_aiops/cli/_common.py +78 -0
- postgres_aiops/cli/_root.py +68 -0
- postgres_aiops/cli/activity.py +52 -0
- postgres_aiops/cli/analyze.py +53 -0
- postgres_aiops/cli/doctor.py +21 -0
- postgres_aiops/cli/index.py +55 -0
- postgres_aiops/cli/init.py +112 -0
- postgres_aiops/cli/overview.py +16 -0
- postgres_aiops/cli/query.py +70 -0
- postgres_aiops/cli/remediate.py +186 -0
- postgres_aiops/cli/replication.py +45 -0
- postgres_aiops/cli/secret.py +103 -0
- postgres_aiops/cli/server.py +69 -0
- postgres_aiops/cli/table.py +45 -0
- postgres_aiops/config.py +154 -0
- postgres_aiops/connection.py +178 -0
- postgres_aiops/doctor.py +84 -0
- postgres_aiops/governance/__init__.py +40 -0
- postgres_aiops/governance/audit.py +377 -0
- postgres_aiops/governance/budget.py +225 -0
- postgres_aiops/governance/decorators.py +474 -0
- postgres_aiops/governance/paths.py +23 -0
- postgres_aiops/governance/patterns.py +378 -0
- postgres_aiops/governance/policy.py +411 -0
- postgres_aiops/governance/sanitize.py +39 -0
- postgres_aiops/governance/undo.py +218 -0
- postgres_aiops/ops/__init__.py +1 -0
- postgres_aiops/ops/_util.py +102 -0
- postgres_aiops/ops/activity.py +193 -0
- postgres_aiops/ops/analysis.py +263 -0
- postgres_aiops/ops/indexes.py +211 -0
- postgres_aiops/ops/overview.py +51 -0
- postgres_aiops/ops/queries.py +123 -0
- postgres_aiops/ops/remediation.py +237 -0
- postgres_aiops/ops/replication.py +144 -0
- postgres_aiops/ops/server.py +151 -0
- postgres_aiops/ops/tables.py +146 -0
- postgres_aiops/secretstore.py +302 -0
- postgres_aiops-0.1.0.dist-info/METADATA +119 -0
- postgres_aiops-0.1.0.dist-info/RECORD +57 -0
- postgres_aiops-0.1.0.dist-info/WHEEL +4 -0
- postgres_aiops-0.1.0.dist-info/entry_points.txt +3 -0
- 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))
|