Skip to content

pkg_version() in Server.create_initialization_options doesn't guard against importlib.metadata.version() returning None #3487

Description

@j-brumback

Summary

Server.create_initialization_options in mcp/server/lowlevel/server.py:183 falls through to pkg_version("mcp") when self.version is falsy. The fallback function catches exceptions and returns "unknown", but doesn't guard against the (rare but real) case where importlib.metadata.version() returns None instead of raising or returning a string.

When that happens, None flows through to InitializationOptions.server_version — which is typed as str on the pydantic model — and pydantic raises ValidationError. The server dies before the stdio handshake completes; the MCP client sees a bare "Connection closed" with no useful diagnostic.

Reproduction

Environment where I hit this: Python 3.12 embedded distribution installed via WiX MSI, with mcp>=1.26,<2 pip-installed into the embedded Python's site-packages via the standard python.exe -m pip install command.

Probe from the affected Python:

from importlib.metadata import version
print(repr(version("mcp")))   # prints: None

The mcp-1.30.0.dist-info/METADATA file on disk correctly declares Version: 1.30.0. Root cause of the None return appears to be a downstream bug in the embedded-Python distribution or its pip metadata setup — a separate concern I'm investigating on my side.

Impact

Any FastMCP server that (a) doesn't set an explicit version (FastMCP doesn't accept a version= kwarg anyway) and (b) runs on a Python where importlib.metadata.version("mcp") returns None instead of a string, crashes on stdio startup:

Exception Group Traceback (most recent call last):
  File "…/mcp/server/fastmcp/server.py", line 775, in run_stdio_async
    self._mcp_server.create_initialization_options(),
  File "…/mcp/server/lowlevel/server.py", line 181, in create_initialization_options
    return InitializationOptions(
  File "…/pydantic/main.py", line 263, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
pydantic_core._pydantic_core.ValidationError: 1 validation error for InitializationOptions
server_version
  Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]

Full traceback from an affected install:

2026-09-09 14:44:00,841 [INFO] __main__: Starting GRAccess MCP server (stdio transport)
  + Exception Group Traceback (most recent call last):
  |   File "<frozen runpy>", line 198, in _run_module_as_main
  |   File "…/mcp/server/fastmcp/server.py", line 312, in run
  |     anyio.run(self.run_stdio_async)
  |   File "…/mcp/server/fastmcp/server.py", line 771, in run_stdio_async
  |     async with stdio_server() as (read_stream, write_stream):
  |   File "…/mcp/server/lowlevel/server.py", line 181, in create_initialization_options
  |     return InitializationOptions(
  |   File "…/pydantic/main.py", line 263, in __init__
  |     validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
  | pydantic_core._pydantic_core.ValidationError: 1 validation error for InitializationOptions
  | server_version
  |   Input should be a valid string [type=string_type, input_value=None, input_type=NoneType]

Suggested fix

Guard the return of pkg_version against None — matches the existing "returns 'unknown' on failure" contract implied by the type annotation and the # pragma: no cover fallback:

def pkg_version(package: str) -> str:
    try:
        from importlib.metadata import version
        v = version(package)
        if v is not None:
            return v
    except Exception:  # pragma: no cover
        pass
    return "unknown"  # pragma: no cover

Doesn't change happy-path behavior for well-formed metadata; catches the pathological None case that pydantic then rejects.

Workaround

Setting mcp._mcp_server.version explicitly on the underlying server after FastMCP(...) construction bypasses the fallback entirely. That's what I ended up shipping in my own server.

Version

  • mcp: 1.30.0
  • Python: 3.12 embedded distribution on Windows Server 2022

Activity

  1. added
    v1Affects the v1.x maintenance line
    on Sep 10, 2026
  2. RegatteVarshithReddy commented on Sep 11, 2026

    @RegatteVarshithReddy

    Confirmed against v1.25.0 (src/mcp/server/lowlevel/server.py, create_initialization_options → the local pkg_version() helper, ~line 171):

    def pkg_version(package: str) -> str:
        try:
            from importlib.metadata import version
            return version(package)
        except Exception:  # pragma: no cover
            pass
        return "unknown"  # pragma: no cover

    importlib.metadata.version() returning None (rather than raising or returning a string) skips the except entirely, so None reaches InitializationOptions(server_version=...), which is typed str — pydantic raises ValidationError before the handshake completes, and the client just sees "Connection closed" with nothing actionable.

    One thing worth confirming first: this helper doesn't exist in the same form on main (v2) — the version handling looks like it was restructured there. Is this issue specifically for the 1.x maintenance line, or is there an equivalent path on v2 I should check too?

    Proposed fix, scoped to 1.x: guard the return value, not just the exception —

    def pkg_version(package: str) -> str:
        try:
            from importlib.metadata import version
            v = version(package)
            return v if v else "unknown"
        except Exception:  # pragma: no cover
            pass
        return "unknown"  # pragma: no cover

    Happy to add a regression test (mocking importlib.metadata.version to return None) and open a PR against the 1.x branch if that approach looks right and someone can assign this to me.

  3. arpankernel commented on Sep 15, 2026

    @arpankernel

    I hit this too and traced the root cause. In Server.create_initialization_options, server_version falls back to the local pkg_version("mcp") helper, which handles importlib.metadata.version() raising but not the case where it returns None. Since InitializationOptions.server_version is typed str, that None reaches the pydantic model and raises ValidationError, so the server dies before the stdio handshake and the client only sees a bare "Connection closed" with no useful diagnostic.

    It is v1.x-only. main (v2) sets server_version=self.version directly and does not go through pkg_version.

    Minimal fix that stays consistent with the helper's existing "return unknown on failure" contract:

    return version(package) or "unknown"

    I have this plus a regression test ready on a v1.x-based branch (fails before, passes after; full suite and 100% coverage green locally). Happy to open a PR if a maintainer wants an outside fix for this, though of course @j-brumback has first call as the reporter.

  4. j-brumback commented on Sep 15, 2026

    @j-brumback
    Author
  5. maxisbey commented on Oct 6, 2026

    @maxisbey
    Contributor

    Thanks for the detailed report, and for tracking it down to pkg_version(). I'm going to close this as not planned.

    This only affects 1.x. On 2.x the helper is gone: the server reports whatever version= you give it (an empty string if you give none) and never looks up its own package metadata at startup. The 1.x line only takes critical and security fixes now, and this one is below that bar, since it needs an environment where importlib.metadata.version("mcp") returns None and setting the version explicitly gets around it.

    For anyone else who lands here on 1.x, the workaround from the report is to set mcp._mcp_server.version after constructing FastMCP(...).

    @arpankernel thanks for putting #3507 together. It will stay closed for the same reason.

    If you still see something like this after moving to 2.x, a new issue is very welcome.

    AI Disclaimer


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    v1Affects the v1.x maintenance line

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions