diff --git a/src/skillspector/mcp_server.py b/src/skillspector/mcp_server.py index 444b75f..f1119bd 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 10c5596..b61751e 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):