diff --git a/src/mcp/server/lowlevel/server.py b/src/mcp/server/lowlevel/server.py index 25a8fde37c..0c25ac9b05 100644 --- a/src/mcp/server/lowlevel/server.py +++ b/src/mcp/server/lowlevel/server.py @@ -172,7 +172,11 @@ def pkg_version(package: str) -> str: try: from importlib.metadata import version - return version(package) + # `version()` can return None (not just raise) on some + # environments, e.g. an embedded Python whose package metadata + # is present but unreadable; `server_version` is typed `str`, + # so fall back to "unknown" rather than let None crash init. + return version(package) or "unknown" except Exception: # pragma: no cover pass diff --git a/tests/server/test_lowlevel_input_validation.py b/tests/server/test_lowlevel_input_validation.py index 0614ad7c46..f317925935 100644 --- a/tests/server/test_lowlevel_input_validation.py +++ b/tests/server/test_lowlevel_input_validation.py @@ -309,3 +309,22 @@ async def test_callback(client_session: ClientSession) -> CallToolResult: assert any( "Tool 'unknown_tool' not listed, no validation will be performed" in record.message for record in caplog.records ) + + +def test_create_initialization_options_falls_back_when_package_version_is_none( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """A None from importlib.metadata.version does not crash create_initialization_options. + + On some environments (e.g. an embedded Python whose package metadata is present but + unreadable) importlib.metadata.version returns None instead of raising. server_version is + typed str, so without a guard that None reaches InitializationOptions and raises a + ValidationError, killing the server before the stdio handshake. It must fall back to + "unknown" instead. + """ + monkeypatch.setattr("importlib.metadata.version", lambda package: None) + + # No explicit version, so create_initialization_options falls back to pkg_version("mcp"). + options = Server("test-server").create_initialization_options() + + assert options.server_version == "unknown"