queue-aiops 0.4.0__tar.gz → 0.6.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.4.0 → queue_aiops-0.6.0}/CHANGELOG.md +17 -0
- queue_aiops-0.6.0/Dockerfile +9 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/PKG-INFO +2 -2
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/docs/VERIFICATION.md +8 -1
- queue_aiops-0.6.0/glama.json +6 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/_shared.py +11 -7
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/undo.py +45 -3
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/pyproject.toml +2 -2
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/decorators.py +10 -2
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/_util.py +4 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/overview.py +4 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/redis_reads.py +36 -2
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/server.json +2 -2
- queue_aiops-0.6.0/tests/test_int_fidelity.py +45 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_reads.py +33 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_undo_executor.py +86 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/uv.lock +66 -26
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/.github/workflows/mcp-publish.yml +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/.github/workflows/publish.yml +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/.gitignore +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/LICENSE +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/README.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/RELEASE_NOTES.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/SECURITY.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/__init__.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/server.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/__init__.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/analysis.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/overview.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/rabbit_reads.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/redis_reads.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/mcp_server/tools/writes.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/__init__.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/__init__.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/_common.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/_root.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/analyze.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/doctor.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/init.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/overview.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/rabbit_cmds.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/redis_cmds.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/secret.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/cli/undo.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/config.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/connection.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/doctor.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/__init__.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/audit.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/budget.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/outcome.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/paths.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/patterns.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/policy.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/sanitize.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/governance/undo.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/__init__.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/analysis.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/rabbit_reads.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/ops/writes.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/platform.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/queue_aiops/secretstore.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/skills/queue-aiops/SKILL.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/skills/queue-aiops/references/agent-guardrails.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/skills/queue-aiops/references/capabilities.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/skills/queue-aiops/references/cli-reference.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/skills/queue-aiops/references/setup-guide.md +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/smithery.yaml +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/conftest.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/fakes.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_analysis.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_cli_rabbit_writes.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_cli_reads.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_cli_secret.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_cli_writes.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_connection.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_doctor.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_gov_audit.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_gov_decorators.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_gov_patterns.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_gov_policy.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_governance_persistence.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_init.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_mcp_reads.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_optional_fields.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_platform.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_secretstore.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_self_kill.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_self_lockout.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_smoke.py +0 -0
- {queue_aiops-0.4.0 → queue_aiops-0.6.0}/tests/test_writes.py +0 -0
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.6.0 — 2026-08-03
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **A cluster node's key count no longer poses as the whole dataset.** `INFO keyspace` on a Redis Cluster node answers for that node's slots only, so `keyspace` / `overview` reported `totalKeys: 100` for a 3-master cluster actually holding 300 — a dataset-wide claim wrong by the shard count, with nothing in the payload to say otherwise. Both now carry `scope` (`node` vs `server`), `clusterMode`, and on a cluster a note explaining that the figure covers one node. Measured on a real 3-master Redis 7.4 cluster (100 / 92 / 108).
|
|
7
|
+
- **`undo apply` replays against the target the original write ran on.** It dispatched the inverse against whatever target the *caller* named — in practice the config's first entry — while the write's own target sat unused in the undo record. On a multi-target config the inverse therefore ran against the wrong host; it only looks harmless because the resource usually is not there, but two hosts holding the same name and the inverse **succeeds on the wrong one, silently**. An explicitly named target still wins. Line-wide: all 24 copies had the identical defect. Caught live in container-host-aiops, where a stop recorded against a Podman target replayed against a Portainer one.
|
|
8
|
+
|
|
9
|
+
## v0.5.0 — 2026-08-02
|
|
10
|
+
|
|
11
|
+
### Changed (BREAKING)
|
|
12
|
+
- **Requires MCP SDK 2.0** (`mcp[cli]>=2.0,<3.0`). `mcp.server.fastmcp` no longer exists in 2.0; the server is now built with `MCPServer` and reports its package version in the stdio handshake.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- **`undo apply` works from the CLI.** Every write tool is imported lazily inside its own CLI command, so a CLI-driven undo ran in a process where the inverse tool was never registered and failed with "inverse tool is not registered" — for every write tool. Only the MCP entry point, which imports the whole server, worked. Found while live-verifying against a real cluster.
|
|
16
|
+
- **An undetermined outcome is audited `unknown`, not `ok`.** The harness only classified a result as undetermined when the payload *also* carried an `error` key, so a write that looked successful but had not been confirmed was recorded as a success.
|
|
17
|
+
- **`as_int` no longer round-trips integers through float64**, which cannot represent values above 2**53 exactly. A line-wide sweep found only one of six vendored copies had actually been fixed after the original precision bug. The bool guard precedes the int short-circuit because `bool` subclasses `int` — otherwise `True` would be returned unchanged and serialised as `true` rather than a number.
|
|
18
|
+
|
|
19
|
+
|
|
3
20
|
## v0.4.0 — 2026-07-21
|
|
4
21
|
|
|
5
22
|
### Changed (BREAKING)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# syntax=docker/dockerfile:1
|
|
2
|
+
# Minimal image for Glama introspection: starts the MCP server over stdio.
|
|
3
|
+
# The tools/list introspection handshake needs no live Redis/RabbitMQ credentials.
|
|
4
|
+
FROM python:3.12-slim
|
|
5
|
+
|
|
6
|
+
RUN pip install --no-cache-dir queue-aiops
|
|
7
|
+
|
|
8
|
+
# MCP server speaks JSON-RPC over stdio.
|
|
9
|
+
ENTRYPOINT ["queue-aiops-mcp"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: queue-aiops
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.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
|
|
@@ -8,7 +8,7 @@ License-File: LICENSE
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Requires-Dist: cryptography>=42.0
|
|
10
10
|
Requires-Dist: httpx<1.0,>=0.27
|
|
11
|
-
Requires-Dist: mcp[cli]<
|
|
11
|
+
Requires-Dist: mcp[cli]<3.0,>=2.0
|
|
12
12
|
Requires-Dist: pyyaml<7.0,>=6.0
|
|
13
13
|
Requires-Dist: redis<7.0,>=5.0
|
|
14
14
|
Requires-Dist: rich<16.0,>=13.0
|
|
@@ -29,6 +29,13 @@ plus a regression test that asserts the *type*, keeping genuine ratios
|
|
|
29
29
|
|
|
30
30
|
- **RabbitMQ** — the entire `rabbitmq` command group and its management-API
|
|
31
31
|
shapes are unit-tested only. This is now the largest gap in this repo.
|
|
32
|
-
- Redis **cluster / sentinel** topologies (only standalone was exercised)
|
|
32
|
+
- ~~Redis **cluster / sentinel** topologies (only standalone was exercised).~~
|
|
33
|
+
**Both exercised 2026-08-03**, and the cluster one found a defect: a node's
|
|
34
|
+
`totalKeys` was reported as the dataset total (100 on a 3-master cluster
|
|
35
|
+
holding 300). Reads, `overview` and the RCAs otherwise behaved correctly on a
|
|
36
|
+
cluster node, on a Sentinel-fronted primary, and on its replica (`role:
|
|
37
|
+
slave`). Still untested: an actual **failover** through Sentinel, and any
|
|
38
|
+
cluster-wide aggregate — this tool talks to one endpoint by design, which is
|
|
39
|
+
exactly why the scope marker matters.
|
|
33
40
|
- AUTH-enabled Redis and TLS connections.
|
|
34
41
|
- `kill-client` against a real blocked client.
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
"""Shared MCP server primitives: the
|
|
1
|
+
"""Shared MCP server primitives: the MCPServer instance, connection helper,
|
|
2
2
|
error sanitisation, and the ``@tool_errors`` decorator.
|
|
3
3
|
|
|
4
4
|
Tool modules under ``mcp_server/tools/`` import ``mcp`` from here and register
|
|
5
5
|
their ``@mcp.tool()`` functions onto it. ``mcp_server/server.py`` then imports
|
|
6
6
|
those modules and runs the server.
|
|
7
7
|
|
|
8
|
-
Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
Keep ``Optional[X]`` (never PEP 604 ``X | None``) in any reflected tool
|
|
9
|
+
signature. mcp>=2.0 accepts the union, but the legacy form is retained for
|
|
10
|
+
consistency and to stay safe under any pydantic reflection path (踩坑 #33 —
|
|
11
|
+
older mcp/pydantic eval'd the union to ``types.UnionType`` and crashed the
|
|
12
|
+
``issubclass`` check).
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
import functools
|
|
@@ -19,8 +21,9 @@ from typing import Any, Optional
|
|
|
19
21
|
|
|
20
22
|
import httpx
|
|
21
23
|
import redis
|
|
22
|
-
from mcp.server
|
|
24
|
+
from mcp.server import MCPServer
|
|
23
25
|
|
|
26
|
+
from queue_aiops import __version__
|
|
24
27
|
from queue_aiops.config import load_config
|
|
25
28
|
from queue_aiops.connection import ConnectionManager, QueueApiError
|
|
26
29
|
from queue_aiops.governance import mark_unknown, sanitize
|
|
@@ -78,7 +81,7 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
78
81
|
"""Wrap a tool body in the canonical try/except → ``_safe_error`` pattern.
|
|
79
82
|
|
|
80
83
|
Place this *between* ``@governed_tool`` and the function so the audit
|
|
81
|
-
decorator and
|
|
84
|
+
decorator and MCPServer still see the original signature.
|
|
82
85
|
"""
|
|
83
86
|
|
|
84
87
|
def decorator(func: Callable) -> Callable:
|
|
@@ -107,8 +110,9 @@ def tool_errors(shape: str = "dict") -> Callable:
|
|
|
107
110
|
return decorator
|
|
108
111
|
|
|
109
112
|
|
|
110
|
-
mcp =
|
|
113
|
+
mcp = MCPServer(
|
|
111
114
|
"queue-aiops",
|
|
115
|
+
version=__version__,
|
|
112
116
|
instructions=(
|
|
113
117
|
"Queue/cache broker operations over redis and rabbitmq: for a "
|
|
114
118
|
"redis target — server/memory/keyspace reads, clients, slowlog, config, "
|
|
@@ -22,8 +22,22 @@ from queue_aiops.governance.undo import get_undo_store
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def _resolve_tool(name: str) -> Any:
|
|
25
|
-
"""Return the governed callable registered under ``name`` (or None).
|
|
25
|
+
"""Return the governed callable registered under ``name`` (or None).
|
|
26
|
+
|
|
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
|
|
29
|
+
its own CLI command and so is absent here. Without forcing a full load,
|
|
30
|
+
``undo_apply`` would fail with "inverse tool not registered" for every write
|
|
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`` —
|
|
33
|
+
which registers every ``@mcp.tool()`` as an import side effect — and retry.
|
|
34
|
+
Imported lazily to avoid the undo↔server cycle.
|
|
35
|
+
"""
|
|
26
36
|
tool = mcp._tool_manager._tools.get(name)
|
|
37
|
+
if tool is None:
|
|
38
|
+
import mcp_server.server # noqa: F401 — registers all tools as a side effect
|
|
39
|
+
|
|
40
|
+
tool = mcp._tool_manager._tools.get(name)
|
|
27
41
|
return getattr(tool, "fn", None) if tool else None
|
|
28
42
|
|
|
29
43
|
|
|
@@ -73,6 +87,32 @@ def undo_list(limit: int = 50, target: Optional[str] = None) -> dict:
|
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
|
|
90
|
+
def _origin_target(rec: dict) -> Optional[str]:
|
|
91
|
+
"""The target the ORIGINAL write ran against, read from its recorded params.
|
|
92
|
+
|
|
93
|
+
Without this the replay goes to whatever target the caller names — in
|
|
94
|
+
practice the config's first entry — so on a multi-target config the inverse
|
|
95
|
+
runs against the wrong host. It only *looks* harmless because the resource
|
|
96
|
+
usually does not exist there; when two hosts both hold a resource with the
|
|
97
|
+
same name the inverse succeeds on the wrong one, silently.
|
|
98
|
+
|
|
99
|
+
Caught live on 2026-08-03 in container-host-aiops: a ``stop_container``
|
|
100
|
+
recorded against a Podman target replayed against a Portainer target.
|
|
101
|
+
|
|
102
|
+
A ``target`` in ``orig_params`` also means the original call accepted one,
|
|
103
|
+
so the fallback cannot hand a keyword to a tool that has no such parameter.
|
|
104
|
+
"""
|
|
105
|
+
raw = rec.get("orig_params") or ""
|
|
106
|
+
try:
|
|
107
|
+
orig = json.loads(raw) if raw else {}
|
|
108
|
+
except (ValueError, TypeError):
|
|
109
|
+
return None # unreadable: fall back to the caller/default, as before
|
|
110
|
+
if not isinstance(orig, dict):
|
|
111
|
+
return None
|
|
112
|
+
value = orig.get("target")
|
|
113
|
+
return value if isinstance(value, str) and value else None
|
|
114
|
+
|
|
115
|
+
|
|
76
116
|
@mcp.tool()
|
|
77
117
|
@governed_tool(risk_level="medium")
|
|
78
118
|
@tool_errors("dict")
|
|
@@ -136,8 +176,10 @@ def undo_apply(undo_id: str, dry_run: bool = False, target: Optional[str] = None
|
|
|
136
176
|
}
|
|
137
177
|
|
|
138
178
|
call_params = dict(params)
|
|
139
|
-
if
|
|
140
|
-
|
|
179
|
+
if "target" not in call_params:
|
|
180
|
+
chosen = target if target is not None else _origin_target(rec)
|
|
181
|
+
if chosen is not None:
|
|
182
|
+
call_params["target"] = chosen
|
|
141
183
|
result = fn(**call_params)
|
|
142
184
|
|
|
143
185
|
# Only mark applied when the inverse did not itself return an error dict.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "queue-aiops"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.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"
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"rich>=13.0,<16.0",
|
|
18
18
|
"pyyaml>=6.0,<7.0",
|
|
19
19
|
"cryptography>=42.0",
|
|
20
|
-
"mcp[cli]>=
|
|
20
|
+
"mcp[cli]>=2.0,<3.0",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[dependency-groups]
|
|
@@ -382,8 +382,16 @@ def _finalize(state: _CallState) -> None:
|
|
|
382
382
|
# compliance exception reports are built from this status. 'unknown' is a
|
|
383
383
|
# distinct verdict, not a softer 'error': the request may have taken effect,
|
|
384
384
|
# so the row must not assert a failure the tool cannot actually vouch for.
|
|
385
|
-
if state.status == "ok" and isinstance(state.result, dict)
|
|
386
|
-
|
|
385
|
+
if state.status == "ok" and isinstance(state.result, dict):
|
|
386
|
+
# is_unknown is checked FIRST and independently of ``error``. A write
|
|
387
|
+
# can be undetermined while looking perfectly successful — a Proxmox
|
|
388
|
+
# task that has not finished yet returns a normal payload — and gating
|
|
389
|
+
# this on ``error`` audited those as 'ok', re-creating the very lie the
|
|
390
|
+
# unknown verdict exists to prevent.
|
|
391
|
+
if is_unknown(state.result):
|
|
392
|
+
state.status = "unknown"
|
|
393
|
+
elif state.result.get("error"):
|
|
394
|
+
state.status = "error"
|
|
387
395
|
|
|
388
396
|
duration = int((time.time() - state.start) * 1000)
|
|
389
397
|
|
|
@@ -74,6 +74,10 @@ def as_int(value: Any) -> int:
|
|
|
74
74
|
Non-numeric/absent yields ``0`` to match :func:`num`'s contract for these
|
|
75
75
|
always-present INFO fields.
|
|
76
76
|
"""
|
|
77
|
+
if isinstance(value, bool): # bool subclasses int; not a quantity
|
|
78
|
+
return 0
|
|
79
|
+
if isinstance(value, int):
|
|
80
|
+
return value # already exact — never round-trip through float64
|
|
77
81
|
try:
|
|
78
82
|
return int(float(value))
|
|
79
83
|
except (TypeError, ValueError):
|
|
@@ -54,6 +54,10 @@ def _redis_overview(conn: Any) -> dict:
|
|
|
54
54
|
"maxmemoryPolicy": mem.get("maxmemoryPolicy"),
|
|
55
55
|
"fragmentationRatio": mem.get("fragmentationRatio"),
|
|
56
56
|
"totalKeys": keys.get("totalKeys"),
|
|
57
|
+
# On a cluster node totalKeys covers this node's slots only — the scope
|
|
58
|
+
# marker has to travel with the number, not be left in a sibling call.
|
|
59
|
+
"keyScope": keys.get("scope"),
|
|
60
|
+
"clusterMode": keys.get("clusterMode"),
|
|
57
61
|
"errors": errors,
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -190,8 +190,38 @@ def config_get(conn: Any, pattern: str = "*") -> dict:
|
|
|
190
190
|
return {"error": s(exc, 200)}
|
|
191
191
|
|
|
192
192
|
|
|
193
|
+
def _scope(conn: Any) -> dict:
|
|
194
|
+
"""Whether the counts that follow describe the whole dataset or one shard.
|
|
195
|
+
|
|
196
|
+
A Redis Cluster node answers ``INFO keyspace`` for **its own slots only**, so
|
|
197
|
+
on a three-master cluster holding 300 keys this reports 100 — a number that
|
|
198
|
+
is wrong by the shard count and, called ``totalKeys`` with nothing else in
|
|
199
|
+
the payload, reads as the whole dataset. Measured on a real 3-master Redis
|
|
200
|
+
7.4 cluster. The marker travels with the counts so a consumer can tell a
|
|
201
|
+
complete answer from a partial one.
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
mode = str(as_obj(conn.redis_info("server")).get("redis_mode") or "")
|
|
205
|
+
except Exception: # noqa: BLE001 — scope is an annotation, never the answer
|
|
206
|
+
return {"scope": "server", "clusterMode": None}
|
|
207
|
+
if mode != "cluster":
|
|
208
|
+
return {"scope": "server", "clusterMode": False}
|
|
209
|
+
return {
|
|
210
|
+
"scope": "node",
|
|
211
|
+
"clusterMode": True,
|
|
212
|
+
"note": (
|
|
213
|
+
"Redis Cluster: these counts cover THIS node's slots only, not the "
|
|
214
|
+
"whole cluster. Query each master, or use a cluster-aware client, "
|
|
215
|
+
"for a dataset-wide figure."
|
|
216
|
+
),
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
|
|
193
220
|
def keyspace(conn: Any) -> dict:
|
|
194
|
-
"""[READ] Per-db key counts + expiry coverage from INFO keyspace.
|
|
221
|
+
"""[READ] Per-db key counts + expiry coverage from INFO keyspace.
|
|
222
|
+
|
|
223
|
+
On a cluster node the counts are per-shard; the returned ``scope`` says so.
|
|
224
|
+
"""
|
|
195
225
|
try:
|
|
196
226
|
info = as_obj(conn.redis_info("keyspace"))
|
|
197
227
|
dbs = []
|
|
@@ -208,7 +238,11 @@ def keyspace(conn: Any) -> dict:
|
|
|
208
238
|
"avgTtlMs": as_int(cell.get("avg_ttl")),
|
|
209
239
|
}
|
|
210
240
|
)
|
|
211
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
"databases": dbs,
|
|
243
|
+
"totalKeys": sum(d["keys"] for d in dbs),
|
|
244
|
+
**_scope(conn),
|
|
245
|
+
}
|
|
212
246
|
except Exception as exc: # noqa: BLE001 — report as partial
|
|
213
247
|
return {"error": s(exc, 200)}
|
|
214
248
|
|
|
@@ -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.6.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "queue-aiops",
|
|
15
|
-
"version": "0.
|
|
15
|
+
"version": "0.6.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Integer quantities must stay exact integers.
|
|
2
|
+
|
|
3
|
+
Line-wide invariant, asserted per repo because the helper is vendored per repo.
|
|
4
|
+
It exists because of a live defect in proxy-aiops (Traefik route priority is an
|
|
5
|
+
int64; routing it through the float helper collapsed two *different*
|
|
6
|
+
priorities — …806 and …805 — onto the same displayed value). A 2026-08-02 sweep
|
|
7
|
+
found that only proxy-aiops had actually been fixed: five other ``as_int``
|
|
8
|
+
implementations, this one included, still round-tripped integers through
|
|
9
|
+
float64, which cannot represent values above 2**53 exactly.
|
|
10
|
+
|
|
11
|
+
The bool case is not pedantry: ``bool`` subclasses ``int``, so an int
|
|
12
|
+
short-circuit placed before a bool guard would return ``True`` unchanged and
|
|
13
|
+
serialise it as ``true`` instead of a number.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from queue_aiops.ops._util import as_int
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.mark.unit
|
|
24
|
+
def test_as_int_never_round_trips_an_int_through_float64():
|
|
25
|
+
hi = 9007199254740993 # 2**53 + 1 — the smallest int a float64 cannot hold
|
|
26
|
+
lo = 9007199254740992
|
|
27
|
+
assert as_int(hi) == hi
|
|
28
|
+
assert as_int(hi) != as_int(lo), "distinct counts must not collapse"
|
|
29
|
+
# What the float path would have done, kept as the reason this test exists:
|
|
30
|
+
assert int(float(hi)) == int(float(lo))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.mark.unit
|
|
34
|
+
def test_as_int_returns_a_real_int_not_a_bool():
|
|
35
|
+
result = as_int(True)
|
|
36
|
+
assert not isinstance(result, bool), "bool subclasses int; it is not a quantity"
|
|
37
|
+
assert result == 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.unit
|
|
41
|
+
def test_as_int_types_and_edges():
|
|
42
|
+
assert isinstance(as_int("42"), int) and as_int("42") == 42
|
|
43
|
+
assert as_int(3.9) == 3
|
|
44
|
+
assert as_int(None) == 0
|
|
45
|
+
assert as_int("nonsense") == 0
|
|
@@ -308,3 +308,36 @@ def test_queue_overview_rabbitmq_shape_degrades_partially():
|
|
|
308
308
|
assert out["platform"] == "rabbitmq"
|
|
309
309
|
assert out["queues"] == 12
|
|
310
310
|
assert any("nodes:" in e for e in out["errors"])
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
@pytest.mark.unit
|
|
314
|
+
def test_keyspace_marks_a_cluster_node_count_as_partial():
|
|
315
|
+
"""A cluster node's key count is per-shard and must say so.
|
|
316
|
+
|
|
317
|
+
``INFO keyspace`` on a Redis Cluster node answers for its own slots only, so
|
|
318
|
+
a 3-master cluster holding 300 keys reports 100 here. Emitting that as
|
|
319
|
+
``totalKeys`` with nothing else in the payload states a dataset-wide figure
|
|
320
|
+
that is wrong by the shard count. Measured on a real 3-master Redis 7.4
|
|
321
|
+
cluster: 100 / 92 / 108.
|
|
322
|
+
"""
|
|
323
|
+
conn = redis_conn(info={
|
|
324
|
+
"keyspace": {"db0": "keys=100,expires=0,avg_ttl=0"},
|
|
325
|
+
"server": {"redis_mode": "cluster"},
|
|
326
|
+
})
|
|
327
|
+
out = redis_reads.keyspace(conn)
|
|
328
|
+
assert out["totalKeys"] == 100
|
|
329
|
+
assert out["scope"] == "node"
|
|
330
|
+
assert out["clusterMode"] is True
|
|
331
|
+
assert "THIS node" in out["note"]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@pytest.mark.unit
|
|
335
|
+
def test_keyspace_on_a_standalone_server_is_scoped_whole():
|
|
336
|
+
conn = redis_conn(info={
|
|
337
|
+
"keyspace": {"db0": "keys=7,expires=0,avg_ttl=0"},
|
|
338
|
+
"server": {"redis_mode": "standalone"},
|
|
339
|
+
})
|
|
340
|
+
out = redis_reads.keyspace(conn)
|
|
341
|
+
assert out["scope"] == "server"
|
|
342
|
+
assert out["clusterMode"] is False
|
|
343
|
+
assert "note" not in out
|
|
@@ -21,18 +21,21 @@ from mcp_server.tools import undo as gov
|
|
|
21
21
|
from queue_aiops.governance import governed_tool
|
|
22
22
|
|
|
23
23
|
_CALLS: list[dict] = []
|
|
24
|
+
_TARGETS: list = []
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@governed_tool(risk_level="low")
|
|
27
28
|
def _undo_probe(value: str = "", target=None) -> dict:
|
|
28
29
|
"""Synthetic inverse target used only by the undo-executor tests."""
|
|
29
30
|
_CALLS.append({"value": value})
|
|
31
|
+
_TARGETS.append(target)
|
|
30
32
|
return {"ok": True, "value": value}
|
|
31
33
|
|
|
32
34
|
|
|
33
35
|
@pytest.fixture
|
|
34
36
|
def gov_home(tmp_path, monkeypatch):
|
|
35
37
|
_CALLS.clear()
|
|
38
|
+
_TARGETS.clear()
|
|
36
39
|
# Register the probe only for the duration of these tests so it never
|
|
37
40
|
# pollutes the real tool registry (which exact-count smoke tests assert on).
|
|
38
41
|
mcp.add_tool(_undo_probe, name="_undo_probe")
|
|
@@ -77,6 +80,52 @@ def test_undo_apply_dispatches_inverse_and_marks_applied(gov_home):
|
|
|
77
80
|
assert uid not in {u["undoId"] for u in gov.undo_list()["undos"]}
|
|
78
81
|
|
|
79
82
|
|
|
83
|
+
@pytest.mark.unit
|
|
84
|
+
def test_undo_apply_replays_against_the_target_the_write_ran_on(gov_home):
|
|
85
|
+
"""A replay must go to the host the original write went to.
|
|
86
|
+
|
|
87
|
+
Without this the inverse ran against whatever target the caller named — in
|
|
88
|
+
practice the config's first entry — so on a multi-target config it hit the
|
|
89
|
+
wrong host. It only looked harmless because the resource usually does not
|
|
90
|
+
exist there; two hosts holding the same name and the inverse succeeds on the
|
|
91
|
+
wrong one, silently. Caught live on 2026-08-03 in container-host-aiops: a
|
|
92
|
+
stop recorded against a Podman target replayed against a Portainer target.
|
|
93
|
+
"""
|
|
94
|
+
uid = undo_mod.get_undo_store().record(
|
|
95
|
+
skill="probe",
|
|
96
|
+
tool="orig_op",
|
|
97
|
+
undo_descriptor={"tool": "_undo_probe", "params": {"value": "v1"}},
|
|
98
|
+
orig_params={"value": "v1", "target": "second-host"},
|
|
99
|
+
)
|
|
100
|
+
result = gov.undo_apply(undo_id=uid)
|
|
101
|
+
assert result["applied"] is True
|
|
102
|
+
assert _TARGETS == ["second-host"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@pytest.mark.unit
|
|
106
|
+
def test_undo_apply_prefers_an_explicit_caller_target(gov_home):
|
|
107
|
+
"""An explicitly named target still wins — the fallback is only a default."""
|
|
108
|
+
uid = undo_mod.get_undo_store().record(
|
|
109
|
+
skill="probe",
|
|
110
|
+
tool="orig_op",
|
|
111
|
+
undo_descriptor={"tool": "_undo_probe", "params": {"value": "v1"}},
|
|
112
|
+
orig_params={"value": "v1", "target": "second-host"},
|
|
113
|
+
)
|
|
114
|
+
gov.undo_apply(undo_id=uid, target="chosen-host")
|
|
115
|
+
assert _TARGETS == ["chosen-host"]
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@pytest.mark.unit
|
|
119
|
+
def test_undo_apply_survives_unreadable_orig_params(gov_home):
|
|
120
|
+
"""Corrupt orig_params must not break the replay — just lose the fallback."""
|
|
121
|
+
uid = _record(params={"value": "v1"})
|
|
122
|
+
with sqlite3.connect(gov_home / "undo.db") as db:
|
|
123
|
+
db.execute("UPDATE undo_log SET orig_params = ? WHERE undo_id = ?",
|
|
124
|
+
("{not json", uid))
|
|
125
|
+
assert gov.undo_apply(undo_id=uid)["applied"] is True
|
|
126
|
+
assert _TARGETS == [None]
|
|
127
|
+
|
|
128
|
+
|
|
80
129
|
@pytest.mark.unit
|
|
81
130
|
def test_undo_apply_dry_run_previews_without_running(gov_home):
|
|
82
131
|
uid = _record()
|
|
@@ -157,3 +206,40 @@ def test_undo_apply_audits_both_wrapper_and_inverse(gov_home):
|
|
|
157
206
|
conn.close()
|
|
158
207
|
assert "undo_apply" in tools
|
|
159
208
|
assert "_undo_probe" in tools
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@pytest.mark.unit
|
|
212
|
+
def test_resolve_tool_loads_full_registry_under_cli_only_import():
|
|
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
|
|
215
|
+
lazily inside its own CLI command, so the inverse was "not registered" and
|
|
216
|
+
CLI-initiated undo failed for every write tool. ``_resolve_tool`` must force
|
|
217
|
+
a full server load on a miss.
|
|
218
|
+
|
|
219
|
+
Reproduced in a FRESH interpreter, because this pytest process has already
|
|
220
|
+
imported the whole server, which would mask the bug.
|
|
221
|
+
"""
|
|
222
|
+
import subprocess
|
|
223
|
+
import sys
|
|
224
|
+
import textwrap
|
|
225
|
+
|
|
226
|
+
script = textwrap.dedent(
|
|
227
|
+
"""
|
|
228
|
+
from mcp_server._shared import mcp
|
|
229
|
+
import mcp_server.tools.undo as u
|
|
230
|
+
before = set(mcp._tool_manager._tools)
|
|
231
|
+
# A miss must trigger the full-registry load, not just return None.
|
|
232
|
+
u._resolve_tool("__definitely_not_a_tool__")
|
|
233
|
+
after = set(mcp._tool_manager._tools)
|
|
234
|
+
new = after - before
|
|
235
|
+
assert new, "fallback loaded no additional tools"
|
|
236
|
+
sample = sorted(new)[0]
|
|
237
|
+
assert u._resolve_tool(sample) is not None, sample
|
|
238
|
+
print("OK", len(before), len(after))
|
|
239
|
+
"""
|
|
240
|
+
)
|
|
241
|
+
r = subprocess.run(
|
|
242
|
+
[sys.executable, "-c", script], capture_output=True, text=True
|
|
243
|
+
)
|
|
244
|
+
assert r.returncode == 0, r.stderr
|
|
245
|
+
assert r.stdout.startswith("OK"), r.stdout
|
|
@@ -352,6 +352,19 @@ wheels = [
|
|
|
352
352
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
|
353
353
|
]
|
|
354
354
|
|
|
355
|
+
[[package]]
|
|
356
|
+
name = "httpcore2"
|
|
357
|
+
version = "2.9.1"
|
|
358
|
+
source = { registry = "https://pypi.org/simple" }
|
|
359
|
+
dependencies = [
|
|
360
|
+
{ name = "h11" },
|
|
361
|
+
{ name = "truststore" },
|
|
362
|
+
]
|
|
363
|
+
sdist = { url = "https://files.pythonhosted.org/packages/39/a8/20ed1ed79cbc2ecdf5301c0968ab7c85547212e2a7bd126ddd2d986e206e/httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2", size = 67089, upload-time = "2026-07-24T09:21:03.867Z" }
|
|
364
|
+
wheels = [
|
|
365
|
+
{ url = "https://files.pythonhosted.org/packages/9f/fb/46c52b781975c335a2bcf1072c7bbc007cbdc8d674217f5ee1daba2c848b/httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26", size = 82809, upload-time = "2026-07-24T09:21:01.178Z" },
|
|
366
|
+
]
|
|
367
|
+
|
|
355
368
|
[[package]]
|
|
356
369
|
name = "httpx"
|
|
357
370
|
version = "0.28.1"
|
|
@@ -368,12 +381,19 @@ wheels = [
|
|
|
368
381
|
]
|
|
369
382
|
|
|
370
383
|
[[package]]
|
|
371
|
-
name = "
|
|
372
|
-
version = "
|
|
384
|
+
name = "httpx2"
|
|
385
|
+
version = "2.9.1"
|
|
373
386
|
source = { registry = "https://pypi.org/simple" }
|
|
374
|
-
|
|
387
|
+
dependencies = [
|
|
388
|
+
{ name = "anyio" },
|
|
389
|
+
{ name = "httpcore2" },
|
|
390
|
+
{ name = "idna" },
|
|
391
|
+
{ name = "truststore" },
|
|
392
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
393
|
+
]
|
|
394
|
+
sdist = { url = "https://files.pythonhosted.org/packages/21/14/38128fbafd7e0ed41d874df6c9a653d47c2d111cfe59e2b4ac95161b4abd/httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a", size = 95458, upload-time = "2026-07-24T09:21:04.972Z" }
|
|
375
395
|
wheels = [
|
|
376
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
396
|
+
{ url = "https://files.pythonhosted.org/packages/13/b8/cfd91c4ab9134d386d48f0b6ac662ff3d4be6efdee59ee1c67ebc3c0487c/httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a", size = 91191, upload-time = "2026-07-24T09:21:02.6Z" },
|
|
377
397
|
]
|
|
378
398
|
|
|
379
399
|
[[package]]
|
|
@@ -435,15 +455,15 @@ wheels = [
|
|
|
435
455
|
|
|
436
456
|
[[package]]
|
|
437
457
|
name = "mcp"
|
|
438
|
-
version = "
|
|
458
|
+
version = "2.0.0"
|
|
439
459
|
source = { registry = "https://pypi.org/simple" }
|
|
440
460
|
dependencies = [
|
|
441
461
|
{ name = "anyio" },
|
|
442
|
-
{ name = "
|
|
443
|
-
{ name = "httpx-sse" },
|
|
462
|
+
{ name = "httpx2" },
|
|
444
463
|
{ name = "jsonschema" },
|
|
464
|
+
{ name = "mcp-types" },
|
|
465
|
+
{ name = "opentelemetry-api" },
|
|
445
466
|
{ name = "pydantic" },
|
|
446
|
-
{ name = "pydantic-settings" },
|
|
447
467
|
{ name = "pyjwt", extra = ["crypto"] },
|
|
448
468
|
{ name = "python-multipart" },
|
|
449
469
|
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
|
@@ -453,9 +473,9 @@ dependencies = [
|
|
|
453
473
|
{ name = "typing-inspection" },
|
|
454
474
|
{ name = "uvicorn", marker = "sys_platform != 'emscripten'" },
|
|
455
475
|
]
|
|
456
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
476
|
+
sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" }
|
|
457
477
|
wheels = [
|
|
458
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
478
|
+
{ url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" },
|
|
459
479
|
]
|
|
460
480
|
|
|
461
481
|
[package.optional-dependencies]
|
|
@@ -464,6 +484,19 @@ cli = [
|
|
|
464
484
|
{ name = "typer" },
|
|
465
485
|
]
|
|
466
486
|
|
|
487
|
+
[[package]]
|
|
488
|
+
name = "mcp-types"
|
|
489
|
+
version = "2.0.0"
|
|
490
|
+
source = { registry = "https://pypi.org/simple" }
|
|
491
|
+
dependencies = [
|
|
492
|
+
{ name = "pydantic" },
|
|
493
|
+
{ name = "typing-extensions" },
|
|
494
|
+
]
|
|
495
|
+
sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" }
|
|
496
|
+
wheels = [
|
|
497
|
+
{ url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" },
|
|
498
|
+
]
|
|
499
|
+
|
|
467
500
|
[[package]]
|
|
468
501
|
name = "mdurl"
|
|
469
502
|
version = "0.1.2"
|
|
@@ -473,6 +506,18 @@ wheels = [
|
|
|
473
506
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
474
507
|
]
|
|
475
508
|
|
|
509
|
+
[[package]]
|
|
510
|
+
name = "opentelemetry-api"
|
|
511
|
+
version = "1.44.0"
|
|
512
|
+
source = { registry = "https://pypi.org/simple" }
|
|
513
|
+
dependencies = [
|
|
514
|
+
{ name = "typing-extensions" },
|
|
515
|
+
]
|
|
516
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" }
|
|
517
|
+
wheels = [
|
|
518
|
+
{ url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" },
|
|
519
|
+
]
|
|
520
|
+
|
|
476
521
|
[[package]]
|
|
477
522
|
name = "packaging"
|
|
478
523
|
version = "26.2"
|
|
@@ -617,20 +662,6 @@ wheels = [
|
|
|
617
662
|
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
|
618
663
|
]
|
|
619
664
|
|
|
620
|
-
[[package]]
|
|
621
|
-
name = "pydantic-settings"
|
|
622
|
-
version = "2.14.2"
|
|
623
|
-
source = { registry = "https://pypi.org/simple" }
|
|
624
|
-
dependencies = [
|
|
625
|
-
{ name = "pydantic" },
|
|
626
|
-
{ name = "python-dotenv" },
|
|
627
|
-
{ name = "typing-inspection" },
|
|
628
|
-
]
|
|
629
|
-
sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" }
|
|
630
|
-
wheels = [
|
|
631
|
-
{ url = "https://files.pythonhosted.org/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" },
|
|
632
|
-
]
|
|
633
|
-
|
|
634
665
|
[[package]]
|
|
635
666
|
name = "pygments"
|
|
636
667
|
version = "2.20.0"
|
|
@@ -781,7 +812,7 @@ wheels = [
|
|
|
781
812
|
|
|
782
813
|
[[package]]
|
|
783
814
|
name = "queue-aiops"
|
|
784
|
-
version = "0.
|
|
815
|
+
version = "0.6.0"
|
|
785
816
|
source = { editable = "." }
|
|
786
817
|
dependencies = [
|
|
787
818
|
{ name = "cryptography" },
|
|
@@ -804,7 +835,7 @@ dev = [
|
|
|
804
835
|
requires-dist = [
|
|
805
836
|
{ name = "cryptography", specifier = ">=42.0" },
|
|
806
837
|
{ name = "httpx", specifier = ">=0.27,<1.0" },
|
|
807
|
-
{ name = "mcp", extras = ["cli"], specifier = ">=
|
|
838
|
+
{ name = "mcp", extras = ["cli"], specifier = ">=2.0,<3.0" },
|
|
808
839
|
{ name = "pyyaml", specifier = ">=6.0,<7.0" },
|
|
809
840
|
{ name = "redis", specifier = ">=5.0,<7.0" },
|
|
810
841
|
{ name = "rich", specifier = ">=13.0,<16.0" },
|
|
@@ -1094,6 +1125,15 @@ wheels = [
|
|
|
1094
1125
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
|
1095
1126
|
]
|
|
1096
1127
|
|
|
1128
|
+
[[package]]
|
|
1129
|
+
name = "truststore"
|
|
1130
|
+
version = "0.10.4"
|
|
1131
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1132
|
+
sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" }
|
|
1133
|
+
wheels = [
|
|
1134
|
+
{ url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" },
|
|
1135
|
+
]
|
|
1136
|
+
|
|
1097
1137
|
[[package]]
|
|
1098
1138
|
name = "typer"
|
|
1099
1139
|
version = "0.27.0"
|
|
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
|
|
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
|