From 23a738aa073dc7885fb5e4b76abebd9d90f10f8e Mon Sep 17 00:00:00 2001 From: ki7mt Date: Fri, 15 May 2026 21:36:07 +0000 Subject: [PATCH] Add get_version_info tool + CI workflow (v0.3.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fleet rollout per IONIS-AI/ionis-devel#49 — third server after solar (exemplar) and pota. Same diff shape, same envelope. Response: { "service_name": "wspr-mcp", "service_version": "0.3.2", "spec_version": "wspr-live-v1" } Changes: - src/wspr_mcp/__init__.py — add __spec_version__ Final constant, modernize to match adif/solar/pota pattern - src/wspr_mcp/server.py — add get_version_info tool + _version_info_payload helper for direct test access - tests/test_tools.py — TestGetVersionInfo class, 5 unit tests (WSPR-L2-046 .. WSPR-L2-050) - README.md — tool added to tools table - CHANGELOG.md — new, Keep a Changelog format - .github/workflows/ci.yml — new, PR-gating CI (py3.10-3.13 matrix) - pyproject.toml — bump 0.3.1 → 0.3.2 Test count: 45 (40 existing + 5 new) + 6 security = 51 tests, all green. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++++++++++++++++++++ CHANGELOG.md | 27 ++++++++++++++++++++ README.md | 1 + pyproject.toml | 2 +- src/wspr_mcp/__init__.py | 17 ++++++++++--- src/wspr_mcp/server.py | 33 ++++++++++++++++++++++++- tests/test_tools.py | 45 ++++++++++++++++++++++++++++++++++ 7 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 CHANGELOG.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..07aeea7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,53 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +concurrency: + group: ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: pytest (${{ matrix.python-version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package + test dependencies + run: | + python -m pip install --upgrade pip + pip install -e . + pip install pytest pytest-asyncio + + - name: Run unit tests (L2) + run: python -m pytest tests/test_tools.py -v + + - name: Run security tests + run: python -m pytest tests/test_security.py -v + + ci-all-green: + name: ci-all-green + needs: [test] + runs-on: ubuntu-latest + if: always() + steps: + - name: Verify required jobs all succeeded + run: | + if [[ "${{ needs.test.result }}" != "success" ]]; then + echo "FAIL: test matrix did not all succeed (result=${{ needs.test.result }})" + exit 1 + fi + echo "All required CI jobs passed." diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..7b5ba67 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +# Changelog + +All notable changes to `wspr-mcp` are documented here. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.3.2] — 2026-05-15 + +### Added +- New tool `get_version_info` — returns `{service_name, service_version, spec_version}` + for fleet identity attestation. Lets agents detect version drift across MCP + deployments without going outside the protocol. Tracks + [IONIS-AI/ionis-devel#49](https://github.com/IONIS-AI/ionis-devel/issues/49) + (fleet rollout). +- `__spec_version__` constant in package `__init__.py`, pinned to `wspr-live-v1` + for the current wspr.live ClickHouse schema. +- L2 unit tests WSPR-L2-046 through WSPR-L2-050 covering the new tool. +- `.github/workflows/ci.yml` — PR-gating CI workflow (py3.10-3.13 matrix, + unit + security tests, ci-all-green aggregator). + +### Changed +- `__init__.py` modernized to mirror the `adif-mcp`/`solar-mcp` pattern + (`Final` types, explicit `PackageNotFoundError` handling). + +## [0.3.1] — Previous release +- See git history for changes prior to the changelog being introduced. diff --git a/README.md b/README.md index 470ab58..2ca4202 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ pip install wspr-mcp | `wspr_grid_activity` | All WSPR activity in/out of a Maidenhead grid square | grid (2 or 4 char), band, hours | | `wspr_longest_paths` | Longest distance WSPR paths in a time window | band, hours, min_distance, limit | | `wspr_snr_trend` | Hourly SNR trend for a specific path over time | tx, rx, band, hours | +| `get_version_info` | Service version + upstream spec version (fleet identity attestation) | — | ## What is WSPR? diff --git a/pyproject.toml b/pyproject.toml index f8a019b..a657ab6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wspr-mcp" -version = "0.3.1" +version = "0.3.2" description = "MCP server for WSPR beacon data analytics — band openings, path analysis, solar correlation" readme = "README.md" license = {text = "GPL-3.0-or-later"} diff --git a/src/wspr_mcp/__init__.py b/src/wspr_mcp/__init__.py index 67267cc..a38a2af 100644 --- a/src/wspr_mcp/__init__.py +++ b/src/wspr_mcp/__init__.py @@ -2,9 +2,18 @@ from __future__ import annotations +from importlib.metadata import PackageNotFoundError, version +from typing import Final + try: - from importlib.metadata import version + _pkg_version = version("wspr-mcp") +except PackageNotFoundError: # local dev / editable installs without dist metadata + _pkg_version = "0.0.0-dev" + +__version__: Final[str] = _pkg_version - __version__ = version("wspr-mcp") -except Exception: - __version__ = "0.0.0-dev" +# Upstream data spec the server is bound to. Pinned to the wspr.live +# ClickHouse schema revision we query — bump this when wspr.live +# publishes a new schema. Reported by the get_version_info tool so +# agents can detect fleet drift without going outside the MCP protocol. +__spec_version__: Final[str] = "wspr-live-v1" diff --git a/src/wspr_mcp/server.py b/src/wspr_mcp/server.py index ac5af1d..b81e37b 100644 --- a/src/wspr_mcp/server.py +++ b/src/wspr_mcp/server.py @@ -12,7 +12,7 @@ from fastmcp import FastMCP -from . import __version__ +from . import __spec_version__, __version__ from .client import WSPRClient mcp = FastMCP( @@ -38,6 +38,37 @@ def _get_client() -> WSPRClient: return _client +# --------------------------------------------------------------------------- +# Tool 0: get_version_info — fleet identity attestation +# --------------------------------------------------------------------------- + + +def _version_info_payload() -> dict[str, Any]: + """Build the version info envelope. Pulled into a helper so tests can + call it directly without going through the FastMCP wrapper.""" + return { + "service_name": "wspr-mcp", + "service_version": __version__, + "spec_version": __spec_version__, + } + + +@mcp.tool() +def get_version_info() -> dict[str, Any]: + """Get wspr-mcp service version and upstream spec version. + + Returns the running PyPI version of wspr-mcp and the wspr.live + ClickHouse schema revision in use. Use this to confirm fleet + alignment across MCP deployments — agents can compare service_version + and spec_version across servers to detect drift without going outside + the MCP protocol. + + Returns: + service_name, service_version (PyPI), and spec_version (wspr.live schema). + """ + return _version_info_payload() + + # --------------------------------------------------------------------------- # Tool 1: wspr_spots — "Who's hearing me?" / "What's on the air?" # --------------------------------------------------------------------------- diff --git a/tests/test_tools.py b/tests/test_tools.py index 19a0edc..bf78d18 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -268,3 +268,48 @@ def test_band_filter_value(self): """WSPR-L2-045: Band filter with value returns SQL fragment.""" result = WSPRClient._band_filter(14) assert result == "band = 14" + + +# --------------------------------------------------------------------------- +# WSPR-L2-046..050: get_version_info — fleet identity attestation +# --------------------------------------------------------------------------- + + +class TestGetVersionInfo: + """Tracks IONIS-AI/ionis-devel#49 — fleet get_version_info convention.""" + + def test_returns_service_name(self): + """WSPR-L2-046: payload includes service_name = 'wspr-mcp'.""" + from wspr_mcp.server import _version_info_payload + + assert _version_info_payload()["service_name"] == "wspr-mcp" + + def test_returns_service_version(self): + """WSPR-L2-047: service_version matches package __version__.""" + from wspr_mcp import __version__ + from wspr_mcp.server import _version_info_payload + + assert _version_info_payload()["service_version"] == __version__ + + def test_returns_spec_version(self): + """WSPR-L2-048: spec_version pins the wspr.live ClickHouse schema.""" + from wspr_mcp.server import _version_info_payload + + assert _version_info_payload()["spec_version"] == "wspr-live-v1" + + def test_payload_keys_are_required_set(self): + """WSPR-L2-049: payload has exactly the required keys (no extras yet).""" + from wspr_mcp.server import _version_info_payload + + result = _version_info_payload() + required = {"service_name", "service_version", "spec_version"} + assert required.issubset(set(result.keys())) + + def test_all_values_are_strings(self): + """WSPR-L2-050: all returned values are strings (JSON-safe envelope).""" + from wspr_mcp.server import _version_info_payload + + result = _version_info_payload() + for k in ("service_name", "service_version", "spec_version"): + assert isinstance(result[k], str), f"{k} should be str, got {type(result[k])}" + assert result[k], f"{k} should be non-empty"