From 7a055a4ac227964e2c9892db0e3a3d2c7e7e1a84 Mon Sep 17 00:00:00 2001 From: abhinav <244986440+erensh27@users.noreply.github.com> Date: Sun, 9 Aug 2026 13:11:53 +0530 Subject: [PATCH] fix(mcp): report incompatible fastmcp installs accurately mcp 2.0.0 (2026-07-28) removed mcp.server.fastmcp, so an install that resolved mcp 2.x fails at server init with the misleading 'requires the optional mcp dependency' error even though mcp is present. - Distinguish a missing 'mcp' package (missing optional extra) from an installed mcp package that lacks 'mcp.server.fastmcp' (incompatible mcp>=2.0.0), and re-raise the original error for unrelated missing modules such as a missing transitive dependency. - Add tests covering all three failure modes. --- src/skillspector/mcp_server.py | 22 ++++++++++--- tests/unit/test_mcp_server.py | 59 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/skillspector/mcp_server.py b/src/skillspector/mcp_server.py index e8aadedc..26bc4927 100644 --- a/src/skillspector/mcp_server.py +++ b/src/skillspector/mcp_server.py @@ -144,10 +144,24 @@ def build_server(name: str = "skillspector") -> FastMCP: try: from mcp.server.fastmcp import FastMCP except ModuleNotFoundError as exc: - raise ModuleNotFoundError( - "The MCP server requires the optional 'mcp' dependency. " - "Install it with: pip install 'skillspector[mcp]'" - ) from exc + if exc.name == "mcp": + raise ModuleNotFoundError( + "The MCP server requires the optional 'mcp' dependency. " + "Install it with: pip install 'skillspector[mcp]'" + ) from exc + if exc.name == "mcp.server.fastmcp": + # mcp>=2.0.0 removed mcp.server.fastmcp, so an install that + # resolved mcp 2.x fails here even though mcp is present. + # Point at the real cause instead of the misleading + # "extra not installed" error. + raise ModuleNotFoundError( + "The installed 'mcp' package is incompatible with the " + "SkillSpector MCP server: 'mcp.server.fastmcp' is " + "unavailable. Reinstall with: pip install 'skillspector[mcp]'" + ) from exc + # Some other module was missing (e.g. a transitive dependency of + # the installed mcp package); keep the original error. + raise exc server = FastMCP(name) diff --git a/tests/unit/test_mcp_server.py b/tests/unit/test_mcp_server.py index 12149095..16e3197a 100644 --- a/tests/unit/test_mcp_server.py +++ b/tests/unit/test_mcp_server.py @@ -231,6 +231,65 @@ async def test_build_server_registers_scan_skill() -> None: assert "scan_skill" in {tool.name for tool in tools} +def test_build_server_reports_missing_mcp_extra(monkeypatch: pytest.MonkeyPatch) -> None: + """An absent mcp package must still report the missing optional extra.""" + import builtins + + original_import = builtins.__import__ + + def import_without_mcp(name: str, *args: object, **kwargs: object) -> object: + if name == "mcp.server.fastmcp": + raise ModuleNotFoundError("No module named 'mcp'", name="mcp") + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", import_without_mcp) + + with pytest.raises(ModuleNotFoundError, match="requires the optional 'mcp' dependency"): + mcp_server.build_server() + + +def test_build_server_reports_incompatible_mcp(monkeypatch: pytest.MonkeyPatch) -> None: + """An installed package without FastMCP must not be reported as missing.""" + import builtins + + original_import = builtins.__import__ + + def import_without_fastmcp(name: str, *args: object, **kwargs: object) -> object: + if name == "mcp.server.fastmcp": + raise ModuleNotFoundError( + "No module named 'mcp.server.fastmcp'", name="mcp.server.fastmcp" + ) + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", import_without_fastmcp) + + with pytest.raises(ModuleNotFoundError, match="installed 'mcp' package is incompatible"): + mcp_server.build_server() + + +def test_build_server_preserves_transitive_import_failure( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A missing transitive dependency must keep the original error.""" + import builtins + + original_import = builtins.__import__ + + def import_with_missing_transitive(name: str, *args: object, **kwargs: object) -> object: + if name == "mcp.server.fastmcp": + raise ModuleNotFoundError( + "No module named 'mcp.server.fastmcp.uvicorn'", + name="mcp.server.fastmcp.uvicorn", + ) + return original_import(name, *args, **kwargs) + + monkeypatch.setattr(builtins, "__import__", import_with_missing_transitive) + + with pytest.raises(ModuleNotFoundError, match="mcp.server.fastmcp.uvicorn") as excinfo: + mcp_server.build_server() + assert excinfo.value.name == "mcp.server.fastmcp.uvicorn" + + async def test_mcp_stdio_initialize_registers_scan_skill() -> None: """The real stdio CLI must initialize and expose the scan_skill tool.""" pytest.importorskip("mcp")