Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 81 additions & 3 deletions src/my_mcp_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,83 @@
"""My MCP Server."""
"""My MCP Server.

from importlib.metadata import version
Single source of truth for this package's distribution identity (name +
version). ``server_info`` (the MCP resource) and any other consumer import
from here rather than re-deriving the dist name or re-reading metadata, so
the two can never drift apart.

__version__ = version("my-mcp-server")
Resolution order, deliberately metadata-first:

1. ``importlib.metadata`` — authoritative for an *installed* distribution
(wheel or ``pip install -e .``). This is what the running server actually
is, regardless of what files happen to sit on disk above it.
2. pyproject ``[project].version`` — dev fallback for a source checkout that
was never installed. Accepted *only* when ``[project].name`` matches this
package, so a foreign / monorepo-parent ``pyproject.toml`` encountered on
the walk-up can never masquerade as this server's identity.
3. ``FALLBACK_VERSION`` — last resort (renamed clone, neither source usable).

The dist name is derived from ``__package__`` (not a hardcoded literal) so a
clone that renames ``src/my_mcp_server/`` picks the new name up automatically
and does not crash on import.
"""

from __future__ import annotations

import tomllib
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

# Convention: PyPI distribution name = import package name with underscores
# rewritten as hyphens. Derived from ``__package__`` so a renamed clone does
# not have to update a hardcoded literal here AND in server_info.
DIST_NAME = (__package__ or "my_mcp_server").replace("_", "-")
FALLBACK_VERSION = "0.0.0"


def _read_own_pyproject() -> dict[str, str] | None:
"""Walk up from this file and return the FIRST ``[project]`` table whose
``name`` matches this package's :data:`DIST_NAME`.

A non-matching ``[project]`` table (a foreign or monorepo-parent
``pyproject.toml``) is skipped — the walk continues — so a parent project
can never be served as this server's identity. Returns ``None`` if no
matching pyproject is reachable (e.g. installed from a wheel without the
source tree).
"""
here = Path(__file__).resolve()
for parent in here.parents:
candidate = parent / "pyproject.toml"
if not candidate.is_file():
continue
with candidate.open("rb") as fh:
data = tomllib.load(fh)
project = data.get("project")
if isinstance(project, dict) and project.get("name") == DIST_NAME:
return project
# Foreign / parent pyproject (or one without a matching [project]):
# keep walking — do NOT let it stand in for this package.
return None


def resolve_version() -> str:
"""Return this distribution's version, metadata-first.

See the module docstring for the full resolution order. This is the single
function every consumer (``__version__`` below, ``server_info``) routes
through, so there is exactly one place that decides what "the version" is.
"""
try:
return version(DIST_NAME)
except PackageNotFoundError:
pass

project = _read_own_pyproject()
if project is not None:
return str(project.get("version", FALLBACK_VERSION))

return FALLBACK_VERSION


__version__ = resolve_version()

__all__ = ["DIST_NAME", "FALLBACK_VERSION", "__version__", "resolve_version"]
69 changes: 27 additions & 42 deletions src/my_mcp_server/resources/server_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,69 +3,54 @@

Resources are how you expose data to the client (in contrast to Tools which
perform actions). Replace with your own resource.

Identity (dist name + version) is NOT re-derived here. It comes from the
package root (:mod:`my_mcp_server`), which is the single source of truth:
``importlib.metadata`` first (authoritative for an installed distribution),
then this package's own ``pyproject.toml`` as a dev fallback, then
``FALLBACK_VERSION``. Routing through that one helper means the version the
server *reports* can never drift from the version the package *is*, and a
foreign / monorepo-parent ``pyproject.toml`` found on a walk-up can never
masquerade as this server's identity. See ``my_mcp_server/__init__.py``.
"""

from __future__ import annotations

import json
import platform
import sys
import tomllib
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path

from mcp.server.fastmcp import FastMCP

from my_mcp_server import DIST_NAME, FALLBACK_VERSION, resolve_version

NAME = "server-info"
URI = "info://server/status"
TITLE = "Server Info"
DESCRIPTION = "Server metadata: name, version, Python runtime, and platform."
MIME_TYPE = "application/json"

# Convention: PyPI distribution name = import package name with underscores
# rewritten as hyphens. Derived from `__package__` so a clone that renames
# `src/my_mcp_server/` (per setup.yml's first-run checklist) doesn't have to
# update a second hardcoded literal — the wheel-install fallback below picks
# the new name up automatically.
PKG_NAME = (__package__ or "my_mcp_server").split(".")[0].replace("_", "-")
FALLBACK_VERSION = "0.0.0"

# Back-compat alias: the dist name lives in the package root now. Kept so any
# external consumer that imported ``server_info.PKG_NAME`` still resolves.
PKG_NAME = DIST_NAME

def _read_pyproject() -> dict[str, str] | None:
"""Walk up from this file to locate pyproject.toml and parse it.

Returns the ``[project]`` table as a dict, or None if not found
(e.g. when installed from a wheel without the source tree).
"""
here = Path(__file__).resolve()
for parent in here.parents:
candidate = parent / "pyproject.toml"
if candidate.is_file():
with candidate.open("rb") as fh:
data = tomllib.load(fh)
project = data.get("project")
if isinstance(project, dict):
return project
return None
return None
__all__ = [
"DESCRIPTION",
"FALLBACK_VERSION",
"MIME_TYPE",
"NAME",
"PKG_NAME",
"TITLE",
"URI",
"register",
"server_info",
]


def _server_metadata() -> dict[str, object]:
project = _read_pyproject()
if project is not None:
pkg_name = str(project.get("name", PKG_NAME))
pkg_version = str(project.get("version", FALLBACK_VERSION))
else:
# Fallback for wheel installs where pyproject.toml isn't shipped.
pkg_name = PKG_NAME
try:
pkg_version = version(pkg_name)
except PackageNotFoundError:
pkg_version = FALLBACK_VERSION

return {
"name": pkg_name,
"version": pkg_version,
"name": DIST_NAME,
"version": resolve_version(),
"runtime": {
"python": sys.version.split()[0],
"platform": platform.system().lower(),
Expand Down
157 changes: 42 additions & 115 deletions tests/test_server_info.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
"""Tests for the server-info resource."""
"""Tests for the server-info resource.

Identity resolution (metadata-first, foreign-pyproject guard, fallback) lives
in the package root now and is tested in ``test_version_resolution.py``. These
tests cover the resource contract and that the resource *consumes* that single
source of truth rather than re-deriving identity on its own.
"""

import json
import sys
import tomllib
from pathlib import Path

import my_mcp_server
from my_mcp_server.resources.server_info import (
DESCRIPTION,
MIME_TYPE,
NAME,
PKG_NAME,
URI,
server_info,
)
Expand Down Expand Up @@ -41,15 +49,40 @@ async def test_returns_json_with_expected_shape() -> None:
assert isinstance(payload["version"], str) and payload["version"]


async def test_version_matches_pyproject() -> None:
"""Version field reflects pyproject.toml, not a hardcoded constant."""
async def test_name_and_version_come_from_package_single_source() -> None:
"""The resource reports the package's own identity, not a re-derivation.

``name`` is the package's ``DIST_NAME`` and ``version`` is whatever
``resolve_version()`` returns — so the value the server reports can never
drift from the value the package is.
"""
payload = json.loads(await server_info())
assert payload["name"] == my_mcp_server.DIST_NAME
assert payload["version"] == my_mcp_server.resolve_version()


async def test_version_matches_pyproject_for_editable_install() -> None:
"""For this source checkout (installed editable in CI), the metadata-first
version coincides with pyproject's — a regression guard that the reported
version is the real one, not a hardcoded constant."""
project = _pyproject_project()
payload = json.loads(await server_info())

assert payload["name"] == project["name"]
assert payload["version"] == project["version"]


async def test_resource_tracks_resolve_version(monkeypatch) -> None:
"""If ``resolve_version()`` changes, the resource changes with it — proves
the resource routes through the single source instead of reading metadata
or pyproject on its own."""
from my_mcp_server.resources import server_info as mod

monkeypatch.setattr(mod, "resolve_version", lambda: "9.9.9-sentinel")
payload = json.loads(await mod.server_info())
assert payload["version"] == "9.9.9-sentinel"


async def test_runtime_reflects_current_interpreter() -> None:
"""Python version in runtime matches sys.version."""
payload = json.loads(await server_info())
Expand All @@ -65,115 +98,9 @@ async def test_registered_on_server() -> None:
assert URI in uris


# --- wheel-install fallback path ---------------------------------------------
# When the package is installed from a wheel, pyproject.toml is NOT shipped,
# so `_read_pyproject()` returns None and `_server_metadata()` must fall back
# to `importlib.metadata.version()`. The tests above all exercise the source
# tree (where pyproject.toml IS reachable).


async def test_falls_back_to_importlib_metadata_when_pyproject_missing(
monkeypatch,
) -> None:
"""Wheel install: _read_pyproject() returns None, both name and version
come from importlib.metadata (both sides of the equality below resolve
against the SAME source, not pyproject-on-disk — so a maintainer who
bumps pyproject.toml's version without re-running ``pip install -e .``
won't see a misleading failure here)."""
from importlib.metadata import version as imp_version

from my_mcp_server.resources import server_info as mod

monkeypatch.setattr(mod, "_read_pyproject", lambda: None)
payload = json.loads(await mod.server_info())

# `mod.PKG_NAME` is derived from `__package__` — survives a clone rename
# without re-hardcoding the literal in the test.
assert payload["name"] == mod.PKG_NAME
assert payload["version"] == imp_version(mod.PKG_NAME)


async def test_falls_back_to_zero_version_when_package_not_installed(
monkeypatch,
) -> None:
"""Edge of the edge: pyproject missing AND importlib.metadata can't find
the dist. Should return FALLBACK_VERSION instead of raising
PackageNotFoundError."""
from my_mcp_server.resources import server_info as mod

def raise_not_found(*_args: object, **_kw: object) -> str:
# Tolerant signature in case `version()` ever gets called with kwargs.
raise mod.PackageNotFoundError("simulated wheel-install-without-metadata")

monkeypatch.setattr(mod, "_read_pyproject", lambda: None)
monkeypatch.setattr(mod, "version", raise_not_found)

payload = json.loads(await mod.server_info())
assert payload["version"] == mod.FALLBACK_VERSION


def test_read_pyproject_returns_none_when_project_table_missing(tmp_path, monkeypatch) -> None:
"""Walk-up finds a pyproject.toml with no [project] table — must exit at
the inner `return None` (NOT continue walking — that's the contract).

Strengthened with a tomllib.load spy so a future refactor that drops the
explicit inner return and falls through to keep walking gets caught here
(the spy would see >1 load) instead of silently passing because no other
pyproject.toml is reachable on the way to /.
"""
from my_mcp_server.resources import server_info as mod

pkg = tmp_path / "pkg"
pkg.mkdir()
fake_pyproject = tmp_path / "pyproject.toml"
fake_pyproject.write_text("[build-system]\nrequires = []\n")
fake_file = pkg / "server_info.py"
fake_file.write_text("")

loaded_paths: list[str] = []
original_load = mod.tomllib.load

def spy_load(fh): # type: ignore[no-untyped-def]
loaded_paths.append(getattr(fh, "name", ""))
return original_load(fh)

monkeypatch.setattr(mod.tomllib, "load", spy_load)
monkeypatch.setattr(mod, "__file__", str(fake_file))

assert mod._read_pyproject() is None
assert loaded_paths == [str(fake_pyproject)], (
f"_read_pyproject must stop at the first pyproject.toml encountered; "
f"saw {len(loaded_paths)} load(s): {loaded_paths}"
)


def test_read_pyproject_returns_none_when_no_pyproject_anywhere(tmp_path, monkeypatch) -> None:
"""Walk-up exhausts the parent chain without finding any pyproject.toml —
exercises the outer `return None` after the loop (server_info.py:42).

Without this test the loop-exhaustion path is 0% covered: the
"fallback" tests above bypass the walk entirely by monkeypatching
`_read_pyproject` itself.
"""
from my_mcp_server.resources import server_info as mod

pkg = tmp_path / "pkg"
pkg.mkdir()
fake_file = pkg / "server_info.py"
fake_file.write_text("")
# Deliberately no pyproject.toml anywhere in tmp_path or its ancestors
# (system tmp dirs don't contain one), so the walk reaches root.

monkeypatch.setattr(mod, "__file__", str(fake_file))
assert mod._read_pyproject() is None


def test_pkg_name_derived_from_package() -> None:
"""PKG_NAME is the dist-name form (hyphens) of the import package name —
pins the rename-safety contract documented in server_info.py."""
from my_mcp_server.resources import server_info as mod

assert mod.PKG_NAME == "my-mcp-server"
# Sanity: derivation tracks __package__, not a stray literal somewhere.
root_import = (mod.__package__ or "").split(".")[0]
assert root_import.replace("_", "-") == mod.PKG_NAME
def test_pkg_name_is_back_compat_alias_of_dist_name() -> None:
"""``PKG_NAME`` is retained as a back-compat alias of the package's
``DIST_NAME`` (the dist-name form, hyphens) so external importers of
``server_info.PKG_NAME`` keep working after identity moved to the root."""
assert PKG_NAME == "my-mcp-server"
assert PKG_NAME == my_mcp_server.DIST_NAME
Loading
Loading