From 39ddc896b11143e1904e8f8a8e14424656a117ac Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sun, 6 Sep 2026 17:12:39 -0500 Subject: [PATCH 1/3] docs(PYEGERIA_ISSUES): log ISSUE-91 -- mcp dependency floor too loose pyproject.toml declares mcp >=0.1 but pyegeria/core/mcp_server.py imports mcp.server.mcpserver.MCPServer, which requires mcp>=2.0 (confirmed live: this dev venv has mcp==2.0.0 installed and the import works; the module path doesn't exist in the 0.x/1.x mcp package line). Found by Egeria Advisor rebuilding its demo deployment against pyegeria 6.1.10 -- worked fine because they pinned mcp==2.1.1 explicitly, not because pyegeria's own floor would have caught an older mcp. Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) 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 — From 09b80c4d0eceb67d845d21b37d186a8247623937 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Sun, 6 Sep 2026 20:53:10 -0500 Subject: [PATCH 2/3] test(view): cover ISSUE-86's caller-held bearer token in exec_report_spec MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit format_set_executor.py's `token` parameter (ISSUE-86 — let a caller that already holds an Egeria bearer token run a report without a password) is on main with no dedicated test. These tests existed only on an unpushed local branch, claude/cranky-chebyshev-be2494, alongside a second, parallel implementation of the same feature that main had meanwhile gained by another route. Pushing that branch would have added a duplicate implementation; the tests were the part worth keeping, so they are salvaged here on their own. They were written against that branch's `_authenticate_client()` helper, which main does not have — verified they pass unchanged against main's inline implementation (7 passed), so they test the behaviour rather than that refactor's shape. Co-Authored-By: Claude Opus 5 Signed-off-by: Dan Wolfson --- .../test_format_set_executor_token.py | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 tests/micro-tests/test_format_set_executor_token.py 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 From 3ca07ba725c780ca9987d9692afcd7a68425e6d9 Mon Sep 17 00:00:00 2001 From: Dan Wolfson Date: Mon, 7 Sep 2026 20:12:29 -0500 Subject: [PATCH 3/3] ISSUE-92: Project Type's description lists 4 of its 6 valid_values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit commands_project_compact.json's `Project Type` attribute declares six valid_values (Project, Campaign, Task, PersonalProject, StudyProject, Experiment) and describes four — `Project`, which is also the attribute's own default_value, and `Experiment` are both missing from the prose. The machine-readable list is correct: it matches Egeria 6's Project classifications in OpenMetadataType.java (model 0130). Only the description is behind. It has propagated. gen_md_cmd_templates and gen_dr_help render the description, so 25 files in the main tree carry the sentence verbatim — the source JSON plus 24 generated artifacts, 16 of which ship as user-facing templates. Notably sample-data/templates/{basic,advanced}/Projects/Create_Experiment.md tells the reader Experiment is not a supported value, and the generated help tables contradict themselves in a single row by printing the prose beside the valid_values column. Logged, not fixed, per the standing rule for this repo. The entry carries a suggested replacement description worded from the Egeria type definitions themselves, the regenerate-and-propagate steps, and a note to check whether any other compact-command attribute enumerates its valid_values in prose — the same latent defect wherever it exists. Found while mapping Resource Explorer's investigation classifications onto Egeria's. Co-Authored-By: Claude Opus 5 Signed-off-by: Dan Wolfson --- PYEGERIA_ISSUES.md | 87 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/PYEGERIA_ISSUES.md b/PYEGERIA_ISSUES.md index a79e2a85..d2e3ae70 100644 --- a/PYEGERIA_ISSUES.md +++ b/PYEGERIA_ISSUES.md @@ -199,6 +199,93 @@ 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-92: `Project Type`'s description in `commands_project_compact.json` lists only 4 of its 6 `valid_values` — omits `Project` and `Experiment`, and the stale text is baked into 24 generated files + +**Layer:** Pyegeria · **Status:** open · **Found:** 2026-09-07 (Resource +Explorer, designing investigation → Egeria Project classification mapping). + +`md_processing/data/compact_commands/commands_project_compact.json`, the +`Project Type` attribute (`variable_name: project_type`): + +```json +"valid_values": [ + "Project", "Campaign", "Task", "PersonalProject", "StudyProject", "Experiment" +], +"description": "A string classifying the project. Supported values are Campaign, Task, PersonalProject and StudyProject." +``` + +`valid_values` has six entries; the description names four. `Project` (which +is also this attribute's own `default_value`) and `Experiment` are both +missing from the prose. The machine-readable list is correct — it matches +Egeria 6's actual Project classifications in +`OpenMetadataType.java` (0130): `CAMPAIGN_CLASSIFICATION`, +`TASK_CLASSIFICATION`, `PERSONAL_PROJECT_CLASSIFICATION`, +`STUDY_PROJECT_CLASSIFICATION`, `EXPERIMENT_CLASSIFICATION`. Only the +description is behind. + +**Why it matters more than one stale string:** the description is what +`gen_md_cmd_templates` and `gen_dr_help` render, so it has propagated. In +this checkout, 25 files in the main tree carry the sentence verbatim — the +one source JSON plus 24 generated artifacts: + +``` +md_processing/data/compact_commands/commands_project_compact.json <- source +md_processing/data/compact_commands_backup/commands_project_compact.json +sample-data/templates/{basic,advanced}/Projects/*.md (16) +sample-data/egeria-inbox/dr-egeria-help-*.md (7) +``` + +The sharpest symptom: **`sample-data/templates/{basic,advanced}/Projects/Create_Experiment.md` +tells the reader that `Experiment` is not a supported value** — on the +template whose entire purpose is to create one. `Create_Project.md` has the +same problem for `Project`. The generated help tables are self-contradictory +in a single row, because they print the prose and the `valid_values` list +side by side: + +``` +... Supported values are Campaign, Task, PersonalProject and StudyProject. | False | Project, Campaign, Task, PersonalProject, StudyProject, Experiment | Domain | +``` + +A reader who trusts the sentence over the column will never reach for +`Experiment`, which is the one classification carrying its own defining +attribute (`hypothesis`), so the omission suppresses a real capability +rather than just reading untidily. + +**Where seen:** found while mapping Resource Explorer's investigation +`project_classification` onto Egeria's Project classifications — the +description was the first thing read, and it made `Experiment` look +unsupported until `valid_values` and the Egeria type source were checked. + +**Candidate fix (one string, then regenerate):** edit the `description` in +`commands_project_compact.json` to cover all six, ideally naming what each +means rather than just listing them again — the `valid_values` array +already lists them, so prose that only repeats the list adds nothing and +will drift again the next time a value is added. Suggested: + +> "A string classifying the project. `Project` (the default) applies no +> classification. `Campaign` is a long-term strategic initiative delivered +> through multiple projects; `Task` a self-contained short activity; +> `PersonalProject` an informal project an individual creates to organize +> their own work; `StudyProject` a focused analysis of a topic, person, +> object or situation; `Experiment` a project testing a hypothesis, which +> is recorded in the `hypothesis` attribute." + +(Wording taken from the Egeria type definitions themselves so the two +cannot disagree.) + +Then re-run `refresh_specs`, `gen_md_cmd_templates` and `gen_dr_help` per +the Dr.Egeria command-sync flow and propagate the regenerated templates/help +to `egeria-workspaces` and `egeria-advisor` — the 24 generated files above +will not update on their own, and 16 of them ship as user-facing templates. + +**Worth checking while in there:** whether any other compact-command +attribute has a `description` that enumerates its `valid_values` in prose. +Any such pair is the same latent defect — two lists that must be edited +together with nothing enforcing it. A cheap guard would be a spec-validation +check that flags a `description` naming a subset of its own `valid_values`. + +--- + ### 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)