Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions PYEGERIA_ISSUES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <module>
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
Expand Down Expand Up @@ -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 —
Expand Down
161 changes: 161 additions & 0 deletions tests/micro-tests/test_format_set_executor_token.py
Original file line number Diff line number Diff line change
@@ -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
Loading