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
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: CI

on:
pull_request:
branches: [main]
push:
branches: [main]

# Cancel superseded runs on the same ref (push or PR) so we don't waste
# minutes when commits land in rapid succession.
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."
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Changelog

All notable changes to `solar-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.2.1] — 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 `noaa-swpc-v1`
for the current NOAA SWPC endpoint set.
- L2 unit tests SOLAR-L2-041 through SOLAR-L2-045 covering the new tool.

### Changed
- `__init__.py` modernized to mirror the `adif-mcp` pattern (`Final` types,
explicit `PackageNotFoundError` handling).

## [0.2.0] — 2026-05-14

- Standardized `pyproject.toml` metadata (email, URLs, classifiers).
- Added L2 unit tests for all 6 tools and helper functions.
- Added L3 live integration tests against NOAA SWPC endpoints.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pip install solar-mcp
| `solar_wind` | Real-time DSCOVR L1 solar wind (Bz, speed, density) |
| `solar_xray` | GOES X-ray flux and solar flare classification |
| `solar_band_outlook` | HF band-by-band propagation assessment (160m-6m) |
| `get_version_info` | Service version + upstream spec version (fleet identity attestation) |

## Quick Start

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "solar-mcp"
version = "0.2.0"
version = "0.2.1"
description = "MCP server for solar indices and space weather — SFI, SSN, Kp, DSCOVR, alerts"
readme = "README.md"
license = {text = "GPL-3.0-or-later"}
Expand Down
18 changes: 14 additions & 4 deletions src/solar_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@

from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version
from typing import Final

try:
from importlib.metadata import version
_pkg_version = version("solar-mcp")
except PackageNotFoundError: # local dev / editable installs without dist metadata
_pkg_version = "0.0.0-dev"

__version__: Final[str] = _pkg_version

__version__ = version("solar-mcp")
except Exception:
__version__ = "0.0.0-dev"
# Upstream data spec the server is bound to. Pinned via the NOAA SWPC public
# endpoint set we consume — bump this when SWPC publishes a new endpoint
# contract (different JSON schema, new feed family, etc.). Reported by the
# get_version_info tool so agents can detect fleet drift without going
# outside the MCP protocol.
__spec_version__: Final[str] = "noaa-swpc-v1"
29 changes: 28 additions & 1 deletion src/solar_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

from fastmcp import FastMCP

from . import __version__
from . import __spec_version__, __version__
from .client import SolarClient

mcp = FastMCP(
Expand Down Expand Up @@ -36,6 +36,33 @@ def _get_client() -> SolarClient:
# ---------------------------------------------------------------------------


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": "solar-mcp",
"service_version": __version__,
"spec_version": __spec_version__,
}


@mcp.tool()
def get_version_info() -> dict[str, Any]:
"""Get solar-mcp service version and upstream spec version.

Returns the running PyPI version of solar-mcp and the NOAA SWPC
endpoint set revision currently 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 (NOAA SWPC
endpoint set).
"""
return _version_info_payload()


@mcp.tool()
def solar_conditions() -> dict[str, Any]:
"""Get current solar conditions — SFI, Kp, and NOAA space weather scales.
Expand Down
45 changes: 45 additions & 0 deletions tests/test_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,3 +298,48 @@ def test_classify_xray_extreme(self):
# Quiet sun
result = SolarClient._classify_xray(1e-9)
assert result.startswith("A")


# ---------------------------------------------------------------------------
# SOLAR-L2-041..045: 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):
"""SOLAR-L2-041: payload includes service_name = 'solar-mcp'."""
from solar_mcp.server import _version_info_payload

assert _version_info_payload()["service_name"] == "solar-mcp"

def test_returns_service_version(self):
"""SOLAR-L2-042: service_version matches package __version__."""
from solar_mcp import __version__
from solar_mcp.server import _version_info_payload

assert _version_info_payload()["service_version"] == __version__

def test_returns_spec_version(self):
"""SOLAR-L2-043: spec_version pins the NOAA SWPC endpoint set."""
from solar_mcp.server import _version_info_payload

assert _version_info_payload()["spec_version"] == "noaa-swpc-v1"

def test_payload_keys_are_required_set(self):
"""SOLAR-L2-044: payload has exactly the required keys (no extras yet)."""
from solar_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):
"""SOLAR-L2-045: all returned values are strings (JSON-safe envelope)."""
from solar_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"
Loading