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
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,38 @@

All notable changes to this project will be documented in this file.

## [1.1.0] - 2026-05-16

### Added

* **ADIF 3.1.7 specification support**. Full official 3.1.7 JSON resources from [adif.org.uk](https://adif.org.uk/317/) installed alongside the sealed 3.1.6 set. New enum entries (pure additive — no breaking changes):
- New Mode: **OFDM** (Orthogonal Frequency-Division Multiplexing including COFDM)
- New Submode: **FT2** (Mode MFSK)
- New Submode: **FREEDATA** (Mode DYNAMIC)
- New Submode: **RIBBIT_PIX** (Mode OFDM — Images transmitted using Ribbit)
- New Submode: **RIBBIT_SMS** (Mode OFDM)
* `src/adif_mcp/resources/spec/317/` — full 30-file canonical 3.1.7 JSON set (29 official from upstream ZIP + project-internal `enumerations_country.json` carried forward).
* Test corpus `test/data/ADIF_317_test_QSOs_2026_03_22.adi` (6,197 records, +6 over 3.1.6) — official G3ZOD-generated file from upstream `ADIF_317_resources_2026_03_22.zip`.
* New tests covering the 5 new enum entries (`test_317_new_mode_ofdm`, `test_317_new_submode_ft2`, `test_317_new_submode_freedata`, `test_317_new_submode_ribbit_pix`, `test_317_new_submode_ribbit_sms`, `test_317_submode_count`).

### Changed

* Default spec set switched to **3.1.7** (`__adif_spec__`, `[tool.adif] spec_version`, `[tool.adif_mcp] spec`, `get_spec_text(version=...)` default).
* `test_list_enumerations_has_mode` — Mode record count expectation 90 → 91. Import-only count stays 42 (OFDM is not import-only).
* `test_official_adif_test_file_zero_errors` — switched to the 3.1.7 corpus, record count 6191 → 6197.
* `test_pkg_meta_exposed` — accepted `__adif_spec__` set now includes "3.1.7".
* Docstrings, README, and project URLs updated to reference 3.1.7.

### Preserved

* `src/adif_mcp/resources/spec/316/` — sealed and untouched. Callers explicitly pinning to 3.1.6 continue to work via `get_spec_text(filename, version="316")`.
* `test/data/ADIF_316_test_QSOs_2025_09_15.adi` — sealed for regression reference.

### Reference

* Upstream spec changes: https://adif.org.uk/317/ADIF_317_annotated.htm
* Tracks fleet rollout pattern from IONIS-AI/ionis-devel#49 (also bumps `get_version_info`'s reported `adif_spec_version` from `3.1.6` to `3.1.7`).

## [0.4.4] - 2025-12-28

### **Added**
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
<!-- mcp-name: io.github.qso-graph/adif-mcp -->
# adif-mcp

Core [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for **Amateur Radio Logging**, built on the [ADIF 3.1.6 specification](https://adif.org.uk/316/ADIF_316.htm).
Core [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for **Amateur Radio Logging**, built on the [ADIF 3.1.7 specification](https://adif.org.uk/317/ADIF_317.htm).

## Overview

adif-mcp gives AI agents safe, typed access to Amateur Radio logging data. It validates and parses ADIF records, searches the full ADIF 3.1.6 specification (fields, enumerations, data types), and provides geospatial utilities for Maidenhead locators.
adif-mcp gives AI agents safe, typed access to Amateur Radio logging data. It validates and parses ADIF records, searches the full ADIF 3.1.7 specification (fields, enumerations, data types), and provides geospatial utilities for Maidenhead locators.

[![Made with Python](https://img.shields.io/badge/Made%20with-Python-blue)](https://www.python.org/)
[![License: GPL-3.0](https://img.shields.io/badge/License-GPL--3.0-blue.svg)](LICENSE)
[![ADIF 3.1.6](https://img.shields.io/badge/ADIF-3.1.6-blue)](https://adif.org.uk/316/ADIF_316.htm)
[![ADIF 3.1.7](https://img.shields.io/badge/ADIF-3.1.7-blue)](https://adif.org.uk/317/ADIF_317.htm)
[![PyPI](https://img.shields.io/pypi/v/adif-mcp)](https://pypi.org/project/adif-mcp/)
[![CI](https://github.com/qso-graph/adif-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/qso-graph/adif-mcp/actions/workflows/ci.yml)
[![Docs](https://img.shields.io/badge/docs-qso--graph.io-blue)](https://qso-graph.io/)
Expand Down Expand Up @@ -114,7 +114,7 @@ adif-mcp exposes **8 tools** via the Model Context Protocol:

| Category | Tool | Description |
|----------|------|-------------|
| **Validation** | `validate_adif_record` | Validate a raw ADIF string against the 3.1.6 spec |
| **Validation** | `validate_adif_record` | Validate a raw ADIF string against the 3.1.7 spec |
| **Validation** | `parse_adif` | Streaming parser for large ADIF files with pagination |
| **Spec** | `read_specification_resource` | Retrieve raw JSON for any spec module (band, mode, fields) |
| **Spec** | `list_enumerations` | List all ADIF enumerations with entry counts |
Expand All @@ -130,7 +130,7 @@ adif-mcp is the **ADIF specification package** -- validation, parsing, and geosp
| Package | PyPI | What It Does |
|---------|------|-------------|
| [`qso-graph-auth`](https://pypi.org/project/qso-graph-auth/) | v0.1.1 | OS keyring credential management, persona CRUD |
| [`adif-mcp`](https://pypi.org/project/adif-mcp/) | v1.0.5 | ADIF 3.1.6 spec tools, validation, parsing, geospatial |
| [`adif-mcp`](https://pypi.org/project/adif-mcp/) | v1.1.0 | ADIF 3.1.7 spec tools, validation, parsing, geospatial |
| [`eqsl-mcp`](https://pypi.org/project/eqsl-mcp/) | v0.3.1 | eQSL inbox, verification, AG status, last upload |
| [`qrz-mcp`](https://pypi.org/project/qrz-mcp/) | v0.3.1 | Callsign lookup, DXCC, logbook status/fetch |
| [`lotw-mcp`](https://pypi.org/project/lotw-mcp/) | v0.3.1 | LoTW confirmations, QSOs, DXCC credits, user activity |
Expand All @@ -148,7 +148,7 @@ Authenticated servers use [qso-graph-auth](https://pypi.org/project/qso-graph-au

## Compliance & Provenance

adif-mcp follows the [ADIF Specification](https://adif.org.uk) (currently 3.1.6) and uses **registered Program IDs** to identify all exports:
adif-mcp follows the [ADIF Specification](https://adif.org.uk) (currently 3.1.7) and uses **registered Program IDs** to identify all exports:

- `ADIF-MCP` -- Core engine
- `ADIF-MCP-LOTW` -- LoTW server
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "adif-mcp"
version = "1.0.5"
version = "1.1.0"
description = "ADIF MCP server — spec parsing, validation, and field enumeration tools"
readme = "README.md"
license = { text = "GPL-3.0-or-later" }
Expand Down Expand Up @@ -84,7 +84,7 @@ Homepage = "https://qso-graph.io"
Documentation = "https://qso-graph.io"
Repository = "https://github.com/qso-graph/adif-mcp"
Issues = "https://github.com/qso-graph/adif-mcp/issues"
"ADIF Specification" = "https://adif.org.uk/316/ADIF_316.htm"
"ADIF Specification" = "https://adif.org.uk/317/ADIF_317.htm"

[project.scripts]
adif-mcp = "adif_mcp.cli.__main__:main"
Expand All @@ -93,7 +93,7 @@ adif-mcp = "adif_mcp.cli.__main__:main"
# ADIF tool config (OS-agnostic & SSOT-ish; helpers interpret it)
# --------------------------------------------------------------------
[tool.adif]
spec_version = "3.1.6"
spec_version = "3.1.7"
features = ["core QSO model", "band/mode/QSL_RCVD enums"]

[tool.adif_mcp]
Expand All @@ -103,7 +103,7 @@ meta_output = "adif_meta.json"
personas_index = "personas.json"
providers_dir = "providers"
schemas = "adif_catalog.json"
spec = "ADIF_316"
spec = "ADIF_317"

[tool.pytest.ini_options]
testpaths = ["test"]
Expand Down
2 changes: 1 addition & 1 deletion src/adif_mcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
_pkg_version = "0.0.0"

__version__: Final[str] = _pkg_version
__adif_spec__: Final[str] = "3.1.6"
__adif_spec__: Final[str] = "3.1.7"
2 changes: 1 addition & 1 deletion src/adif_mcp/adif_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
"core QSO model",
"band/mode/QSL_RCVD enums"
],
"spec_version": "3.1.6"
"spec_version": "3.1.7"
}
2 changes: 1 addition & 1 deletion src/adif_mcp/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
_pkg_version = "0.0.0"

__version__: Final[str] = _pkg_version
__adif_spec__: Final[str] = "3.1.6"
__adif_spec__: Final[str] = "3.1.7"

__all__: list[str] = []

Expand Down
2 changes: 1 addition & 1 deletion src/adif_mcp/dev/build_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
During `uv build` / `hatchling` builds, this hook reads:

[tool.adif]
spec_version = "3.1.6"
spec_version = "3.1.7"
features = ["core QSO model", "band/mode/QSL_RCVD enums"]

…and emits `src/adif_mcp/adif_meta.json` so the wheel/sdist contains a
Expand Down
14 changes: 7 additions & 7 deletions src/adif_mcp/mcp/server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
ADIF-MCP Server: Authoritative 3.1.6 Specification Server.
ADIF-MCP Server: Authoritative 3.1.7 Specification Server.

Provides tools for parsing, streaming, and validating ADIF data.
"""
Expand Down Expand Up @@ -412,8 +412,8 @@ def _validate_time(field_name: str, value: str) -> List[str]:
# --- Spec File Loader ---


def get_spec_text(filename: str, version: str = "316") -> str:
"""Retrieve raw text of a 3.1.6 specification JSON file."""
def get_spec_text(filename: str, version: str = "317") -> str:
"""Retrieve raw text of a 3.1.7 specification JSON file."""
current_dir = os.path.dirname(os.path.abspath(__file__))
json_dir = os.path.abspath(
os.path.join(current_dir, "..", "resources", "spec", version)
Expand Down Expand Up @@ -539,13 +539,13 @@ async def parse_adif(

@mcp.tool()
def read_specification_resource(resource_name: str) -> str:
"""Reads an ADIF 3.1.6 specification resource (e.g., 'mode')."""
"""Reads an ADIF 3.1.7 specification resource (e.g., 'mode')."""
return get_spec_text(resource_name)


@mcp.tool()
def list_enumerations() -> Dict[str, Any]:
"""Lists all 25 ADIF 3.1.6 enumerations with record counts and fields."""
"""Lists all 25 ADIF 3.1.7 enumerations with record counts and fields."""
result: Dict[str, Any] = {}
for enum_name, fields in ENUMERATION_FIELDS.items():
records = _load_enum_records(enum_name)
Expand All @@ -566,7 +566,7 @@ def search_enumerations(
search_term: str,
enumeration: Optional[str] = None,
) -> Dict[str, Any]:
"""Searches ADIF 3.1.6 enumerations. Optionally filter by enumeration name."""
"""Searches ADIF 3.1.7 enumerations. Optionally filter by enumeration name."""
term = search_term.upper().strip()
if not term:
return {"error": "Search term must not be empty."}
Expand Down Expand Up @@ -619,7 +619,7 @@ def search_enumerations(

@mcp.tool()
def validate_adif_record(adif_string: str) -> Dict[str, Any]:
"""Validates an ADIF record against 3.1.6 rules including enum membership."""
"""Validates an ADIF record against 3.1.7 rules including enum membership."""
parsed = parse_adif_internal(adif_string)

try:
Expand Down
4 changes: 2 additions & 2 deletions src/adif_mcp/models/qso.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""ADIF 3.1.6 QSO Model definition."""
"""ADIF 3.1.7 QSO Model definition."""

from datetime import date, time
from typing import Optional
Expand All @@ -8,7 +8,7 @@

class QSO(BaseModel):
"""
Represents a single ADIF 3.1.6 QSO record.
Represents a single ADIF 3.1.7 QSO record.

Strictly typed to prevent hallucinations and ensure protocol integrity.
"""
Expand Down
6 changes: 6 additions & 0 deletions src/adif_mcp/resources/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""JSON Schemas bundled with adif-mcp.

Currently contains:
- manifest.v1.json — schema for src/adif_mcp/mcp/manifest.json validation
(consumed by the `adif-mcp validate-manifest` CLI command).
"""
74 changes: 74 additions & 0 deletions src/adif_mcp/resources/schemas/manifest.v1.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://qso-graph.io/schemas/adif-mcp/manifest.v1.json",
"title": "adif-mcp manifest schema (v1)",
"description": "JSON Schema validating src/adif_mcp/mcp/manifest.json — the declarative description of the adif-mcp MCP server, its tool surface, and the shared QSO record schema.",
"type": "object",
"required": ["schema_version", "name", "version", "description", "tools"],
"additionalProperties": true,
"properties": {
"schema_version": {
"type": "string",
"description": "Manifest schema major version (e.g. '1.0'). Bumped when the manifest contract changes incompatibly.",
"pattern": "^[0-9]+\\.[0-9]+$"
},
"name": {
"type": "string",
"description": "MCP server name (matches PyPI package name).",
"minLength": 1
},
"version": {
"type": "string",
"description": "Manifest content version — semver-shaped.",
"minLength": 1
},
"description": {
"type": "string",
"description": "One-line description of the server.",
"minLength": 1
},
"components": {
"type": "object",
"description": "Reusable shared schemas referenced by tool input/output definitions.",
"additionalProperties": true,
"properties": {
"schemas": {
"type": "object",
"description": "Map of schema name → JSON Schema. Referenced from tools via $ref: '#/components/schemas/<Name>'.",
"additionalProperties": {
"type": "object"
}
}
}
},
"tools": {
"type": "array",
"description": "MCP tools exposed by this server. Each entry declares its input/output contract.",
"items": {
"type": "object",
"required": ["name", "description"],
"additionalProperties": true,
"properties": {
"name": {
"type": "string",
"description": "Fully-qualified tool name (e.g. 'eqsl.fetch_inbox').",
"minLength": 1
},
"description": {
"type": "string",
"description": "Human-readable description of what the tool does.",
"minLength": 1
},
"input_schema": {
"type": "object",
"description": "JSON Schema describing the tool's expected input arguments."
},
"output_schema": {
"type": "object",
"description": "JSON Schema describing the tool's response shape."
}
}
}
}
}
}
Loading
Loading