From 3c3320082e7d9ef9f431e56f2a3cc62217cb3a28 Mon Sep 17 00:00:00 2001 From: Werner Kasselman <145896621+wernerkasselman-au@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:37:19 +1000 Subject: [PATCH 1/2] fix(mcp): report llm_available via the chat-model credential resolver run_scan gated the LLM pass on resolve_provider_credentials(), which resolves only the active provider's credentials. create_chat_model() falls back to a standard OpenAI client (OPENAI_API_KEY / OPENAI_BASE_URL) when the active provider is unconfigured, so an OpenAI-only setup reported llm_available=false and the semantic pass was skipped even though the CLI would have run it. Gate on resolve_chat_model_credentials(), which includes that fallback, so the MCP server's accounting matches the model path the graph actually takes. Update the existing accounting tests to patch the new resolver and add a regression test for the OpenAI-fallback case. Refs #200 Signed-off-by: Werner Kasselman <145896621+wernerkasselman-au@users.noreply.github.com> --- src/skillspector/mcp_server.py | 10 ++++++++-- tests/unit/test_mcp_server.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/skillspector/mcp_server.py b/src/skillspector/mcp_server.py index 444b75fc5..f1119bdb5 100644 --- a/src/skillspector/mcp_server.py +++ b/src/skillspector/mcp_server.py @@ -33,7 +33,7 @@ from skillspector import __version__ from skillspector.graph import graph from skillspector.logging_config import get_logger -from skillspector.providers import resolve_provider_credentials +from skillspector.providers import resolve_chat_model_credentials if TYPE_CHECKING: from mcp.server.fastmcp import FastMCP @@ -74,7 +74,13 @@ async def run_scan( if output_format not in VALID_FORMATS: raise ValueError(f"output_format must be one of {VALID_FORMATS}, got {output_format!r}") - llm_available = resolve_provider_credentials() is not None + # Use the chat-model credential resolver, not the active-provider-only one, + # so availability matches what ``create_chat_model`` actually does: it falls + # back to a standard OpenAI client (OPENAI_API_KEY / OPENAI_BASE_URL) when the + # active provider is unconfigured. Gating on the active provider alone made the + # server report llm_available=false and skip the LLM pass for an OpenAI-only + # setup the CLI would have run (see issue #200). + llm_available = resolve_chat_model_credentials() is not None llm_used = use_llm and llm_available state: dict[str, Any] = { diff --git a/tests/unit/test_mcp_server.py b/tests/unit/test_mcp_server.py index 10c5596b2..b61751ee2 100644 --- a/tests/unit/test_mcp_server.py +++ b/tests/unit/test_mcp_server.py @@ -33,7 +33,7 @@ async def test_run_scan_returns_structured_verdict( ) -> None: """run_scan returns a JSON-serialisable verdict with the expected shape.""" # No credentials: the LLM pass cannot run regardless of what is requested. - monkeypatch.setattr(mcp_server, "resolve_provider_credentials", lambda: None) + monkeypatch.setattr(mcp_server, "resolve_chat_model_credentials", lambda: None) _write_skill(tmp_path) result = await run_scan(str(tmp_path), use_llm=True, output_format="json") @@ -51,7 +51,7 @@ async def test_run_scan_llm_accounting_is_honest_without_credentials( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Requesting the LLM with no credentials must report it as not used.""" - monkeypatch.setattr(mcp_server, "resolve_provider_credentials", lambda: None) + monkeypatch.setattr(mcp_server, "resolve_chat_model_credentials", lambda: None) _write_skill(tmp_path) result = await run_scan(str(tmp_path), use_llm=True, output_format="json") @@ -66,7 +66,7 @@ async def test_run_scan_reports_llm_available_with_credentials( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: """Credentials present but use_llm=False: available, but honestly not used.""" - monkeypatch.setattr(mcp_server, "resolve_provider_credentials", lambda: ("key", None)) + monkeypatch.setattr(mcp_server, "resolve_chat_model_credentials", lambda: ("key", None)) _write_skill(tmp_path) result = await run_scan(str(tmp_path), use_llm=False, output_format="json") @@ -77,6 +77,29 @@ async def test_run_scan_reports_llm_available_with_credentials( assert result["scan_mode"] == "static-only" +async def test_run_scan_reports_llm_available_via_openai_fallback( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """Regression for #200: availability follows the chat-model resolver. + + When the active provider is unconfigured but the standard OpenAI fallback + (OPENAI_API_KEY / OPENAI_BASE_URL) is set, ``create_chat_model`` would run the + LLM, so ``resolve_chat_model_credentials`` resolves credentials even though + ``resolve_provider_credentials`` (active provider only) returns ``None``. The + server must report the LLM as available and must not gate it out. Patching the + active-provider-only resolver to ``None`` proves the gate no longer consults it. + """ + monkeypatch.setattr(mcp_server, "resolve_chat_model_credentials", lambda: ("openai-key", None)) + if hasattr(mcp_server, "resolve_provider_credentials"): + monkeypatch.setattr(mcp_server, "resolve_provider_credentials", lambda: None) + _write_skill(tmp_path) + + result = await run_scan(str(tmp_path), use_llm=False, output_format="json") + + assert result["llm_available"] is True + assert result["scan_mode"] == "static-only" # use_llm=False, so still not used + + async def test_run_scan_rejects_invalid_format(tmp_path: Path) -> None: """An unsupported output_format is rejected before any scan runs.""" with pytest.raises(ValueError): From 9b54c7e023aaffcce7ce3f8b83b95e797d107d8a Mon Sep 17 00:00:00 2001 From: ubuntu Date: Sun, 19 Jul 2026 12:10:51 +0800 Subject: [PATCH 2/2] fix: pin jsonschema-rs>=0.40.4 for Python 3.14 compatibility Issue #111: build failure on Python 3.14 due to jsonschema-rs shipping with an outdated pyo3 that does not support 3.14. Root cause: jsonschema-rs is pulled in transitively via langgraph-cli[inmem] -> langgraph-api. Releases 0.35.0-0.40.3 ship no cp314 wheels; an unlocked fresh resolve could pick one and force-build from source against an old pyo3, failing on 3.14. The committed lock already selected 0.44.1 (cp314/abi3 wheel), but the floor prevents regressions on fresh resolves. Fix: add minimum-version floor jsonschema-rs>=0.40.4 (lowest version that begins a continuous run of cp314-compatible wheels through the langgraph-api cap of <0.45). Resolved lock keeps 0.44.1 (cp310-abi3, forward-compatible to 3.14, no source build). --- pyproject.toml | 8 ++++++++ uv.lock | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6728f55ca..d57145f25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,14 @@ dependencies = [ "boto3>=1.34.0", "langsmith>=0.7.30", "yara-python>=4.5.0", + # Python 3.14 compat: jsonschema-rs (pulled in transitively via + # langgraph-cli[inmem] -> langgraph-api) only ships prebuilt cp314 wheels + # continuously from 0.40.4 onward. Releases 0.35.0-0.40.3 have no cp314 + # wheels, so an unlocked fresh resolve could pick one and be force-built + # from source against an outdated pyo3 that does not support 3.14, breaking + # the install. langgraph-api caps jsonschema-rs at <0.45, so >=0.40.4 keeps + # a 3.14-compatible wheel while staying within the cap. + "jsonschema-rs>=0.40.4", ] [project.optional-dependencies] diff --git a/uv.lock b/uv.lock index cedf295b1..673c2e5fb 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.12, <3.15" resolution-markers = [ "python_full_version >= '3.14' and sys_platform == 'win32'", @@ -2665,6 +2665,7 @@ source = { editable = "." } dependencies = [ { name = "boto3" }, { name = "httpx" }, + { name = "jsonschema-rs" }, { name = "langchain-anthropic" }, { name = "langchain-aws" }, { name = "langchain-core" }, @@ -2701,6 +2702,7 @@ requires-dist = [ { name = "boto3", specifier = ">=1.34.0" }, { name = "build", marker = "extra == 'dev'", specifier = ">=1.4.0" }, { name = "httpx", specifier = ">=0.28.0" }, + { name = "jsonschema-rs", specifier = ">=0.40.4" }, { name = "langchain-anthropic", specifier = ">=1.4.5" }, { name = "langchain-aws", specifier = ">=0.2.0" }, { name = "langchain-core", specifier = ">=1.2.17" },