postgres-aiops 0.10.3__tar.gz → 0.12.0__tar.gz

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 (114) hide show
  1. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.claude-plugin/plugin.json +1 -1
  2. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.mcp.json +1 -1
  3. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/CHANGELOG.md +33 -0
  4. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/PKG-INFO +1 -1
  5. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/RELEASE_NOTES.md +3 -0
  6. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/SECURITY.md +1 -1
  7. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/openclaw.plugin.json +1 -1
  8. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/package.json +1 -1
  9. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/_root.py +1 -1
  10. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/query.py +1 -1
  11. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/remediate.py +8 -8
  12. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/undo.py +3 -3
  13. postgres_aiops-0.12.0/postgres_aiops/mcp_server/_annotations.py +66 -0
  14. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/_shared.py +2 -2
  15. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/server.py +8 -3
  16. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/activity.py +1 -1
  17. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/analysis.py +1 -1
  18. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/indexes.py +1 -1
  19. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/queries.py +1 -1
  20. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/remediation.py +1 -1
  21. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/replication.py +1 -1
  22. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/server.py +1 -1
  23. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/tables.py +1 -1
  24. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/undo.py +4 -4
  25. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/analysis.py +28 -8
  26. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/pyproject.toml +4 -4
  27. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/server.json +2 -2
  28. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/agent-guardrails.md +1 -1
  29. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/capabilities.md +1 -1
  30. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_analysis.py +45 -0
  31. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_audited.py +3 -2
  32. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_governed_exit.py +1 -1
  33. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_remediate.py +3 -3
  34. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_writes.py +4 -4
  35. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_connection_lost.py +1 -1
  36. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_audit.py +1 -1
  37. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_decorators.py +1 -1
  38. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_patterns.py +1 -1
  39. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_governance_persistence.py +1 -1
  40. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_mcp_tools.py +18 -18
  41. postgres_aiops-0.12.0/tests/test_package_layout.py +62 -0
  42. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_smoke.py +15 -15
  43. postgres_aiops-0.12.0/tests/test_tool_annotations.py +139 -0
  44. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_truncation.py +2 -2
  45. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_undo_executor.py +5 -5
  46. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_undo_replay.py +1 -1
  47. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/uv.lock +1 -1
  48. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.github/workflows/mcp-publish.yml +0 -0
  49. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.github/workflows/publish.yml +0 -0
  50. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/.gitignore +0 -0
  51. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/Dockerfile +0 -0
  52. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/LICENSE +0 -0
  53. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/README.md +0 -0
  54. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/docs/VERIFICATION.md +0 -0
  55. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/glama.json +0 -0
  56. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/__init__.py +0 -0
  57. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/__init__.py +0 -0
  58. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/_common.py +0 -0
  59. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/activity.py +0 -0
  60. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/analyze.py +0 -0
  61. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/doctor.py +0 -0
  62. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/index.py +0 -0
  63. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/init.py +0 -0
  64. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/overview.py +0 -0
  65. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/replication.py +0 -0
  66. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/secret.py +0 -0
  67. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/server.py +0 -0
  68. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/cli/table.py +0 -0
  69. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/config.py +0 -0
  70. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/connection.py +0 -0
  71. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/doctor.py +0 -0
  72. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/__init__.py +0 -0
  73. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/audit.py +0 -0
  74. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/budget.py +0 -0
  75. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/decorators.py +0 -0
  76. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/outcome.py +0 -0
  77. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/paths.py +0 -0
  78. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/patterns.py +0 -0
  79. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/policy.py +0 -0
  80. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/sanitize.py +0 -0
  81. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/governance/undo.py +0 -0
  82. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/__init__.py +0 -0
  83. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0/postgres_aiops}/mcp_server/tools/__init__.py +0 -0
  84. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/__init__.py +0 -0
  85. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/_util.py +0 -0
  86. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/activity.py +0 -0
  87. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/indexes.py +0 -0
  88. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/overview.py +0 -0
  89. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/queries.py +0 -0
  90. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/remediation.py +0 -0
  91. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/replication.py +0 -0
  92. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/server.py +0 -0
  93. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/ops/tables.py +0 -0
  94. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/postgres_aiops/secretstore.py +0 -0
  95. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/SKILL.md +0 -0
  96. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/cli-reference.md +0 -0
  97. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/skills/postgres-aiops/references/setup-guide.md +0 -0
  98. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/smithery.yaml +0 -0
  99. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/conftest.py +0 -0
  100. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_reads.py +0 -0
  101. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_cli_secret.py +0 -0
  102. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_config.py +0 -0
  103. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_connection.py +0 -0
  104. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_connection_more.py +0 -0
  105. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_doctor.py +0 -0
  106. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_gov_policy.py +0 -0
  107. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_init.py +0 -0
  108. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_ops_more.py +0 -0
  109. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_optional_fields.py +0 -0
  110. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_reads.py +0 -0
  111. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_secretstore.py +0 -0
  112. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_secretstore_more.py +0 -0
  113. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_self_lockout.py +0 -0
  114. {postgres_aiops-0.10.3 → postgres_aiops-0.12.0}/tests/test_writes.py +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "postgres-aiops",
4
4
  "displayName": "Postgres AIops",
5
- "version": "0.10.3",
5
+ "version": "0.12.0",
6
6
  "description": "Governed PostgreSQL DBA ops: slow-query RCA, bloat/vacuum & blocking-lock analysis; 35 MCP tools.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "postgres-aiops==0.10.3",
8
+ "postgres-aiops==0.12.0",
9
9
  "postgres-aiops-mcp"
10
10
  ]
11
11
  }
@@ -1,5 +1,38 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.12.0 — 2026-10-15
4
+
5
+ ### Fixed
6
+ - The MCP server moved from a top-level `mcp_server` package to
7
+ `postgres_aiops.mcp_server`. Every AIops tool used to ship its own top-level
8
+ `mcp_server`, so installing two of them into one environment merged the two
9
+ into one package: `postgres-aiops-mcp` could start another tool's server, and
10
+ `postgres-aiops undo` wrote audit and undo rows into the other tool's home
11
+ directory. Installs through `uvx`, the Claude Code plugin or the OpenClaw
12
+ bundle were isolated and unaffected. The `postgres-aiops-mcp` command is
13
+ unchanged; only code that imported `mcp_server` directly must now import
14
+ `postgres_aiops.mcp_server`.
15
+
16
+ ## v0.11.0 — 2026-10-15
17
+
18
+ ### Added
19
+ - MCP tool annotations derived from each tool's governance `risk_level`, so
20
+ clients can gate approval from `tools/list` alone. `low` tools advertise
21
+ `readOnlyHint: true`; every other tool advertises `readOnlyHint: false` and
22
+ `destructiveHint: true`. The `medium` tier includes deletes and hard
23
+ power-offs, so a tool is never advertised as non-destructive on the strength
24
+ of its tier. `idempotentHint` only repeats an explicit declaration. The exact
25
+ level is published in `_meta` under `io.github.AIops-tools/riskLevel`. These
26
+ are hints for the client; audit and undo are unchanged.
27
+
28
+ ### Fixed
29
+ - `blocking_lock_chain_rca`: the chain root's state was dropped and a missing
30
+ query became `""`. For a backend that is `idle in transaction`, `rootQuery` is
31
+ the last statement it ran, not a running one, so the recommended
32
+ `cancel_query` had nothing to cancel. Roots now carry `rootState` and
33
+ `rootIdle` (true for any `idle*` state, including plain `idle` with session-level locks), a missing query stays `null`, and the action for an
34
+ idle root points at `terminate_backend` (which rolls the transaction back).
35
+
3
36
  ## v0.10.3 — 2026-09-15
4
37
 
5
38
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: postgres-aiops
3
- Version: 0.10.3
3
+ Version: 0.12.0
4
4
  Summary: Governed AI-ops for PostgreSQL DBA operations: slow-query RCA, bloat/vacuum & blocking-lock analysis with a built-in governance harness (audit, budget, undo, risk tiers)
5
5
  Author-email: Wei Zhou <zhouwei008@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,8 @@
1
1
  # Release notes — postgres-aiops 0.6.0
2
2
 
3
+ > Historical — this file covers the 0.6.0 release only and is not updated.
4
+ > For every release since, see [CHANGELOG.md](CHANGELOG.md) and the GitHub releases.
5
+
3
6
  Previous release: 0.5.0.
4
7
 
5
8
  ## BREAKING — the authorization layer is removed
@@ -82,7 +82,7 @@ connection. No post-install scripts or background services.
82
82
  ## Static Analysis
83
83
 
84
84
  ```bash
85
- uvx bandit -r postgres_aiops/ mcp_server/
85
+ uvx bandit -r postgres_aiops/
86
86
  uv run ruff check .
87
87
  ```
88
88
 
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "postgres-aiops==0.10.3",
14
+ "postgres-aiops==0.12.0",
15
15
  "postgres-aiops-mcp"
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zw008/postgres-aiops",
3
- "version": "0.10.3",
3
+ "version": "0.12.0",
4
4
  "description": "Governed PostgreSQL DBA ops: slow-query RCA, bloat/vacuum & blocking-lock analysis; 35 MCP tools.",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -61,7 +61,7 @@ def mcp_cmd() -> None:
61
61
  )
62
62
  raise typer.Exit(2)
63
63
 
64
- from mcp_server.server import main as _mcp_main
64
+ from postgres_aiops.mcp_server.server import main as _mcp_main
65
65
 
66
66
  _mcp_main()
67
67
 
@@ -70,7 +70,7 @@ def query_reset(target: TargetOption = None, dry_run: DryRunOption = False) -> N
70
70
  ``@governed_tool``-wrapped MCP function, so each is audited on the same
71
71
  governance path as MCP calls.
72
72
  """
73
- from mcp_server.tools import queries as gov
73
+ from postgres_aiops.mcp_server.tools import queries as gov
74
74
 
75
75
  if dry_run:
76
76
  dry_run_preview(
@@ -32,7 +32,7 @@ def remediate_terminate(
32
32
  dry_run: DryRunOption = False,
33
33
  ) -> None:
34
34
  """Terminate a backend (no undo; dry-run + confirm)."""
35
- from mcp_server.tools import remediation as gov
35
+ from postgres_aiops.mcp_server.tools import remediation as gov
36
36
 
37
37
  if dry_run:
38
38
  # Through the governed call: terminate_backend refuses this tool's own
@@ -54,7 +54,7 @@ def remediate_cancel(
54
54
  dry_run: DryRunOption = False,
55
55
  ) -> None:
56
56
  """Cancel a backend's running query (no undo; dry-run + confirm)."""
57
- from mcp_server.tools import remediation as gov
57
+ from postgres_aiops.mcp_server.tools import remediation as gov
58
58
 
59
59
  if dry_run:
60
60
  dry_run_preview(
@@ -76,7 +76,7 @@ def remediate_vacuum(
76
76
  dry_run: DryRunOption = False,
77
77
  ) -> None:
78
78
  """VACUUM a table (dry-run + confirm)."""
79
- from mcp_server.tools import remediation as gov
79
+ from postgres_aiops.mcp_server.tools import remediation as gov
80
80
 
81
81
  if dry_run:
82
82
  dry_run_preview(
@@ -97,7 +97,7 @@ def remediate_analyze(
97
97
  dry_run: DryRunOption = False,
98
98
  ) -> None:
99
99
  """ANALYZE a table (dry-run + confirm)."""
100
- from mcp_server.tools import remediation as gov
100
+ from postgres_aiops.mcp_server.tools import remediation as gov
101
101
 
102
102
  if dry_run:
103
103
  dry_run_preview(
@@ -120,7 +120,7 @@ def remediate_create_index(
120
120
  dry_run: DryRunOption = False,
121
121
  ) -> None:
122
122
  """Create an index (reversible; dry-run + confirm)."""
123
- from mcp_server.tools import remediation as gov
123
+ from postgres_aiops.mcp_server.tools import remediation as gov
124
124
 
125
125
  if dry_run:
126
126
  # Through the governed call: create_index validates the table/columns vs
@@ -146,7 +146,7 @@ def remediate_drop_index(
146
146
  dry_run: DryRunOption = False,
147
147
  ) -> None:
148
148
  """Drop an index (reversible; captures the definition first; dry-run + confirm)."""
149
- from mcp_server.tools import remediation as gov
149
+ from postgres_aiops.mcp_server.tools import remediation as gov
150
150
 
151
151
  if dry_run:
152
152
  dry_run_preview(
@@ -168,7 +168,7 @@ def remediate_reindex(
168
168
  dry_run: DryRunOption = False,
169
169
  ) -> None:
170
170
  """REINDEX an index/table/schema (dry-run + confirm)."""
171
- from mcp_server.tools import remediation as gov
171
+ from postgres_aiops.mcp_server.tools import remediation as gov
172
172
 
173
173
  if dry_run:
174
174
  dry_run_preview(
@@ -195,7 +195,7 @@ def remediate_set(
195
195
  dry_run: DryRunOption = False,
196
196
  ) -> None:
197
197
  """ALTER SYSTEM SET a parameter, or --reset it (reversible; dry-run + confirm)."""
198
- from mcp_server.tools import remediation as gov
198
+ from postgres_aiops.mcp_server.tools import remediation as gov
199
199
 
200
200
  action = f"ALTER SYSTEM RESET {name}" if reset else f"ALTER SYSTEM SET {name} ="
201
201
  if dry_run:
@@ -1,7 +1,7 @@
1
1
  """``postgres-aiops undo`` — list recorded undo tokens and apply them.
2
2
 
3
3
  Real execution is delegated to the ``@governed_tool``-wrapped functions in
4
- ``mcp_server.tools.undo`` so an applied undo is audited on the SAME governance
4
+ ``postgres_aiops.mcp_server.tools.undo`` so an applied undo is audited on the SAME governance
5
5
  path as any other write (the inverse tool it dispatches is itself re-gated).
6
6
  """
7
7
 
@@ -38,7 +38,7 @@ def undo_list_cmd(
38
38
  target: TargetOption = None,
39
39
  ) -> None:
40
40
  """List recorded, not-yet-applied undo tokens."""
41
- from mcp_server.tools import undo as gov
41
+ from postgres_aiops.mcp_server.tools import undo as gov
42
42
 
43
43
  console.print_json(json.dumps(gov.undo_list(limit=limit, target=target)))
44
44
 
@@ -49,7 +49,7 @@ def undo_apply_cmd(
49
49
  undo_id: UndoIdArg, target: TargetOption = None, dry_run: DryRunOption = False
50
50
  ) -> None:
51
51
  """Apply a recorded undo (dispatches its inverse tool; dry-run + confirm)."""
52
- from mcp_server.tools import undo as gov
52
+ from postgres_aiops.mcp_server.tools import undo as gov
53
53
 
54
54
  if dry_run:
55
55
  # undo_apply refuses an unknown or already-applied token, and refuses one
@@ -0,0 +1,66 @@
1
+ """Derive MCP tool annotations from the governance risk level.
2
+
3
+ Every tool already declares ``risk_level`` on ``@governed_tool``; this module
4
+ publishes it to MCP clients as the standard ``ToolAnnotations`` hints plus the
5
+ exact level under ``_meta``, so a client can gate approval without reading our
6
+ docstrings.
7
+
8
+ Only the safe direction is ever asserted:
9
+
10
+ - ``low`` → ``readOnlyHint: true``. Backed by the smoke-test invariant that
11
+ ``risk_level == "low"`` exactly when the docstring says ``[READ]``.
12
+ - anything else → ``readOnlyHint: false, destructiveHint: true``. The
13
+ ``medium`` tier is NOT non-destructive — it holds deletes, hard power-offs,
14
+ state kills and stat resets — so claiming ``destructiveHint: false`` from the
15
+ tier would tell a client it may skip approval for a delete. Over-warning
16
+ costs a confirmation prompt; under-warning costs data.
17
+ - ``idempotentHint`` only echoes an explicit ``@governed_tool(idempotent=True)``
18
+ declaration; it is never inferred.
19
+ - ``openWorldHint`` is left unset (spec default ``true``): every tool talks to
20
+ an external system.
21
+
22
+ This file is identical across the AIops line; keep it that way.
23
+ """
24
+
25
+ from typing import Any
26
+
27
+ from mcp.types import ToolAnnotations
28
+
29
+ RISK_META_KEY = "io.github.AIops-tools/riskLevel"
30
+ RISK_LEVELS = frozenset({"low", "medium", "high", "critical"})
31
+
32
+
33
+ def annotations_for(risk_level: str, idempotent: bool) -> ToolAnnotations:
34
+ """Return the MCP hints that a tool of ``risk_level`` may honestly claim."""
35
+ if risk_level not in RISK_LEVELS:
36
+ raise ValueError(
37
+ f"unknown risk_level {risk_level!r}; expected one of {sorted(RISK_LEVELS)}"
38
+ )
39
+ if risk_level == "low":
40
+ return ToolAnnotations(read_only_hint=True)
41
+ return ToolAnnotations(
42
+ read_only_hint=False,
43
+ destructive_hint=True,
44
+ idempotent_hint=idempotent,
45
+ )
46
+
47
+
48
+ def apply_risk_annotations(server: Any) -> None:
49
+ """Replace every registered tool with a copy carrying its risk annotations.
50
+
51
+ Fails fast on a tool without ``@governed_tool`` metadata: an unannotated
52
+ tool would be advertised with the spec defaults, i.e. as a destructive
53
+ write with no stated risk level — wrong for a read, silent for a write.
54
+ """
55
+ tools = server._tool_manager._tools # same private registry undo.py relies on
56
+ for name, tool in list(tools.items()):
57
+ risk_level = getattr(tool.fn, "_risk_level", None)
58
+ if risk_level is None:
59
+ raise RuntimeError(f"tool {name!r} has no @governed_tool risk_level")
60
+ idempotent = bool(getattr(tool.fn, "_idempotent", False))
61
+ tools[name] = tool.model_copy(
62
+ update={
63
+ "annotations": annotations_for(risk_level, idempotent),
64
+ "meta": {**(tool.meta or {}), RISK_META_KEY: risk_level},
65
+ }
66
+ )
@@ -1,8 +1,8 @@
1
1
  """Shared MCP server primitives: the MCPServer instance, connection helper,
2
2
  error sanitisation, and the ``@tool_errors`` decorator.
3
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
4
+ Tool modules under ``postgres_aiops/mcp_server/tools/`` import ``mcp`` from here and register
5
+ their ``@mcp.tool()`` functions onto it. ``postgres_aiops/mcp_server/server.py`` then imports
6
6
  those modules and runs the server.
7
7
 
8
8
  Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any reflected tool
@@ -1,6 +1,6 @@
1
1
  """MCP server wrapping postgres-aiops operations (stdio transport).
2
2
 
3
- Thin adapter layer: each ``@mcp.tool()`` function (in ``mcp_server/tools/``)
3
+ Thin adapter layer: each ``@mcp.tool()`` function (in ``postgres_aiops/mcp_server/tools/``)
4
4
  delegates to the ``postgres_aiops`` ops package and is wrapped with the
5
5
  postgres-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
6
6
 
@@ -14,11 +14,12 @@ License: MIT
14
14
  import logging
15
15
  import os
16
16
 
17
- from mcp_server._shared import _safe_error, mcp, tool_errors
17
+ from postgres_aiops.mcp_server._annotations import apply_risk_annotations
18
+ from postgres_aiops.mcp_server._shared import _safe_error, mcp, tool_errors
18
19
 
19
20
  # Importing the tool modules registers every @mcp.tool() onto the shared
20
21
  # `mcp` instance. Order does not matter; each module is self-contained.
21
- from mcp_server.tools import ( # noqa: F401 — side effects
22
+ from postgres_aiops.mcp_server.tools import ( # noqa: F401 — side effects
22
23
  activity,
23
24
  analysis,
24
25
  indexes,
@@ -30,6 +31,10 @@ from mcp_server.tools import ( # noqa: F401 — side effects
30
31
  undo,
31
32
  )
32
33
 
34
+ # Publish each tool's governance risk level as MCP annotations. Runs after
35
+ # every tool module has registered, so no tool is advertised unannotated.
36
+ apply_risk_annotations(mcp)
37
+
33
38
  __all__ = ["mcp", "main", "_safe_error", "tool_errors"]
34
39
 
35
40
  logger = logging.getLogger(__name__)
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import activity as ops
8
8
 
9
9
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Any, Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import activity as activity_ops
8
8
  from postgres_aiops.ops import analysis as ops
9
9
  from postgres_aiops.ops import queries as query_ops
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import indexes as ops
8
8
 
9
9
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import queries as ops
8
8
 
9
9
 
@@ -12,8 +12,8 @@ Risk tiers:
12
12
 
13
13
  from typing import Any, Optional
14
14
 
15
- from mcp_server._shared import _get_connection, mcp, tool_errors
16
15
  from postgres_aiops.governance import governed_tool
16
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
17
17
  from postgres_aiops.ops import remediation as ops
18
18
 
19
19
  # ── undo descriptors (built from the fetched before-state) ──────────────────
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import replication as ops
8
8
 
9
9
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import overview as overview_ops
8
8
  from postgres_aiops.ops import server as ops
9
9
 
@@ -2,8 +2,8 @@
2
2
 
3
3
  from typing import Optional
4
4
 
5
- from mcp_server._shared import _get_connection, mcp, tool_errors
6
5
  from postgres_aiops.governance import governed_tool
6
+ from postgres_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
7
7
  from postgres_aiops.ops import tables as ops
8
8
 
9
9
 
@@ -15,26 +15,26 @@ design, inverses in this line key off ids/names, not credentials.
15
15
  import json
16
16
  from typing import Any, Optional
17
17
 
18
- from mcp_server._shared import mcp, tool_errors
19
18
  from postgres_aiops.governance import governed_tool
20
19
  from postgres_aiops.governance.undo import get_undo_store
20
+ from postgres_aiops.mcp_server._shared import mcp, tool_errors
21
21
 
22
22
 
23
23
  def _resolve_tool(name: str) -> Any:
24
24
  """Return the governed callable registered under ``name`` (or None).
25
25
 
26
26
  The CLI reaches this through ``undo apply``, whose process only imports
27
- ``mcp_server.tools.undo`` — every other tool module is imported lazily inside
27
+ ``postgres_aiops.mcp_server.tools.undo`` — every other tool module is imported lazily inside
28
28
  its own CLI command and so is absent here. Without forcing a full load,
29
29
  ``undo_apply`` would fail with "inverse tool not registered" for every write
30
30
  tool when driven from the CLI (the MCP entry point loads all modules, so it
31
- never hit this). If the first lookup misses, import ``mcp_server.server`` —
31
+ never hit this). If the first lookup misses, import ``postgres_aiops.mcp_server.server`` —
32
32
  which registers every ``@mcp.tool()`` as an import side effect — and retry.
33
33
  Imported lazily to avoid the undo↔server cycle.
34
34
  """
35
35
  tool = mcp._tool_manager._tools.get(name)
36
36
  if tool is None:
37
- import mcp_server.server # noqa: F401 — registers all tools as a side effect
37
+ import postgres_aiops.mcp_server.server # noqa: F401 — registers all tools as a side effect
38
38
 
39
39
  tool = mcp._tool_manager._tools.get(name)
40
40
  return getattr(tool, "fn", None) if tool else None
@@ -201,6 +201,22 @@ def _descendants(root: int, children: dict[int, list[int]]) -> set[int]:
201
201
  return seen
202
202
 
203
203
 
204
+ def _lock_root_action(pid: int, idle: bool | None) -> str:
205
+ if idle:
206
+ return (
207
+ f"Backend {pid} heads the chain but is not running a statement (see "
208
+ "rootState: an open transaction, or session-level locks): rootQuery is "
209
+ "the last statement it ran, not a running one. cancel_query has nothing "
210
+ f"to cancel; terminate_backend({pid}) ends the session, rolling back any "
211
+ "transaction and releasing its locks — confirm with its owner first."
212
+ )
213
+ return (
214
+ f"Backend {pid} is the head of the chain — investigate its transaction; "
215
+ f"cancel_query({pid}) to stop its statement, or terminate_backend({pid}) to "
216
+ "end the session and release the locks."
217
+ )
218
+
219
+
204
220
  def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
205
221
  """[READ] Build the wait-for tree from blocking pairs and name the root blocker.
206
222
 
@@ -217,7 +233,8 @@ def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
217
233
  return {"blockedBackends": 0, "roots": [], "note": "No blocking detected."}
218
234
 
219
235
  children: dict[int, list[int]] = {}
220
- blocking_query: dict[int, str] = {}
236
+ blocking_query: dict[int, str | None] = {}
237
+ blocking_state: dict[int, str | None] = {}
221
238
  blocked_pids: set[int] = set()
222
239
  blocking_pids: set[int] = set()
223
240
  for e in edges:
@@ -225,7 +242,8 @@ def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
225
242
  children.setdefault(g, []).append(b)
226
243
  blocked_pids.add(b)
227
244
  blocking_pids.add(g)
228
- blocking_query.setdefault(g, e.get("blockingQuery") or "")
245
+ blocking_query.setdefault(g, e.get("blockingQuery"))
246
+ blocking_state.setdefault(g, e.get("blockingState"))
229
247
 
230
248
  root_pids = [pid for pid in blocking_pids if pid not in blocked_pids]
231
249
  if not root_pids:
@@ -242,16 +260,18 @@ def blocking_lock_chain_rca(pairs: list[dict]) -> dict:
242
260
  roots = []
243
261
  for pid in root_pids:
244
262
  blocked = _descendants(pid, children)
263
+ state = blocking_state.get(pid)
264
+ # Any idle state means no statement is running: "idle in transaction"
265
+ # holds row locks, plain "idle" can still hold session-level advisory locks.
266
+ idle = None if state is None else str(state).startswith("idle")
245
267
  roots.append({
246
268
  "rootPid": pid,
247
269
  "blockedCount": len(blocked),
248
270
  "blockedPids": sorted(blocked),
249
- "rootQuery": blocking_query.get(pid, ""),
250
- "action": (
251
- f"Backend {pid} is the head of the chain — investigate its "
252
- "transaction; cancel_query(pid) to stop its statement, or "
253
- "terminate_backend(pid) to end the session and release the locks."
254
- ),
271
+ "rootQuery": blocking_query.get(pid),
272
+ "rootState": state,
273
+ "rootIdle": idle,
274
+ "action": _lock_root_action(pid, idle),
255
275
  })
256
276
  roots.sort(key=lambda r: r["blockedCount"], reverse=True)
257
277
  return {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "postgres-aiops"
7
- version = "0.10.3"
7
+ version = "0.12.0"
8
8
  description = "Governed AI-ops for PostgreSQL DBA operations: slow-query RCA, bloat/vacuum & blocking-lock analysis with a built-in governance harness (audit, budget, undo, risk tiers)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -28,10 +28,10 @@ dev = [
28
28
 
29
29
  [project.scripts]
30
30
  postgres-aiops = "postgres_aiops.cli:app"
31
- postgres-aiops-mcp = "mcp_server.server:main"
31
+ postgres-aiops-mcp = "postgres_aiops.mcp_server.server:main"
32
32
 
33
33
  [tool.hatch.build.targets.wheel]
34
- packages = ["postgres_aiops", "mcp_server"]
34
+ packages = ["postgres_aiops"]
35
35
 
36
36
  [tool.ruff]
37
37
  line-length = 100
@@ -47,7 +47,7 @@ select = ["E", "F", "I", "N", "W", "UP"]
47
47
  "postgres_aiops/governance/*.py" = ["E501", "N818"]
48
48
  # MCP server files are reflected by FastMCP/Pydantic; PEP 604 unions (X | None)
49
49
  # crash on older mcp/pydantic, so they intentionally use Optional[X].
50
- "mcp_server/**/*.py" = ["UP007", "UP045"]
50
+ "postgres_aiops/mcp_server/**/*.py" = ["UP007", "UP045"]
51
51
  # The MCP entry keeps an explicit Python-version guard even though the declared
52
52
  # minimum is already 3.11.
53
53
  "postgres_aiops/cli/_root.py" = ["UP036"]
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/AIops-tools/Postgres-AIops",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.10.3",
10
+ "version": "0.12.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "postgres-aiops",
15
- "version": "0.10.3",
15
+ "version": "0.12.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  }
@@ -28,7 +28,7 @@ recorded on the audit row; they are never required and never block a call.
28
28
  ## What still needs a prompt
29
29
 
30
30
  These are model-behaviour problems the harness cannot fix from the outside.
31
- Copy this into your agent's system prompt:
31
+ Recommended setup for a local model (adapt it to your agent client):
32
32
 
33
33
  ```text
34
34
  You operate a PostgreSQL server through the postgres-aiops MCP tools.
@@ -32,7 +32,7 @@
32
32
  | `wal_status` | WAL fns + `pg_stat_archiver` | currentLsn, walLevel, max/minWalSize, archiver |
33
33
  | `slow_query_rca` | pg_stat_statements + EXPLAIN | worst{}, findings[] (cited cause/action) |
34
34
  | `bloat_and_vacuum_analysis` | table-bloat rows | recommendations[] (cited reasons + action) |
35
- | `blocking_lock_chain_rca` | `pg_blocking_pids` pairs | roots[], worstRootPid, deadlockSuspected |
35
+ | `blocking_lock_chain_rca` | `pg_blocking_pids` pairs | roots[] (rootQuery, rootState, rootIdle — true for any `idle*` state; rootQuery is then the last statement run, not a running one — and action), worstRootPid, deadlockSuspected |
36
36
  | `undo_list` | local undo store | recorded, not-yet-applied reversible writes: undoId, ts, originalTool, inverseTool, note |
37
37
 
38
38
  The flagship analyses accept injected records (`statements=` / `tables=` /
@@ -82,3 +82,48 @@ def test_blocking_chain_detects_cycle():
82
82
  def test_blocking_chain_no_blocking():
83
83
  out = analysis.blocking_lock_chain_rca([])
84
84
  assert out["blockedBackends"] == 0
85
+
86
+
87
+ # ── lock-chain root that is idle inside an open transaction ──────────────────
88
+
89
+
90
+ def _pg_pairs(state, query="UPDATE t SET v=1 WHERE id=1"):
91
+ return [{"blockedPid": 201, "blockingPid": 100, "blockedQuery": "DELETE FROM t",
92
+ "blockingQuery": query, "blockingState": state}]
93
+
94
+
95
+ def test_idle_in_transaction_root_is_named_and_cancel_is_ruled_out():
96
+ from postgres_aiops.ops import analysis as a
97
+
98
+ (root,) = a.blocking_lock_chain_rca(_pg_pairs("idle in transaction"))["roots"]
99
+ assert root["rootState"] == "idle in transaction"
100
+ assert root["rootIdle"] is True
101
+ # The query is the LAST statement it ran, not a running one.
102
+ assert "last statement" in root["action"]
103
+ assert "terminate_backend(100)" in root["action"]
104
+ assert "nothing to cancel" in root["action"]
105
+
106
+
107
+ def test_active_root_offers_cancel_first():
108
+ from postgres_aiops.ops import analysis as a
109
+
110
+ (root,) = a.blocking_lock_chain_rca(_pg_pairs("active"))["roots"]
111
+ assert root["rootIdle"] is False
112
+ assert "cancel_query(100)" in root["action"]
113
+
114
+
115
+ def test_missing_root_query_stays_null():
116
+ from postgres_aiops.ops import analysis as a
117
+
118
+ (root,) = a.blocking_lock_chain_rca(_pg_pairs(None, query=None))["roots"]
119
+ assert root["rootQuery"] is None
120
+ assert root["rootState"] is None
121
+ assert root["rootIdle"] is None # state unknown
122
+
123
+
124
+ def test_plain_idle_root_with_session_locks_is_not_offered_cancel():
125
+ from postgres_aiops.ops import analysis as a
126
+
127
+ (root,) = a.blocking_lock_chain_rca(_pg_pairs("idle"))["roots"]
128
+ assert root["rootIdle"] is True
129
+ assert "terminate_backend(100)" in root["action"]