diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index 3ea041a8..a79e2a85 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -199,6 +199,50 @@ security context changes; (2) quickstart content: give `generalnpa` read access elements its engine actions anchor to, or anchor those actions to elements the engine-host identity can read. Full draft: trellis session scratch `egeria-issue-engine-host-403-loop.md`. +### ISSUE-91: `pyegeria.core.mcp_server` imports `mcp.server.mcpserver` (mcp 2.x only) but `pyproject.toml` declares `mcp >=0.1` — any consumer that resolves mcp 1.x gets a server that dies at import + +**Layer:** pyegeria packaging · **Status:** open · **Found:** 2026-09-05 (Egeria Advisor dev startup on the M3 Max) + +Commit 2b39ba06 (2026-07-30, "migrate mcp_server.py to mcp 2.0.0's MCPServer") changed the +server's import to: + +```python +from mcp.server.mcpserver import MCPServer +``` + +That module exists only in `mcp >= 2.0.0` (1.x ships `mcp.server.fastmcp` instead). The +dependency declaration was not updated and still reads `"mcp >=0.1"` — verified in the released +6.1.5 wheel's METADATA (`Requires-Dist: mcp>=0.1`) and in the current `pyproject.toml` at 6.1.10. + +**How it shows up.** trellis pinned `mcp>=1.0.0` and its lock resolved mcp 1.29.0, which satisfies +pyegeria's declared range. Launching the server then fails before it can speak MCP: + +``` +$ python -m pyegeria.core.mcp_server +MCP import failed. + File ".../pyegeria/core/mcp_server.py", line 24, in + from mcp.server.mcpserver import MCPServer +ModuleNotFoundError: No module named 'mcp.server.mcpserver' +``` + +A client sees only "No response from MCP server" because the traceback goes to stderr and the +process exits without writing a JSON-RPC frame. With mcp 2.1.1 installed the same command answers +`initialize` normally (`serverInfo.name = "pyegeria-mcp"`). + +**Why it went unnoticed.** The quickstart containers run mcp 2.1.1 (pyegeria 6.1.9) and use their own +`/app/mcp_server.py` mounted over SSE in `pyegeria_handler`, not this stdio module, so the +egeria-workspaces path never exercised it. The egeria-python checkout's own venv also has mcp 2.0.0. + +**Proposed fix (backward compatible for callers).** In `pyproject.toml` declare `"mcp >=2.0"`. Nothing +else needs to change: the import is already 2.x-only, so raising the floor only turns a runtime +import crash into a resolver error at install time. If 1.x support is wanted instead, gate the +import (`try: from mcp.server.mcpserver import MCPServer except ImportError: from mcp.server.fastmcp +import FastMCP as MCPServer`) — but the 2.x API differs beyond the class name, so the floor bump is +the honest option. + +**Consumer-side workaround applied in trellis (8441efb):** both packages now declare `mcp>=2.0.0` +and the lock carries mcp 2.1.1, matching the quickstart containers. + ### ISSUE-38 (PY-18): `count_relationships_between_elements("Exception")` (276) disagrees with `ClassificationExplorer.get_relationships("Exception")` (55) **Update 2026-08-30, from the Egeria team (Mandy Chessell).** Leaving this @@ -751,6 +795,37 @@ on an Egeria Server capability that doesn't exist yet — but the pyegeria/ Dr.Egeria-side work each will need once that capability ships is written into the entry now, so it isn't rediscovered from scratch later. +### ISSUE-91: `pyproject.toml` declares `mcp >=0.1`, but `pyegeria.core.mcp_server` needs `mcp>=2.0` — the declared floor lets a resolver install a version too old to import the module at all + +**Layer:** Pyegeria · **Status:** open · **Found:** 2026-09-06 (Egeria Advisor, containerized demo deployment rebuild against pyegeria 6.1.10). + +`pyproject.toml`'s `[project.dependencies]` declares `"mcp >=0.1"`, but +`pyegeria/core/mcp_server.py` imports `from mcp.server.mcpserver import +MCPServer` — confirmed live in this checkout's dev venv: +`importlib.metadata.version("mcp")` is `2.0.0`, and +`mcp.server.mcpserver.MCPServer` only exists at that version; the module +path is new to the 2.x line, not present in the 0.x/1.x `mcp` package +history. A resolver that's free to pick anything satisfying `>=0.1` (no +upper or tighter lower bound forcing 2.x) can legitimately land on a much +older `mcp` release, at which point `import pyegeria.core.mcp_server` +fails outright rather than degrading gracefully. + +**Why it matters, found how:** a container rebuild of Egeria Advisor's +demo deployment against pyegeria 6.1.10 pinned `mcp==2.1.1` explicitly +(not resolved from pyegeria's own floor) and confirmed EA's MCP agent +pre-warms cleanly on it — so 6.1.10 itself works fine when the caller +pins a modern `mcp`, but nothing in pyegeria's own declared dependency +would have caught a caller who didn't. + +**Ask:** tighten `pyproject.toml`'s `mcp` constraint to actually match +what `mcp_server.py` requires (`mcp>=2.0` at minimum — confirm the exact +version `MCPServer`/`mcp.server.mcpserver` was introduced at, and pin to +that) rather than the inherited placeholder `>=0.1` floor. Not +independently verified against the `mcp` package's own changelog/git +history here — the live import check above shows 2.0.0 works and the +module path is absent from the 0.x/1.x line by inspection, but the exact +first-working version wasn't pinned down. + ### ISSUE-87: `ClassificationExplorer.add_ownership_to_element`'s docstring sample body says `"class": "OwnerProperties"` — the method itself only accepts `"OwnershipProperties"`, so the documented body cannot be sent **Layer:** Pyegeria · **Status:** fixed 2026-09-05 (Pyegeria — diff --git a/tests/micro-tests/test_format_set_executor_token.py b/tests/micro-tests/test_format_set_executor_token.py new file mode 100644 index 00000000..d4ea5360 --- /dev/null +++ b/tests/micro-tests/test_format_set_executor_token.py @@ -0,0 +1,161 @@ +"""ISSUE-86: `exec_report_spec(..., token=...)` must authenticate the client it +builds with `set_bearer_token(token)` instead of minting one from +user/user_pass, on BOTH client-building paths (the format-row/find path and +the analytic_function path). With no token, behaviour is unchanged +(`create_egeria_bearer_token()`).""" +import pytest + +from pyegeria.view import format_set_executor as fse +from pyegeria.core import mcp_adapter + + +class _RecordingClient: + """Stands in for any OMVS client class the executor instantiates.""" + instances: list = [] + + def __init__(self, view_server, view_url, user_id=None, user_pwd=None, **kw): + self.view_server = view_server + self.platform_url = view_url + self.user_id = user_id + self.user_pwd = user_pwd + self.token = None + self.calls: list[str] = [] + _RecordingClient.instances.append(self) + + def create_egeria_bearer_token(self, *a, **kw): + self.calls.append("create") + self.token = "minted" + return self.token + + def set_bearer_token(self, token): + self.calls.append("set") + self.token = token + + def find_things(self, **kwargs): + return [{"guid": "1", "displayName": "x"}] + + +@pytest.fixture(autouse=True) +def _reset_instances(): + _RecordingClient.instances = [] + yield + _RecordingClient.instances = [] + + +_FIND_FMT = { + "action": { + "function": "Fake.find_things", + "required_params": [], + "optional_params": [], + "spec_params": {}, + }, + "target_type": "Referenceable", +} + +_ANALYTIC_FMT = { + "action": {"analytic_function": "fake.analytic", "analytic_spec_params": {}}, + "target_type": "Referenceable", +} + + +def _patch_find_path(monkeypatch): + monkeypatch.setattr(fse, "select_report_spec", lambda name, out: _FIND_FMT) + monkeypatch.setattr(fse, "get_report_registry", lambda: {}) + monkeypatch.setattr(fse, "_resolve_client_and_method", + lambda decl: (_RecordingClient, "find_things")) + + +def _patch_analytic_path(monkeypatch): + def _analytic(client): + return {"count": 3, "token_seen": client.token} + monkeypatch.setattr(fse, "select_report_spec", lambda name, out: _ANALYTIC_FMT) + monkeypatch.setattr(fse, "get_report_registry", lambda: {}) + monkeypatch.setattr(fse, "_resolve_analytic_function", lambda decl: _analytic) + monkeypatch.setattr(fse, "EgeriaTech", _RecordingClient) + + +# --- format-row (find_method) path ------------------------------------------- + +def test_find_path_with_token_uses_set_bearer_token(monkeypatch): + _patch_find_path(monkeypatch) + result = fse.exec_report_spec( + "Anything", output_format="DICT", view_server="vs", view_url="https://x", + user="svc", user_pass="svc-pw", token="user-token", + ) + assert result["kind"] == "json" + (client,) = _RecordingClient.instances + assert client.calls == ["set"] + assert client.token == "user-token" + + +def test_find_path_without_token_mints_bearer_token(monkeypatch): + _patch_find_path(monkeypatch) + result = fse.exec_report_spec( + "Anything", output_format="DICT", view_server="vs", view_url="https://x", + user="svc", user_pass="svc-pw", + ) + assert result["kind"] == "json" + (client,) = _RecordingClient.instances + assert client.calls == ["create"] + assert client.user_id == "svc" and client.user_pwd == "svc-pw" + + +# --- analytic_function path --------------------------------------------------- + +def test_analytic_path_with_token_uses_set_bearer_token(monkeypatch): + _patch_analytic_path(monkeypatch) + result = fse.exec_report_spec( + "Anything", output_format="DICT", view_server="vs", view_url="https://x", + user="svc", user_pass="svc-pw", token="user-token", + ) + assert result == {"kind": "json", "data": {"count": 3, "token_seen": "user-token"}} + (client,) = _RecordingClient.instances + assert client.calls == ["set"] + + +def test_analytic_path_without_token_mints_bearer_token(monkeypatch): + _patch_analytic_path(monkeypatch) + result = fse.exec_report_spec( + "Anything", output_format="DICT", view_server="vs", view_url="https://x", + user="svc", user_pass="svc-pw", + ) + assert result["data"]["token_seen"] == "minted" + (client,) = _RecordingClient.instances + assert client.calls == ["create"] + + +def test_chart_path_threads_token(monkeypatch): + """SERIES/BAR/PIE dispatch to _exec_analytic_chart before the Format-row + lookup -- the token must survive that hop too.""" + _patch_analytic_path(monkeypatch) + monkeypatch.setattr(fse, "get_report_spec_heading", lambda name: "H", raising=False) + fse.exec_report_spec( + "Anything", output_format="BAR", view_server="vs", view_url="https://x", + user="svc", user_pass="svc-pw", token="user-token", + ) + (client,) = _RecordingClient.instances + assert client.calls == ["set"] + assert client.token == "user-token" + + +# --- MCP adapter passthrough -------------------------------------------------- + +def test_mcp_run_report_forwards_token(monkeypatch): + seen = {} + + def _fake_exec(**kwargs): + seen.update(kwargs) + return {"kind": "empty"} + + monkeypatch.setattr(mcp_adapter, "exec_report_spec", _fake_exec) + mcp_adapter.run_report(report="R", user="u", user_pass="p", token="tok") + assert seen["token"] == "tok" + assert seen["user"] == "u" and seen["user_pass"] == "p" + + +def test_mcp_run_report_default_token_is_none(monkeypatch): + seen = {} + monkeypatch.setattr(mcp_adapter, "exec_report_spec", + lambda **kw: seen.update(kw) or {"kind": "empty"}) + mcp_adapter.run_report(report="R", user="u", user_pass="p") + assert seen["token"] is None