queue-aiops 0.10.0__tar.gz → 0.11.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.
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/.claude-plugin/plugin.json +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/.mcp.json +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/CHANGELOG.md +13 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/PKG-INFO +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/SECURITY.md +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/openclaw.plugin.json +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/package.json +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/pyproject.toml +4 -4
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/_root.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/rabbit_cmds.py +5 -5
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/redis_cmds.py +2 -2
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/undo.py +3 -3
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/_shared.py +2 -2
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/server.py +4 -4
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/analysis.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/overview.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/rabbit_reads.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/redis_reads.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/undo.py +4 -4
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/writes.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/server.json +2 -2
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_analysis.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_cli_audited.py +3 -2
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_cli_governed_exit.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_cli_rabbit_writes.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_cli_writes.py +3 -3
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_gov_audit.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_gov_decorators.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_gov_patterns.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_governance_persistence.py +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_mcp_reads.py +12 -12
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_optional_fields.py +1 -1
- queue_aiops-0.11.0/tests/test_package_layout.py +62 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_self_lockout.py +4 -4
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_smoke.py +9 -9
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_tool_annotations.py +17 -6
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_undo_executor.py +5 -5
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_writes.py +7 -7
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/uv.lock +1 -1
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/.github/workflows/mcp-publish.yml +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/.github/workflows/publish.yml +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/.gitignore +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/Dockerfile +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/LICENSE +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/README.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/RELEASE_NOTES.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/docs/VERIFICATION.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/glama.json +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/__init__.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/__init__.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/_common.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/analyze.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/doctor.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/init.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/overview.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/cli/secret.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/config.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/connection.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/doctor.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/__init__.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/audit.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/budget.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/decorators.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/outcome.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/paths.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/patterns.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/policy.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/sanitize.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/governance/undo.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/__init__.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/_annotations.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0/queue_aiops}/mcp_server/tools/__init__.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/__init__.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/_util.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/analysis.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/overview.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/rabbit_reads.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/redis_reads.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/ops/writes.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/platform.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/queue_aiops/secretstore.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/skills/queue-aiops/SKILL.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/skills/queue-aiops/references/agent-guardrails.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/skills/queue-aiops/references/capabilities.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/skills/queue-aiops/references/cli-reference.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/skills/queue-aiops/references/setup-guide.md +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/smithery.yaml +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/conftest.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/fakes.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_cli_reads.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_cli_secret.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_connection.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_doctor.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_gov_policy.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_init.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_int_fidelity.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_platform.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_reads.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_secretstore.py +0 -0
- {queue_aiops-0.10.0 → queue_aiops-0.11.0}/tests/test_self_kill.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "queue-aiops",
|
|
4
4
|
"displayName": "Queue AIops",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.11.0",
|
|
6
6
|
"description": "Governed redis + rabbitmq ops: memory/latency/backlog/churn RCA, policies. 28 tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.11.0 — 2026-10-15
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- The MCP server moved from a top-level `mcp_server` package to
|
|
7
|
+
`queue_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: `queue-aiops-mcp` could start another tool's server, and
|
|
10
|
+
`queue-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 `queue-aiops-mcp` command is
|
|
13
|
+
unchanged; only code that imported `mcp_server` directly must now import
|
|
14
|
+
`queue_aiops.mcp_server`.
|
|
15
|
+
|
|
3
16
|
## v0.10.0 — 2026-10-15
|
|
4
17
|
|
|
5
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: queue-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) 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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queue-aiops"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11.0"
|
|
8
8
|
description = "Governed AI-ops for redis + rabbitmq: memory-pressure/latency/queue-backlog/connection-churn RCAs, slowlog and SCAN-budgeted big-key sampling, queues/policies/nodes, and governed writes (config set, client kill, purge/delete queue, policies) with a built-in governance harness (audit, budget, undo, risk tiers)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -29,10 +29,10 @@ dev = [
|
|
|
29
29
|
|
|
30
30
|
[project.scripts]
|
|
31
31
|
queue-aiops = "queue_aiops.cli:app"
|
|
32
|
-
queue-aiops-mcp = "mcp_server.server:main"
|
|
32
|
+
queue-aiops-mcp = "queue_aiops.mcp_server.server:main"
|
|
33
33
|
|
|
34
34
|
[tool.hatch.build.targets.wheel]
|
|
35
|
-
packages = ["queue_aiops"
|
|
35
|
+
packages = ["queue_aiops"]
|
|
36
36
|
|
|
37
37
|
[tool.ruff]
|
|
38
38
|
line-length = 100
|
|
@@ -48,7 +48,7 @@ select = ["E", "F", "I", "N", "W", "UP"]
|
|
|
48
48
|
"queue_aiops/governance/*.py" = ["E501", "N818"]
|
|
49
49
|
# MCP server files are reflected by FastMCP/Pydantic; PEP 604 unions (X | None)
|
|
50
50
|
# crash on older mcp/pydantic, so they intentionally use Optional[X].
|
|
51
|
-
"mcp_server/**/*.py" = ["UP007", "UP045"]
|
|
51
|
+
"queue_aiops/mcp_server/**/*.py" = ["UP007", "UP045"]
|
|
52
52
|
# The MCP entry keeps an explicit Python-version guard even though the declared
|
|
53
53
|
# minimum is already 3.11.
|
|
54
54
|
"queue_aiops/cli/_root.py" = ["UP036"]
|
|
@@ -155,7 +155,7 @@ def rabbit_purge(
|
|
|
155
155
|
dry_run: DryRunOption = False,
|
|
156
156
|
) -> None:
|
|
157
157
|
"""Purge all ready messages from a queue (IRREVERSIBLE, risk=high)."""
|
|
158
|
-
from mcp_server.tools import writes as gov
|
|
158
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
159
159
|
|
|
160
160
|
if dry_run:
|
|
161
161
|
# Through the governed twin, so the preview is audited and any harness
|
|
@@ -178,7 +178,7 @@ def rabbit_delete_queue(
|
|
|
178
178
|
dry_run: DryRunOption = False,
|
|
179
179
|
) -> None:
|
|
180
180
|
"""Delete a queue (risk=high; undo re-declares the captured definition)."""
|
|
181
|
-
from mcp_server.tools import writes as gov
|
|
181
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
182
182
|
|
|
183
183
|
if dry_run:
|
|
184
184
|
# Through the governed twin, so the preview is audited and any harness
|
|
@@ -207,7 +207,7 @@ def rabbit_declare_queue(
|
|
|
207
207
|
dry_run: DryRunOption = False,
|
|
208
208
|
) -> None:
|
|
209
209
|
"""Declare (create) a queue (undo deletes it when newly created)."""
|
|
210
|
-
from mcp_server.tools import writes as gov
|
|
210
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
211
211
|
|
|
212
212
|
if dry_run:
|
|
213
213
|
# Through the governed twin, so the preview is audited and any harness
|
|
@@ -242,7 +242,7 @@ def rabbit_set_policy(
|
|
|
242
242
|
dry_run: DryRunOption = False,
|
|
243
243
|
) -> None:
|
|
244
244
|
"""Create/update a policy (reversible — prior policy captured for undo)."""
|
|
245
|
-
from mcp_server.tools import writes as gov
|
|
245
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
246
246
|
|
|
247
247
|
try:
|
|
248
248
|
definition_obj = json.loads(definition)
|
|
@@ -276,7 +276,7 @@ def rabbit_delete_policy(
|
|
|
276
276
|
dry_run: DryRunOption = False,
|
|
277
277
|
) -> None:
|
|
278
278
|
"""Delete a policy (reversible — its definition is captured for undo)."""
|
|
279
|
-
from mcp_server.tools import writes as gov
|
|
279
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
280
280
|
|
|
281
281
|
if dry_run:
|
|
282
282
|
# Through the governed twin, so the preview is audited and any harness
|
|
@@ -134,7 +134,7 @@ def redis_config_set(
|
|
|
134
134
|
dry_run: DryRunOption = False,
|
|
135
135
|
) -> None:
|
|
136
136
|
"""Set one config parameter (reversible — prior value captured for undo)."""
|
|
137
|
-
from mcp_server.tools import writes as gov
|
|
137
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
138
138
|
|
|
139
139
|
if dry_run:
|
|
140
140
|
# Through the governed call: redis_config_set refuses the self-affecting
|
|
@@ -160,7 +160,7 @@ def redis_kill_client(
|
|
|
160
160
|
dry_run: DryRunOption = False,
|
|
161
161
|
) -> None:
|
|
162
162
|
"""Disconnect one client (irreversible; its info is captured as priorState)."""
|
|
163
|
-
from mcp_server.tools import writes as gov
|
|
163
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
164
164
|
|
|
165
165
|
who = str(client_id) if client_id else addr
|
|
166
166
|
if dry_run:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""``queue-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
|
+
``queue_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 governed).
|
|
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 queue_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 queue_aiops.mcp_server.tools import undo as gov
|
|
53
53
|
|
|
54
54
|
if dry_run:
|
|
55
55
|
# This path already routed through the governed call, but rendered the
|
|
@@ -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 ``queue_aiops/mcp_server/tools/`` import ``mcp`` from here and register
|
|
5
|
+
their ``@mcp.tool()`` functions onto it. ``queue_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 queue-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 ``queue_aiops/mcp_server/tools/``)
|
|
4
4
|
delegates to the ``queue_aiops`` ops package and is wrapped with the
|
|
5
5
|
queue-aiops ``@governed_tool`` harness (audit / budget / undo / risk-tier).
|
|
6
6
|
|
|
@@ -17,12 +17,12 @@ License: MIT
|
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
19
|
|
|
20
|
-
from mcp_server._annotations import apply_risk_annotations
|
|
21
|
-
from mcp_server._shared import _safe_error, mcp, tool_errors
|
|
20
|
+
from queue_aiops.mcp_server._annotations import apply_risk_annotations
|
|
21
|
+
from queue_aiops.mcp_server._shared import _safe_error, mcp, tool_errors
|
|
22
22
|
|
|
23
23
|
# Importing the tool modules registers every @mcp.tool() onto the shared
|
|
24
24
|
# `mcp` instance. Order does not matter; each module is self-contained.
|
|
25
|
-
from mcp_server.tools import ( # noqa: F401 — side effects
|
|
25
|
+
from queue_aiops.mcp_server.tools import ( # noqa: F401 — side effects
|
|
26
26
|
analysis,
|
|
27
27
|
overview,
|
|
28
28
|
rabbit_reads,
|
|
@@ -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 queue_aiops.governance import governed_tool
|
|
6
|
+
from queue_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from queue_aiops.ops import analysis as ops
|
|
8
8
|
from queue_aiops.ops._util import as_obj, num
|
|
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 queue_aiops.governance import governed_tool
|
|
6
|
+
from queue_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from queue_aiops.ops import overview 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 queue_aiops.governance import governed_tool
|
|
6
|
+
from queue_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from queue_aiops.ops import rabbit_reads 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 queue_aiops.governance import governed_tool
|
|
6
|
+
from queue_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
7
7
|
from queue_aiops.ops import redis_reads as ops
|
|
8
8
|
|
|
9
9
|
|
|
@@ -16,26 +16,26 @@ design, inverses in this line key off ids/names, not credentials.
|
|
|
16
16
|
import json
|
|
17
17
|
from typing import Any, Optional
|
|
18
18
|
|
|
19
|
-
from mcp_server._shared import mcp, tool_errors
|
|
20
19
|
from queue_aiops.governance import governed_tool
|
|
21
20
|
from queue_aiops.governance.undo import get_undo_store
|
|
21
|
+
from queue_aiops.mcp_server._shared import mcp, tool_errors
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _resolve_tool(name: str) -> Any:
|
|
25
25
|
"""Return the governed callable registered under ``name`` (or None).
|
|
26
26
|
|
|
27
27
|
The CLI reaches this through ``undo apply``, whose process only imports
|
|
28
|
-
``mcp_server.tools.undo`` — every other tool module is imported lazily inside
|
|
28
|
+
``queue_aiops.mcp_server.tools.undo`` — every other tool module is imported lazily inside
|
|
29
29
|
its own CLI command and so is absent here. Without forcing a full load,
|
|
30
30
|
``undo_apply`` would fail with "inverse tool not registered" for every write
|
|
31
31
|
tool when driven from the CLI (the MCP entry point loads all modules, so it
|
|
32
|
-
never hit this). If the first lookup misses, import ``mcp_server.server`` —
|
|
32
|
+
never hit this). If the first lookup misses, import ``queue_aiops.mcp_server.server`` —
|
|
33
33
|
which registers every ``@mcp.tool()`` as an import side effect — and retry.
|
|
34
34
|
Imported lazily to avoid the undo↔server cycle.
|
|
35
35
|
"""
|
|
36
36
|
tool = mcp._tool_manager._tools.get(name)
|
|
37
37
|
if tool is None:
|
|
38
|
-
import mcp_server.server # noqa: F401 — registers all tools as a side effect
|
|
38
|
+
import queue_aiops.mcp_server.server # noqa: F401 — registers all tools as a side effect
|
|
39
39
|
|
|
40
40
|
tool = mcp._tool_manager._tools.get(name)
|
|
41
41
|
return getattr(tool, "fn", None) if tool else None
|
|
@@ -13,8 +13,8 @@ delete_policy = medium.
|
|
|
13
13
|
|
|
14
14
|
from typing import Any, Optional
|
|
15
15
|
|
|
16
|
-
from mcp_server._shared import _get_connection, mcp, tool_errors
|
|
17
16
|
from queue_aiops.governance import governed_tool
|
|
17
|
+
from queue_aiops.mcp_server._shared import _get_connection, mcp, tool_errors
|
|
18
18
|
from queue_aiops.ops import writes as ops
|
|
19
19
|
|
|
20
20
|
# ── undo descriptors (built from the fetched before-state) ──────────────────
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/AIops-tools/Queue-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "0.11.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "queue-aiops",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.11.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -244,7 +244,7 @@ def _scanned_sources():
|
|
|
244
244
|
by the drift gate, so it is not this repo's to edit.
|
|
245
245
|
"""
|
|
246
246
|
root = Path(ops.__file__).parents[2]
|
|
247
|
-
found = sorted(root.glob("queue_aiops/**/*.py"))
|
|
247
|
+
found = sorted(root.glob("queue_aiops/**/*.py")) # includes queue_aiops/mcp_server
|
|
248
248
|
files = [p for p in found if "governance" not in p.parts]
|
|
249
249
|
assert len(files) > 10, f"source sweep found only {len(files)} modules — the glob is wrong"
|
|
250
250
|
return files
|
|
@@ -52,11 +52,12 @@ def _chain(fn):
|
|
|
52
52
|
|
|
53
53
|
def _calls_an_mcp_tool(fn) -> bool:
|
|
54
54
|
source = inspect.getsource(fn)
|
|
55
|
-
if "mcp_server" in source:
|
|
55
|
+
if "queue_aiops.mcp_server" in source:
|
|
56
56
|
return True
|
|
57
57
|
names = set()
|
|
58
58
|
for node in ast.parse(open(inspect.getsourcefile(fn)).read()).body:
|
|
59
|
-
if isinstance(node, ast.ImportFrom)
|
|
59
|
+
module = (node.module or "") if isinstance(node, ast.ImportFrom) else ""
|
|
60
|
+
if module.startswith("queue_aiops.mcp_server"):
|
|
60
61
|
names |= {a.asname or a.name for a in node.names}
|
|
61
62
|
used = {n.id for n in ast.walk(ast.parse(textwrap.dedent(source))) if isinstance(n, ast.Name)}
|
|
62
63
|
return bool(names & used)
|
|
@@ -28,7 +28,7 @@ import typer
|
|
|
28
28
|
from queue_aiops.cli._common import checked
|
|
29
29
|
|
|
30
30
|
CLI_DIR = pathlib.Path(__file__).resolve().parents[1] / "queue_aiops" / "cli"
|
|
31
|
-
TOOLS_DIR = pathlib.Path(__file__).resolve().parents[1] / "mcp_server" / "tools"
|
|
31
|
+
TOOLS_DIR = pathlib.Path(__file__).resolve().parents[1] / "queue_aiops" / "mcp_server" / "tools"
|
|
32
32
|
|
|
33
33
|
#: Helpers that end the command non-zero when the payload reports a failure.
|
|
34
34
|
CHECKERS = ['checked', 'dry_run_preview']
|
|
@@ -44,7 +44,7 @@ def gov_home(tmp_path, monkeypatch):
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def _wire(monkeypatch, routes):
|
|
47
|
-
from mcp_server.tools import writes as gov
|
|
47
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
48
48
|
|
|
49
49
|
conn = rabbit_conn(routes)
|
|
50
50
|
monkeypatch.setattr(gov, "_get_connection", lambda target=None: conn)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""CLI confirmed-write path — past dry-run, through governance, onto disk.
|
|
2
2
|
|
|
3
3
|
The CLI write commands delegate real execution to the ``@governed_tool``
|
|
4
|
-
functions in ``mcp_server.tools``. These tests drive ``redis config-set`` and
|
|
4
|
+
functions in ``queue_aiops.mcp_server.tools``. These tests drive ``redis config-set`` and
|
|
5
5
|
``rabbitmq purge`` PAST the dry-run branch and the double-confirm prompts and
|
|
6
6
|
assert the call really went through the governed path (audit row on disk) —
|
|
7
7
|
the regression test for the "CLI writes were unaudited" line-wide fix.
|
|
@@ -35,7 +35,7 @@ def gov_home(tmp_path, monkeypatch):
|
|
|
35
35
|
@pytest.fixture
|
|
36
36
|
def cache_conn(monkeypatch):
|
|
37
37
|
"""A fake redis connection wired into the governed write module."""
|
|
38
|
-
from mcp_server.tools import writes as gov
|
|
38
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
39
39
|
|
|
40
40
|
conn = redis_conn(config={"maxmemory-policy": "noeviction"})
|
|
41
41
|
monkeypatch.setattr(gov, "_get_connection", lambda target=None: conn)
|
|
@@ -45,7 +45,7 @@ def cache_conn(monkeypatch):
|
|
|
45
45
|
@pytest.fixture
|
|
46
46
|
def broker_conn(monkeypatch):
|
|
47
47
|
"""A fake rabbitmq connection wired into the governed write module."""
|
|
48
|
-
from mcp_server.tools import writes as gov
|
|
48
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
49
49
|
|
|
50
50
|
conn = rabbit_conn({
|
|
51
51
|
("GET", "/api/queues/%2F/orders"): {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Portable across the whole tool line: references ONLY
|
|
4
4
|
``queue_aiops.governance.audit`` and binds state to an isolated home via
|
|
5
|
-
``QUEUE_AIOPS_HOME``. No ops / cli / connection / mcp_server imports.
|
|
5
|
+
``QUEUE_AIOPS_HOME``. No ops / cli / connection / queue_aiops.mcp_server imports.
|
|
6
6
|
|
|
7
7
|
Exercises row writes with every column, query filters, stats aggregation,
|
|
8
8
|
size-based rotation + archive cleanup, permission hardening, in-place column
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Portable across the whole tool line: references ONLY
|
|
4
4
|
``queue_aiops.governance.*`` and binds state to an isolated home via
|
|
5
5
|
``QUEUE_AIOPS_HOME``. Every governed function under test is SYNTHETIC and
|
|
6
|
-
defined inline — no real ops / cli / connection / mcp_server tool is imported.
|
|
6
|
+
defined inline — no real ops / cli / connection / queue_aiops.mcp_server tool is imported.
|
|
7
7
|
|
|
8
8
|
Exercises the async wrapper (audit parity with sync), the error-capture path
|
|
9
9
|
with secret redaction, BudgetExceeded propagation, positional /
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Portable across the whole tool line: references ONLY
|
|
4
4
|
``queue_aiops.governance.patterns`` and binds state to an isolated home via
|
|
5
|
-
``QUEUE_AIOPS_HOME``. No ops / cli / connection / mcp_server imports.
|
|
5
|
+
``QUEUE_AIOPS_HOME``. No ops / cli / connection / queue_aiops.mcp_server imports.
|
|
6
6
|
|
|
7
7
|
Exercises pattern loading + signature validation, the armable preconditions,
|
|
8
8
|
skill/tool/target matching, rate limiting, the circuit breaker's state
|
|
@@ -124,7 +124,7 @@ def test_real_write_tool_persists_priorstate_undo(gov_home, monkeypatch):
|
|
|
124
124
|
"""End-to-end through a REAL product write tool: redis_config_set must
|
|
125
125
|
capture the parameter's prior value before setting and persist the
|
|
126
126
|
restore-undo on disk."""
|
|
127
|
-
from mcp_server.tools import writes as gov
|
|
127
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
128
128
|
from tests.fakes import redis_conn
|
|
129
129
|
|
|
130
130
|
conn = redis_conn(config={"maxmemory-policy": "noeviction"})
|
|
@@ -61,7 +61,7 @@ def wire_redis(monkeypatch):
|
|
|
61
61
|
for mod in ("analysis", "redis_reads", "overview"):
|
|
62
62
|
import importlib
|
|
63
63
|
|
|
64
|
-
m = importlib.import_module(f"mcp_server.tools.{mod}")
|
|
64
|
+
m = importlib.import_module(f"queue_aiops.mcp_server.tools.{mod}")
|
|
65
65
|
monkeypatch.setattr(m, "_get_connection", lambda target=None, _c=conn: _c)
|
|
66
66
|
return conn
|
|
67
67
|
|
|
@@ -71,7 +71,7 @@ def wire_redis(monkeypatch):
|
|
|
71
71
|
|
|
72
72
|
@pytest.mark.unit
|
|
73
73
|
def test_redis_read_wrappers_hit_expected_commands(wire_redis):
|
|
74
|
-
from mcp_server.tools import redis_reads as t
|
|
74
|
+
from queue_aiops.mcp_server.tools import redis_reads as t
|
|
75
75
|
|
|
76
76
|
assert t.redis_server_info()["version"] == "7.2.5"
|
|
77
77
|
assert t.redis_memory_stats()["usedPctOfMax"] == 92.0
|
|
@@ -84,7 +84,7 @@ def test_redis_read_wrappers_hit_expected_commands(wire_redis):
|
|
|
84
84
|
|
|
85
85
|
@pytest.mark.unit
|
|
86
86
|
def test_rabbit_read_wrappers_hit_expected_endpoints(monkeypatch):
|
|
87
|
-
from mcp_server.tools import rabbit_reads as t
|
|
87
|
+
from queue_aiops.mcp_server.tools import rabbit_reads as t
|
|
88
88
|
|
|
89
89
|
conn = rabbit_conn({
|
|
90
90
|
("GET", "/api/overview"): {"rabbitmq_version": "3.13.2",
|
|
@@ -113,7 +113,7 @@ def test_rabbit_read_wrappers_hit_expected_endpoints(monkeypatch):
|
|
|
113
113
|
|
|
114
114
|
@pytest.mark.unit
|
|
115
115
|
def test_queue_overview_wrapper_dispatches_by_platform(wire_redis):
|
|
116
|
-
from mcp_server.tools import overview as t
|
|
116
|
+
from queue_aiops.mcp_server.tools import overview as t
|
|
117
117
|
|
|
118
118
|
out = t.queue_overview()
|
|
119
119
|
assert out["platform"] == "redis"
|
|
@@ -125,7 +125,7 @@ def test_queue_overview_wrapper_dispatches_by_platform(wire_redis):
|
|
|
125
125
|
|
|
126
126
|
@pytest.mark.unit
|
|
127
127
|
def test_memory_pressure_rca_pulls_live_and_flags_noeviction_oom(wire_redis):
|
|
128
|
-
from mcp_server.tools import analysis as t
|
|
128
|
+
from queue_aiops.mcp_server.tools import analysis as t
|
|
129
129
|
|
|
130
130
|
out = t.redis_memory_pressure_rca()
|
|
131
131
|
assert out["pressure"] is True
|
|
@@ -137,7 +137,7 @@ def test_memory_pressure_rca_pulls_live_and_flags_noeviction_oom(wire_redis):
|
|
|
137
137
|
|
|
138
138
|
@pytest.mark.unit
|
|
139
139
|
def test_latency_rca_pulls_live_slowlog_and_stalls(wire_redis):
|
|
140
|
-
from mcp_server.tools import analysis as t
|
|
140
|
+
from queue_aiops.mcp_server.tools import analysis as t
|
|
141
141
|
|
|
142
142
|
out = t.redis_latency_rca()
|
|
143
143
|
top = out["slowlogPatterns"][0]
|
|
@@ -149,7 +149,7 @@ def test_latency_rca_pulls_live_slowlog_and_stalls(wire_redis):
|
|
|
149
149
|
|
|
150
150
|
@pytest.mark.unit
|
|
151
151
|
def test_memory_rca_injected_telemetry_skips_live_pull():
|
|
152
|
-
from mcp_server.tools import analysis as t
|
|
152
|
+
from queue_aiops.mcp_server.tools import analysis as t
|
|
153
153
|
|
|
154
154
|
out = t.redis_memory_pressure_rca(telemetry={
|
|
155
155
|
"memory": {"usedPctOfMax": 20.0, "maxmemoryBytes": 1000,
|
|
@@ -161,7 +161,7 @@ def test_memory_rca_injected_telemetry_skips_live_pull():
|
|
|
161
161
|
|
|
162
162
|
@pytest.mark.unit
|
|
163
163
|
def test_backlog_rca_pulls_live_queues_and_classifies_no_consumers(monkeypatch):
|
|
164
|
-
from mcp_server.tools import analysis as t
|
|
164
|
+
from queue_aiops.mcp_server.tools import analysis as t
|
|
165
165
|
|
|
166
166
|
conn = rabbit_conn({
|
|
167
167
|
("GET", "/api/queues"): [
|
|
@@ -178,7 +178,7 @@ def test_backlog_rca_pulls_live_queues_and_classifies_no_consumers(monkeypatch):
|
|
|
178
178
|
|
|
179
179
|
@pytest.mark.unit
|
|
180
180
|
def test_churn_analysis_pulls_live_redis_snapshot_and_flags_reconnects(wire_redis):
|
|
181
|
-
from mcp_server.tools import analysis as t
|
|
181
|
+
from queue_aiops.mcp_server.tools import analysis as t
|
|
182
182
|
|
|
183
183
|
out = t.connection_churn_analysis()
|
|
184
184
|
assert out["platform"] == "redis"
|
|
@@ -189,7 +189,7 @@ def test_churn_analysis_pulls_live_redis_snapshot_and_flags_reconnects(wire_redi
|
|
|
189
189
|
|
|
190
190
|
@pytest.mark.unit
|
|
191
191
|
def test_rca_tool_sanitises_broker_failure_into_error_envelope(monkeypatch):
|
|
192
|
-
from mcp_server.tools import analysis as t
|
|
192
|
+
from queue_aiops.mcp_server.tools import analysis as t
|
|
193
193
|
|
|
194
194
|
def boom(_target=None):
|
|
195
195
|
raise ValueError("redis down: 10.0.0.1:6379 unreachable")
|
|
@@ -202,8 +202,8 @@ def test_rca_tool_sanitises_broker_failure_into_error_envelope(monkeypatch):
|
|
|
202
202
|
|
|
203
203
|
@pytest.mark.unit
|
|
204
204
|
def test_read_and_rca_tools_are_low_risk():
|
|
205
|
-
from mcp_server.tools import analysis as a
|
|
206
|
-
from mcp_server.tools import redis_reads as r
|
|
205
|
+
from queue_aiops.mcp_server.tools import analysis as a
|
|
206
|
+
from queue_aiops.mcp_server.tools import redis_reads as r
|
|
207
207
|
|
|
208
208
|
for fn in (a.redis_memory_pressure_rca, a.redis_latency_rca,
|
|
209
209
|
a.rabbitmq_queue_backlog_rca, a.connection_churn_analysis,
|
|
@@ -159,7 +159,7 @@ def test_queue_list_carries_the_standard_envelope():
|
|
|
159
159
|
|
|
160
160
|
@pytest.mark.unit
|
|
161
161
|
def test_undo_list_envelope_measures_truncation(monkeypatch):
|
|
162
|
-
from mcp_server.tools import undo as undo_tools
|
|
162
|
+
from queue_aiops.mcp_server.tools import undo as undo_tools
|
|
163
163
|
|
|
164
164
|
rows = [
|
|
165
165
|
{
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The wheel ships exactly one top-level package, and nothing imports a shared one.
|
|
2
|
+
|
|
3
|
+
Every AIops tool used to install a top-level ``mcp_server`` package beside its
|
|
4
|
+
own. Two tools in one environment then merged into a single ``mcp_server``:
|
|
5
|
+
one tool's ``-mcp`` entry point started the other tool's server, and its CLI
|
|
6
|
+
``undo`` wrote audit rows into the other tool's home. The server now lives at
|
|
7
|
+
``<pkg>.mcp_server``. This file is identical across the AIops line.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import tomllib
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
PYPROJECT = tomllib.loads((ROOT / "pyproject.toml").read_text("utf-8"))
|
|
16
|
+
WHEEL_PACKAGES = PYPROJECT["tool"]["hatch"]["build"]["targets"]["wheel"]["packages"]
|
|
17
|
+
PKG = WHEEL_PACKAGES[0]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_wheel_ships_only_the_tool_package():
|
|
21
|
+
assert WHEEL_PACKAGES == [PKG]
|
|
22
|
+
assert PKG != "mcp_server"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_no_top_level_mcp_server_directory():
|
|
26
|
+
assert not (ROOT / "mcp_server").exists()
|
|
27
|
+
assert (ROOT / PKG / "mcp_server" / "server.py").is_file()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_mcp_entry_point_starts_this_tools_server():
|
|
31
|
+
entries = [v for k, v in PYPROJECT["project"]["scripts"].items() if k.endswith("-mcp")]
|
|
32
|
+
assert entries == [f"{PKG}.mcp_server.server:main"]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _imported_modules(path: Path) -> list[str]:
|
|
36
|
+
names: list[str] = []
|
|
37
|
+
for node in ast.walk(ast.parse(path.read_text("utf-8"))):
|
|
38
|
+
if isinstance(node, ast.ImportFrom) and node.level == 0 and node.module:
|
|
39
|
+
names.append(node.module)
|
|
40
|
+
elif isinstance(node, ast.Import):
|
|
41
|
+
names.extend(alias.name for alias in node.names)
|
|
42
|
+
return names
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_nothing_imports_a_top_level_mcp_server():
|
|
46
|
+
files = [p for d in (ROOT / PKG, ROOT / "tests") for p in d.rglob("*.py")]
|
|
47
|
+
assert files, "no source files found"
|
|
48
|
+
offenders = sorted(
|
|
49
|
+
f"{p.relative_to(ROOT)}: {name}"
|
|
50
|
+
for p in files
|
|
51
|
+
if "__pycache__" not in p.parts
|
|
52
|
+
for name in _imported_modules(p)
|
|
53
|
+
if name == "mcp_server" or name.startswith("mcp_server.")
|
|
54
|
+
)
|
|
55
|
+
assert not offenders, offenders
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_server_module_resolves_inside_the_package():
|
|
59
|
+
import importlib
|
|
60
|
+
|
|
61
|
+
server = importlib.import_module(f"{PKG}.mcp_server.server")
|
|
62
|
+
assert Path(server.__file__).resolve().is_relative_to((ROOT / PKG).resolve())
|
|
@@ -115,7 +115,7 @@ def test_self_lockout_is_a_valueerror():
|
|
|
115
115
|
|
|
116
116
|
@pytest.mark.unit
|
|
117
117
|
def test_dry_run_of_a_denylisted_parameter_is_refused(monkeypatch):
|
|
118
|
-
from mcp_server.tools import writes as t
|
|
118
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
119
119
|
|
|
120
120
|
conn = redis_conn(config={"requirepass": "current-password"})
|
|
121
121
|
monkeypatch.setattr(t, "_get_connection", lambda target=None: conn)
|
|
@@ -130,7 +130,7 @@ def test_dry_run_of_a_denylisted_parameter_is_refused(monkeypatch):
|
|
|
130
130
|
@pytest.mark.unit
|
|
131
131
|
def test_dry_run_of_an_ordinary_parameter_still_previews(monkeypatch):
|
|
132
132
|
"""The dry-run guard must be exact, not a blanket refusal of every preview."""
|
|
133
|
-
from mcp_server.tools import writes as t
|
|
133
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
134
134
|
|
|
135
135
|
conn = redis_conn(config={"maxmemory-policy": "noeviction"})
|
|
136
136
|
monkeypatch.setattr(t, "_get_connection", lambda target=None: conn)
|
|
@@ -159,8 +159,8 @@ def _cli_dry_run(monkeypatch, tmp_path, argv, conn):
|
|
|
159
159
|
import queue_aiops.governance.audit as audit_mod
|
|
160
160
|
import queue_aiops.governance.policy as policy_mod
|
|
161
161
|
import queue_aiops.governance.undo as undo_mod
|
|
162
|
-
from mcp_server.tools import writes as gov
|
|
163
162
|
from queue_aiops.cli import app
|
|
163
|
+
from queue_aiops.mcp_server.tools import writes as gov
|
|
164
164
|
|
|
165
165
|
monkeypatch.setenv("QUEUE_AIOPS_HOME", str(tmp_path))
|
|
166
166
|
audit_mod.reset_engine()
|
|
@@ -210,7 +210,7 @@ def test_the_refusal_reaches_the_agent_intact_through_the_mcp_layer(monkeypatch)
|
|
|
210
210
|
than replaced — but it is truncated. The route back sits at the END of the
|
|
211
211
|
message, so an over-long refusal loses exactly the part the caller acts on.
|
|
212
212
|
"""
|
|
213
|
-
from mcp_server.tools import writes as t
|
|
213
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
214
214
|
|
|
215
215
|
conn = redis_conn(config={"requirepass": "current-password"})
|
|
216
216
|
monkeypatch.setattr(t, "_get_connection", lambda target=None: conn)
|
|
@@ -12,7 +12,7 @@ import importlib
|
|
|
12
12
|
import pytest
|
|
13
13
|
from typer.testing import CliRunner
|
|
14
14
|
|
|
15
|
-
# Kept in sync with mcp_server/server.py (the full registered tool surface).
|
|
15
|
+
# Kept in sync with queue_aiops/mcp_server/server.py (the full registered tool surface).
|
|
16
16
|
EXPECTED_TOOLS = {
|
|
17
17
|
# overview
|
|
18
18
|
"queue_overview",
|
|
@@ -45,10 +45,10 @@ def test_all_modules_import():
|
|
|
45
45
|
"queue_aiops.cli.redis_cmds", "queue_aiops.cli.rabbit_cmds",
|
|
46
46
|
"queue_aiops.cli.analyze", "queue_aiops.cli.overview",
|
|
47
47
|
"queue_aiops.cli.doctor",
|
|
48
|
-
"mcp_server.server", "mcp_server._shared",
|
|
49
|
-
"mcp_server.tools.redis_reads", "mcp_server.tools.rabbit_reads",
|
|
50
|
-
"mcp_server.tools.analysis", "mcp_server.tools.writes",
|
|
51
|
-
"mcp_server.tools.overview",
|
|
48
|
+
"queue_aiops.mcp_server.server", "queue_aiops.mcp_server._shared",
|
|
49
|
+
"queue_aiops.mcp_server.tools.redis_reads", "queue_aiops.mcp_server.tools.rabbit_reads",
|
|
50
|
+
"queue_aiops.mcp_server.tools.analysis", "queue_aiops.mcp_server.tools.writes",
|
|
51
|
+
"queue_aiops.mcp_server.tools.overview",
|
|
52
52
|
):
|
|
53
53
|
importlib.import_module(name)
|
|
54
54
|
|
|
@@ -102,7 +102,7 @@ def test_cli_leaf_help_triggers_lazy_imports():
|
|
|
102
102
|
|
|
103
103
|
@pytest.mark.unit
|
|
104
104
|
def test_mcp_list_tools_exposes_expected_tools():
|
|
105
|
-
from mcp_server.server import mcp
|
|
105
|
+
from queue_aiops.mcp_server.server import mcp
|
|
106
106
|
|
|
107
107
|
tools = asyncio.run(mcp.list_tools())
|
|
108
108
|
names = {t.name for t in tools}
|
|
@@ -111,7 +111,7 @@ def test_mcp_list_tools_exposes_expected_tools():
|
|
|
111
111
|
|
|
112
112
|
@pytest.mark.unit
|
|
113
113
|
def test_every_mcp_tool_is_governed_by_harness():
|
|
114
|
-
from mcp_server import _shared
|
|
114
|
+
from queue_aiops.mcp_server import _shared
|
|
115
115
|
|
|
116
116
|
tool_objs = _shared.mcp._tool_manager._tools
|
|
117
117
|
assert EXPECTED_TOOLS <= set(tool_objs), "tool registry incomplete"
|
|
@@ -123,7 +123,7 @@ def test_every_mcp_tool_is_governed_by_harness():
|
|
|
123
123
|
|
|
124
124
|
@pytest.mark.unit
|
|
125
125
|
def test_tool_count_is_expected():
|
|
126
|
-
from mcp_server import _shared
|
|
126
|
+
from queue_aiops.mcp_server import _shared
|
|
127
127
|
|
|
128
128
|
assert len(_shared.mcp._tool_manager._tools) == 28
|
|
129
129
|
|
|
@@ -166,7 +166,7 @@ def test_risk_level_agrees_with_read_write_docstring_tag():
|
|
|
166
166
|
16 such mislabels line-wide once, so it is kept even though read-only mode
|
|
167
167
|
(its original motivation) is gone.
|
|
168
168
|
"""
|
|
169
|
-
from mcp_server import server
|
|
169
|
+
from queue_aiops.mcp_server import server
|
|
170
170
|
|
|
171
171
|
untagged, mismatched = [], []
|
|
172
172
|
for name, tool in server.mcp._tool_manager._tools.items():
|
|
@@ -6,18 +6,29 @@ here too. This file is identical across the AIops line.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import asyncio
|
|
9
|
+
import importlib
|
|
9
10
|
import re
|
|
11
|
+
from pathlib import Path
|
|
10
12
|
from types import SimpleNamespace
|
|
11
13
|
|
|
12
14
|
import pytest
|
|
13
15
|
from mcp.server import MCPServer
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
)
|
|
17
|
+
|
|
18
|
+
def _server_package() -> str:
|
|
19
|
+
"""``<pkg>.mcp_server``, found on disk so this file needs no per-tool name."""
|
|
20
|
+
root = Path(__file__).resolve().parents[1]
|
|
21
|
+
found = sorted(p.parent.parent.name for p in root.glob("*/mcp_server/server.py"))
|
|
22
|
+
assert len(found) == 1, f"expected exactly one <pkg>/mcp_server/server.py, found {found}"
|
|
23
|
+
return f"{found[0]}.mcp_server"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
SERVER_PACKAGE = _server_package()
|
|
27
|
+
server = importlib.import_module(f"{SERVER_PACKAGE}.server")
|
|
28
|
+
_annotations = importlib.import_module(f"{SERVER_PACKAGE}._annotations")
|
|
29
|
+
RISK_META_KEY = _annotations.RISK_META_KEY
|
|
30
|
+
annotations_for = _annotations.annotations_for
|
|
31
|
+
apply_risk_annotations = _annotations.apply_risk_annotations
|
|
21
32
|
|
|
22
33
|
|
|
23
34
|
@pytest.fixture(scope="module")
|
|
@@ -16,9 +16,9 @@ import pytest
|
|
|
16
16
|
import queue_aiops.governance.audit as audit_mod
|
|
17
17
|
import queue_aiops.governance.policy as policy_mod
|
|
18
18
|
import queue_aiops.governance.undo as undo_mod
|
|
19
|
-
from mcp_server._shared import mcp
|
|
20
|
-
from mcp_server.tools import undo as gov
|
|
21
19
|
from queue_aiops.governance import governed_tool
|
|
20
|
+
from queue_aiops.mcp_server._shared import mcp
|
|
21
|
+
from queue_aiops.mcp_server.tools import undo as gov
|
|
22
22
|
|
|
23
23
|
_CALLS: list[dict] = []
|
|
24
24
|
_TARGETS: list = []
|
|
@@ -211,7 +211,7 @@ def test_undo_apply_audits_both_wrapper_and_inverse(gov_home):
|
|
|
211
211
|
@pytest.mark.unit
|
|
212
212
|
def test_resolve_tool_loads_full_registry_under_cli_only_import():
|
|
213
213
|
"""Regression (live-found 2026-07-31): CLI ``undo apply`` runs in a process
|
|
214
|
-
that imports only ``mcp_server.tools.undo`` — every write tool is imported
|
|
214
|
+
that imports only ``queue_aiops.mcp_server.tools.undo`` — every write tool is imported
|
|
215
215
|
lazily inside its own CLI command, so the inverse was "not registered" and
|
|
216
216
|
CLI-initiated undo failed for every write tool. ``_resolve_tool`` must force
|
|
217
217
|
a full server load on a miss.
|
|
@@ -225,8 +225,8 @@ def test_resolve_tool_loads_full_registry_under_cli_only_import():
|
|
|
225
225
|
|
|
226
226
|
script = textwrap.dedent(
|
|
227
227
|
"""
|
|
228
|
-
from mcp_server._shared import mcp
|
|
229
|
-
import mcp_server.tools.undo as u
|
|
228
|
+
from queue_aiops.mcp_server._shared import mcp
|
|
229
|
+
import queue_aiops.mcp_server.tools.undo as u
|
|
230
230
|
before = set(mcp._tool_manager._tools)
|
|
231
231
|
# A miss must trigger the full-registry load, not just return None.
|
|
232
232
|
u._resolve_tool("__definitely_not_a_tool__")
|
|
@@ -215,8 +215,8 @@ def test_delete_policy_captures_prior_and_teaches_when_missing():
|
|
|
215
215
|
|
|
216
216
|
@pytest.mark.unit
|
|
217
217
|
def test_governed_config_set_records_undo_token(monkeypatch):
|
|
218
|
-
from mcp_server.tools import writes as t
|
|
219
218
|
from queue_aiops.governance.undo import get_undo_store
|
|
219
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
220
220
|
|
|
221
221
|
conn = redis_conn(config={"maxmemory-policy": "noeviction"})
|
|
222
222
|
monkeypatch.setattr(t, "_get_connection", lambda target=None: conn)
|
|
@@ -238,8 +238,8 @@ def test_governed_delete_queue_undo_replays_through_declare_queue(monkeypatch):
|
|
|
238
238
|
"""THE replay test: delete_queue's recorded undo, fed back into the
|
|
239
239
|
declare_queue tool with exactly its recorded params, must re-issue the PUT
|
|
240
240
|
with the captured definition."""
|
|
241
|
-
from mcp_server.tools import writes as t
|
|
242
241
|
from queue_aiops.governance.undo import get_undo_store
|
|
242
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
243
243
|
|
|
244
244
|
conn = rabbit_conn({
|
|
245
245
|
("GET", "/api/queues/%2F/orders"): _Q,
|
|
@@ -267,8 +267,8 @@ def test_governed_delete_queue_undo_replays_through_declare_queue(monkeypatch):
|
|
|
267
267
|
|
|
268
268
|
@pytest.mark.unit
|
|
269
269
|
def test_governed_set_policy_undo_replays_prior_policy(monkeypatch):
|
|
270
|
-
from mcp_server.tools import writes as t
|
|
271
270
|
from queue_aiops.governance.undo import get_undo_store
|
|
271
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
272
272
|
|
|
273
273
|
conn = rabbit_conn({
|
|
274
274
|
("GET", "/api/policies/%2F/lim"): _POLICY,
|
|
@@ -287,7 +287,7 @@ def test_governed_set_policy_undo_replays_prior_policy(monkeypatch):
|
|
|
287
287
|
|
|
288
288
|
@pytest.mark.unit
|
|
289
289
|
def test_undo_descriptor_shapes_invert_correctly():
|
|
290
|
-
from mcp_server.tools import writes as t
|
|
290
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
291
291
|
|
|
292
292
|
cfg = t._config_set_undo({"parameter": "maxmemory"}, {"priorState": {"value": "100mb"}})
|
|
293
293
|
assert cfg["tool"] == "redis_config_set"
|
|
@@ -332,7 +332,7 @@ def test_undo_descriptor_shapes_invert_correctly():
|
|
|
332
332
|
|
|
333
333
|
@pytest.mark.unit
|
|
334
334
|
def test_write_risk_tiers():
|
|
335
|
-
from mcp_server.tools import writes as t
|
|
335
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
336
336
|
|
|
337
337
|
assert t.purge_queue._risk_level == "high"
|
|
338
338
|
assert t.delete_queue._risk_level == "high"
|
|
@@ -350,7 +350,7 @@ _REDIS_MUTATORS = {"config_set", "client_kill_filter"}
|
|
|
350
350
|
@pytest.mark.unit
|
|
351
351
|
def test_dry_run_previews_do_not_mutate(monkeypatch):
|
|
352
352
|
"""The invariant is: a dry_run MAY read, it must never WRITE."""
|
|
353
|
-
from mcp_server.tools import writes as t
|
|
353
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
354
354
|
|
|
355
355
|
rconn = redis_conn(config={"maxmemory": "0"}, client_id=11)
|
|
356
356
|
qconn = rabbit_conn({("GET", "/api/queues/%2F/orders"): _Q})
|
|
@@ -372,8 +372,8 @@ def test_dry_run_previews_do_not_mutate(monkeypatch):
|
|
|
372
372
|
|
|
373
373
|
@pytest.mark.unit
|
|
374
374
|
def test_irreversible_writes_record_no_undo(monkeypatch):
|
|
375
|
-
from mcp_server.tools import writes as t
|
|
376
375
|
from queue_aiops.governance.undo import get_undo_store
|
|
376
|
+
from queue_aiops.mcp_server.tools import writes as t
|
|
377
377
|
|
|
378
378
|
conn = rabbit_conn({
|
|
379
379
|
("GET", "/api/queues/%2F/orders"): _Q,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|