diff --git a/semantica/mcp_server/__init__.py b/semantica/mcp_server/__init__.py index cb22b434..19fe0bf4 100644 --- a/semantica/mcp_server/__init__.py +++ b/semantica/mcp_server/__init__.py @@ -45,14 +45,16 @@ import json import logging import os import sys -from importlib.metadata import PackageNotFoundError, version from typing import Any -try: - _SEMANTICA_VERSION = version("semantica") -except PackageNotFoundError: - # Preserve direct source-tree execution when distribution metadata is absent. - from semantica import __version__ as _SEMANTICA_VERSION +# `semantica.__version__` is the authoritative package version — it is kept in +# sync with pyproject.toml's static `version` field by the release process and +# is always present whenever this submodule is importable. Using it directly +# is simpler and more reliable than `importlib.metadata.version("semantica")`, +# which reads dist-info written at install time and can lag the source in +# editable installs (egg-info / dist-info is not regenerated on every version +# bump, so it can reflect a stale value). +from semantica import __version__ as _SEMANTICA_VERSION # ── logging ──────────────────────────────────────────────────────────────── _log_level = getattr(logging, os.environ.get("SEMANTICA_LOG_LEVEL", "WARNING").upper(), logging.WARNING) diff --git a/tests/test_mcp_server_version.py b/tests/test_mcp_server_version.py index 9dd9f9a4..fbe03d1d 100644 --- a/tests/test_mcp_server_version.py +++ b/tests/test_mcp_server_version.py @@ -1,35 +1,93 @@ -"""Regression tests for MCP server version reporting.""" +"""Regression tests for MCP server version reporting (issue #863). + +Both public MCP version surfaces must derive from the same authoritative +package version rather than a hardcoded stale literal: + 1. MCP ``initialize`` → ``serverInfo.version`` + 2. ``semantica://schema/info`` → ``version`` + +The authoritative source of truth is ``semantica.__version__``, which is +maintained in sync with ``pyproject.toml``'s static ``version`` field by +the release process. We assert equality against that value rather than +duplicating the version-resolution logic here, so the tests remain valid +through future version bumps without modification. + +The ``assertNotEqual(..., "0.4.0")`` canaries guard against regression to +the original stale literal that triggered issue #863. +""" import unittest -from importlib.metadata import PackageNotFoundError, version import semantica from semantica import mcp_server +_EXPECTED = semantica.__version__ + class TestMCPServerVersion(unittest.TestCase): - def test_server_info_uses_distribution_version(self): - try: - expected = version("semantica") - except PackageNotFoundError: - expected = semantica.__version__ - self.assertEqual(mcp_server.SERVER_INFO["version"], expected) + # ------------------------------------------------------------------ # + # SERVER_INFO (used directly in the initialize response) + # ------------------------------------------------------------------ # - def test_initialize_reports_package_version(self): + def test_server_info_version_matches_package(self): + """SERVER_INFO['version'] must equal the authoritative package version.""" + self.assertEqual(mcp_server.SERVER_INFO["version"], _EXPECTED) + + def test_server_info_version_is_not_stale_literal(self): + """Guard: SERVER_INFO must not report the original hardcoded 0.4.0.""" + self.assertNotEqual(mcp_server.SERVER_INFO["version"], "0.4.0") + + # ------------------------------------------------------------------ # + # MCP initialize → serverInfo.version + # ------------------------------------------------------------------ # + + def test_initialize_server_info_version_matches_package(self): + """The MCP initialize response must report the authoritative package version.""" response = mcp_server._handle( {"jsonrpc": "2.0", "id": 1, "method": "initialize"} ) - self.assertIsNotNone(response) self.assertEqual( - response["result"]["serverInfo"]["version"], semantica.__version__ + response["result"]["serverInfo"]["version"], + _EXPECTED, ) - def test_schema_info_resource_reports_package_version(self): - resource = mcp_server._read_resource("semantica://schema/info") + def test_initialize_server_info_version_is_not_stale_literal(self): + """Guard: initialize must not report the original hardcoded 0.4.0.""" + response = mcp_server._handle( + {"jsonrpc": "2.0", "id": 1, "method": "initialize"} + ) + self.assertNotEqual(response["result"]["serverInfo"]["version"], "0.4.0") - self.assertEqual(resource["version"], semantica.__version__) + # ------------------------------------------------------------------ # + # semantica://schema/info → version + # ------------------------------------------------------------------ # + + def test_schema_info_resource_version_matches_package(self): + """The semantica://schema/info resource must report the authoritative package version.""" + resource = mcp_server._read_resource("semantica://schema/info") + self.assertEqual(resource["version"], _EXPECTED) + + def test_schema_info_resource_version_is_not_stale_literal(self): + """Guard: schema/info must not report the original hardcoded 0.4.0.""" + resource = mcp_server._read_resource("semantica://schema/info") + self.assertNotEqual(resource["version"], "0.4.0") + + # ------------------------------------------------------------------ # + # Both surfaces must agree + # ------------------------------------------------------------------ # + + def test_both_version_surfaces_are_identical(self): + """SERVER_INFO and schema/info must report the exact same version string, + confirming both surfaces derive from a single authoritative value.""" + init_response = mcp_server._handle( + {"jsonrpc": "2.0", "id": 1, "method": "initialize"} + ) + schema_resource = mcp_server._read_resource("semantica://schema/info") + self.assertEqual( + init_response["result"]["serverInfo"]["version"], + schema_resource["version"], + ) if __name__ == "__main__":