queue-aiops 0.4.0__tar.gz → 0.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/CHANGELOG.md +11 -0
  2. queue_aiops-0.5.0/Dockerfile +9 -0
  3. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/PKG-INFO +2 -2
  4. queue_aiops-0.5.0/glama.json +6 -0
  5. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/_shared.py +11 -7
  6. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/undo.py +15 -1
  7. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/pyproject.toml +2 -2
  8. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/decorators.py +10 -2
  9. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/_util.py +4 -0
  10. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/server.json +2 -2
  11. queue_aiops-0.5.0/tests/test_int_fidelity.py +45 -0
  12. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_undo_executor.py +37 -0
  13. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/uv.lock +66 -26
  14. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/.github/workflows/mcp-publish.yml +0 -0
  15. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/.github/workflows/publish.yml +0 -0
  16. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/.gitignore +0 -0
  17. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/LICENSE +0 -0
  18. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/README.md +0 -0
  19. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/RELEASE_NOTES.md +0 -0
  20. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/SECURITY.md +0 -0
  21. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/docs/VERIFICATION.md +0 -0
  22. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/__init__.py +0 -0
  23. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/server.py +0 -0
  24. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/__init__.py +0 -0
  25. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/analysis.py +0 -0
  26. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/overview.py +0 -0
  27. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/rabbit_reads.py +0 -0
  28. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/redis_reads.py +0 -0
  29. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/mcp_server/tools/writes.py +0 -0
  30. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/__init__.py +0 -0
  31. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/__init__.py +0 -0
  32. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/_common.py +0 -0
  33. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/_root.py +0 -0
  34. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/analyze.py +0 -0
  35. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/doctor.py +0 -0
  36. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/init.py +0 -0
  37. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/overview.py +0 -0
  38. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/rabbit_cmds.py +0 -0
  39. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/redis_cmds.py +0 -0
  40. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/secret.py +0 -0
  41. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/cli/undo.py +0 -0
  42. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/config.py +0 -0
  43. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/connection.py +0 -0
  44. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/doctor.py +0 -0
  45. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/__init__.py +0 -0
  46. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/audit.py +0 -0
  47. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/budget.py +0 -0
  48. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/outcome.py +0 -0
  49. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/paths.py +0 -0
  50. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/patterns.py +0 -0
  51. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/policy.py +0 -0
  52. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/sanitize.py +0 -0
  53. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/governance/undo.py +0 -0
  54. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/__init__.py +0 -0
  55. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/analysis.py +0 -0
  56. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/overview.py +0 -0
  57. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/rabbit_reads.py +0 -0
  58. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/redis_reads.py +0 -0
  59. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/ops/writes.py +0 -0
  60. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/platform.py +0 -0
  61. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/queue_aiops/secretstore.py +0 -0
  62. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/skills/queue-aiops/SKILL.md +0 -0
  63. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/agent-guardrails.md +0 -0
  64. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/capabilities.md +0 -0
  65. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/cli-reference.md +0 -0
  66. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/skills/queue-aiops/references/setup-guide.md +0 -0
  67. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/smithery.yaml +0 -0
  68. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/conftest.py +0 -0
  69. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/fakes.py +0 -0
  70. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_analysis.py +0 -0
  71. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_cli_rabbit_writes.py +0 -0
  72. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_cli_reads.py +0 -0
  73. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_cli_secret.py +0 -0
  74. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_cli_writes.py +0 -0
  75. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_connection.py +0 -0
  76. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_doctor.py +0 -0
  77. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_gov_audit.py +0 -0
  78. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_gov_decorators.py +0 -0
  79. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_gov_patterns.py +0 -0
  80. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_gov_policy.py +0 -0
  81. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_governance_persistence.py +0 -0
  82. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_init.py +0 -0
  83. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_mcp_reads.py +0 -0
  84. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_optional_fields.py +0 -0
  85. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_platform.py +0 -0
  86. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_reads.py +0 -0
  87. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_secretstore.py +0 -0
  88. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_self_kill.py +0 -0
  89. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_self_lockout.py +0 -0
  90. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_smoke.py +0 -0
  91. {queue_aiops-0.4.0 → queue_aiops-0.5.0}/tests/test_writes.py +0 -0
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.5.0 — 2026-08-02
4
+
5
+ ### Changed (BREAKING)
6
+ - **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.
7
+
8
+ ### Fixed
9
+ - **`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.
10
+ - **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.
11
+ - **`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.
12
+
13
+
3
14
  ## v0.4.0 — 2026-07-21
4
15
 
5
16
  ### 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.4.0
3
+ Version: 0.5.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]<2.0,>=1.10
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
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://glama.ai/mcp/schemas/server.json",
3
+ "maintainers": [
4
+ "zw008"
5
+ ]
6
+ }
@@ -1,13 +1,15 @@
1
- """Shared MCP server primitives: the FastMCP instance, connection helper,
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 FastMCP-reflected
9
- tool signature — on older mcp/pydantic the union eval'd to ``types.UnionType``
10
- crashes FastMCP's ``issubclass`` check.
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.fastmcp import FastMCP
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 FastMCP still see the original signature.
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 = FastMCP(
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "queue-aiops"
7
- version = "0.4.0"
7
+ version = "0.5.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]>=1.10,<2.0",
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) and state.result.get("error"):
386
- state.status = "unknown" if is_unknown(state.result) else "error"
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):
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/AIops-tools/Queue-AIops",
8
8
  "source": "github"
9
9
  },
10
- "version": "0.4.0",
10
+ "version": "0.5.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "queue-aiops",
15
- "version": "0.4.0",
15
+ "version": "0.5.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
@@ -157,3 +157,40 @@ def test_undo_apply_audits_both_wrapper_and_inverse(gov_home):
157
157
  conn.close()
158
158
  assert "undo_apply" in tools
159
159
  assert "_undo_probe" in tools
160
+
161
+
162
+ @pytest.mark.unit
163
+ def test_resolve_tool_loads_full_registry_under_cli_only_import():
164
+ """Regression (live-found 2026-07-31): CLI ``undo apply`` runs in a process
165
+ that imports only ``mcp_server.tools.undo`` — every write tool is imported
166
+ lazily inside its own CLI command, so the inverse was "not registered" and
167
+ CLI-initiated undo failed for every write tool. ``_resolve_tool`` must force
168
+ a full server load on a miss.
169
+
170
+ Reproduced in a FRESH interpreter, because this pytest process has already
171
+ imported the whole server, which would mask the bug.
172
+ """
173
+ import subprocess
174
+ import sys
175
+ import textwrap
176
+
177
+ script = textwrap.dedent(
178
+ """
179
+ from mcp_server._shared import mcp
180
+ import mcp_server.tools.undo as u
181
+ before = set(mcp._tool_manager._tools)
182
+ # A miss must trigger the full-registry load, not just return None.
183
+ u._resolve_tool("__definitely_not_a_tool__")
184
+ after = set(mcp._tool_manager._tools)
185
+ new = after - before
186
+ assert new, "fallback loaded no additional tools"
187
+ sample = sorted(new)[0]
188
+ assert u._resolve_tool(sample) is not None, sample
189
+ print("OK", len(before), len(after))
190
+ """
191
+ )
192
+ r = subprocess.run(
193
+ [sys.executable, "-c", script], capture_output=True, text=True
194
+ )
195
+ assert r.returncode == 0, r.stderr
196
+ 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 = "httpx-sse"
372
- version = "0.4.3"
384
+ name = "httpx2"
385
+ version = "2.9.1"
373
386
  source = { registry = "https://pypi.org/simple" }
374
- sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" }
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/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
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 = "1.28.1"
458
+ version = "2.0.0"
439
459
  source = { registry = "https://pypi.org/simple" }
440
460
  dependencies = [
441
461
  { name = "anyio" },
442
- { name = "httpx" },
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/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" }
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/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" },
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.4.0"
815
+ version = "0.5.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 = ">=1.10,<2.0" },
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