adif-mcp 0.1.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,90 @@
1
+ .venv/
2
+
3
+ .pytest_cache/
4
+ .cache/
5
+ data/*.duckdb
6
+ *.tar.gz
7
+ .DS_Store
8
+ .cache_history.json
9
+ z-my-notes.md
10
+ pre-release-checklist*
11
+ rubric*
12
+ todo*
13
+ src/adif_mcp/adif_meta.json
14
+ .smoke-venv/
15
+ dist/
16
+ build/
17
+ __pycache__/
18
+ *.py[cod]
19
+ *$py.class
20
+
21
+ # ------------------------
22
+ # MkDocs Local Site Build
23
+ # ------------------------
24
+ site/
25
+ !mkdocs.yml
26
+ !docs/
27
+ !docs/**
28
+
29
+
30
+ # ------------------------
31
+ # OS cruft
32
+ # ------------------------
33
+ .DS_Store
34
+ Thumbs.db
35
+
36
+ # ------------------------
37
+ # Python / venv / tooling
38
+ # ------------------------
39
+ .venv/
40
+
41
+ .pytest_cache/
42
+ .coverage
43
+ htmlcov/
44
+ *.pyc
45
+ *.pyo
46
+ *.pyd
47
+ *.egg-info/
48
+ .eggs/
49
+ .cache/ # local caches (pytest, tools, etc.)
50
+
51
+ # ------------------------
52
+ # Streamlit & app secrets
53
+ # ------------------------
54
+ .streamlit/secrets.toml
55
+ .env
56
+ .env.* # .env.local, etc.
57
+
58
+ # ------------------------
59
+ # Data / artifacts
60
+ # ------------------------
61
+ data/*.duckdb # local DB produced by ingest
62
+ data/*.duckdb-wal
63
+ *.tar.gz # release/packaging artifacts
64
+ *.zip
65
+
66
+ # WSPR ingest cache tracking (local machine paths)
67
+ .cache_history.json
68
+
69
+ # Optional: any custom download cache dirs you use
70
+ .cache/
71
+ .cache_wspr/
72
+
73
+ # ------------------------
74
+ # Notebooks (if you add any later)
75
+ # ------------------------
76
+ .ipynb_checkpoints/
77
+
78
+ # ------------------------
79
+ # IDE/editor
80
+ # ------------------------
81
+ .vscode/
82
+ .idea/
83
+ *.code-workspace
84
+
85
+ # ------------------------
86
+ # Logs / temp
87
+ # ------------------------
88
+ *.log
89
+ tmp/
90
+ temp/
adif_mcp-0.1.5/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Metadata-Version: 2.4
2
+ Name: adif-mcp
3
+ Version: 0.1.5
4
+ Summary: ADIF MCP core engine with plugin support (LoTW, eQSL)
5
+ Project-URL: ADIF Spec, https://adif.org.uk/315/ADIF_315.htm
6
+ Project-URL: Compatibility, https://adif-mcp.com/changelog/#adif-compatibility
7
+ Author: KI7MT
8
+ License: MIT
9
+ License-File: LICENSE
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: click
12
+ Requires-Dist: pytest>=8.4.1
13
+ Requires-Dist: requests>=2.32.5
14
+ Requires-Dist: typing-extensions>=4.15.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # adif-mcp
18
+
19
+ Core [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) service for **Amateur Radio Logging**, per [ADIF 3.1.5 specification](https://adif.org.uk/315/ADIF_315.htm)
20
+
21
+ ---
22
+
23
+ ## Resources
24
+
25
+ [![Made with Python](https://img.shields.io/badge/Made%20with-Python-blue)](https://www.python.org/)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
27
+
28
+ ## Core Project Health
29
+ ![ADIF](https://img.shields.io/badge/ADIF-3.1.5-blue)
30
+ [![GitHub release](https://img.shields.io/github/v/release/KI7MT/adif-mcp)](https://github.com/KI7MT/adif-mcp/releases)
31
+ [![GitHub tag](https://img.shields.io/github/v/tag/KI7MT/adif-mcp?sort=semver)](https://github.com/KI7MT/adif-mcp/tags)
32
+ [![CI](https://github.com/KI7MT/adif-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/KI7MT/adif-mcp/actions/workflows/ci.yml)
33
+ [![Docs](https://img.shields.io/badge/docs-github_pages-blue)](https://adif-mcp.com/)
34
+ [![pre-commit](https://github.com/KI7MT/wspr-ai-lite/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/KI7MT/wspr-ai-lite/actions/workflows/pre-commit.yml)
35
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
36
+ [![SSL Certificate Expiry Check](https://github.com/KI7MT/adif-mcp/actions/workflows/ssl-expiry.yml/badge.svg)](https://github.com/KI7MT/adif-mcp/actions/workflows/ssl-expiry.yml)
37
+
38
+
39
+ ## MCP / API Readiness
40
+ [![MCP](https://img.shields.io/badge/AI--Agent--Ready-MCP-green)](https://modelcontextprotocol.io/)
41
+ [![API Docs](https://img.shields.io/badge/API-Schema-blue)](https://adif-mcp.com/mcp/manifest.html)
42
+ [![JSON Schema](https://img.shields.io/badge/Schema-JSON--Schema-lightgrey)](#)
43
+ [![Manifest Validate](https://github.com/KI7MT/adif-mcp/actions/workflows/manifest-validate.yml/badge.svg)](https://github.com/KI7MT/adif-mcp/actions/workflows/manifest-validate.yml)
44
+
45
+
46
+ ## Compliance and Program Registry
47
+ [![ADIF 3.1.5](https://img.shields.io/badge/ADIF-3.1.5-blue?label=Spec)](#-compliance--provenance)
48
+ [![Program ID](https://img.shields.io/badge/Program%20ID-Registered-success)](https://adif.org.uk/programids.html)
49
+
50
+ ---
51
+
52
+ ## Overview
53
+
54
+ This package defines the ADIF MCP core engine, with plugins for:
55
+ - LoTW (`adif-mcp-lotw`)
56
+ - eQSL (`adif-mcp-eqsl`)
57
+
58
+ Performs these tasks
59
+ - Validation & normalization of ADIF records
60
+ - Unified schema for consistent QSO storage and exchange
61
+ - MCP-ready tools for safe AI-agent access
62
+ - Foundation for service adapters (e.g., LoTW, eQSL)
63
+
64
+ ---
65
+
66
+ ## Why?
67
+ Every amateur radio logger supports ADIF, but implementations are fragmented.
68
+ `adif-mcp` offers a **single, standards-based interface** to make QSO data portable, auditable, and agent-friendly.
69
+
70
+ ## Next Steps
71
+ - Build `adif-mcp-lotw` and `adif-mcp-eqsl` adapters
72
+ - Expose MCP tools for validation, award tracking, and service sync
73
+ - Support cross-logger interoperability with AI-driven agents
74
+
75
+ ## Compliance & Provenance
76
+
77
+ ADIF-MCP and its plugins follow the [ADIF Specification](https://adif.org.uk) (currently 3.1.5) and use **registered Program IDs** to identify all exports:
78
+
79
+ - `ADIF-MCP` — Core engine
80
+ - `ADIF-MCP-LOTW` — Plugin for ARRL Logbook of The World
81
+ - `ADIF-MCP-EQSL` — Plugin for eQSL.cc
82
+
83
+ To ensure transparency and auditability, the project also uses **APP_ fields** for provenance when augmenting records.
84
+ Examples include:
85
+
86
+ - `APP_ADIF-MCP_OP` → operation performed (`normalize`, `validate`, `merge`)
87
+ - `APP_ADIF-MCP-LOTW_ACTION` → LoTW plugin operation
88
+ - `APP_ADIF-MCP-EQSL_TIME` → timestamp of eQSL merge
89
+
90
+ See the [Program ID & APP_ Field Policy](docs/program-id-policy.md) for full details.
91
+
92
+ ## License
93
+ MIT — open and free for amateur radio use.
@@ -0,0 +1,77 @@
1
+ # adif-mcp
2
+
3
+ Core [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) service for **Amateur Radio Logging**, per [ADIF 3.1.5 specification](https://adif.org.uk/315/ADIF_315.htm)
4
+
5
+ ---
6
+
7
+ ## Resources
8
+
9
+ [![Made with Python](https://img.shields.io/badge/Made%20with-Python-blue)](https://www.python.org/)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
11
+
12
+ ## Core Project Health
13
+ ![ADIF](https://img.shields.io/badge/ADIF-3.1.5-blue)
14
+ [![GitHub release](https://img.shields.io/github/v/release/KI7MT/adif-mcp)](https://github.com/KI7MT/adif-mcp/releases)
15
+ [![GitHub tag](https://img.shields.io/github/v/tag/KI7MT/adif-mcp?sort=semver)](https://github.com/KI7MT/adif-mcp/tags)
16
+ [![CI](https://github.com/KI7MT/adif-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/KI7MT/adif-mcp/actions/workflows/ci.yml)
17
+ [![Docs](https://img.shields.io/badge/docs-github_pages-blue)](https://adif-mcp.com/)
18
+ [![pre-commit](https://github.com/KI7MT/wspr-ai-lite/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/KI7MT/wspr-ai-lite/actions/workflows/pre-commit.yml)
19
+ [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
20
+ [![SSL Certificate Expiry Check](https://github.com/KI7MT/adif-mcp/actions/workflows/ssl-expiry.yml/badge.svg)](https://github.com/KI7MT/adif-mcp/actions/workflows/ssl-expiry.yml)
21
+
22
+
23
+ ## MCP / API Readiness
24
+ [![MCP](https://img.shields.io/badge/AI--Agent--Ready-MCP-green)](https://modelcontextprotocol.io/)
25
+ [![API Docs](https://img.shields.io/badge/API-Schema-blue)](https://adif-mcp.com/mcp/manifest.html)
26
+ [![JSON Schema](https://img.shields.io/badge/Schema-JSON--Schema-lightgrey)](#)
27
+ [![Manifest Validate](https://github.com/KI7MT/adif-mcp/actions/workflows/manifest-validate.yml/badge.svg)](https://github.com/KI7MT/adif-mcp/actions/workflows/manifest-validate.yml)
28
+
29
+
30
+ ## Compliance and Program Registry
31
+ [![ADIF 3.1.5](https://img.shields.io/badge/ADIF-3.1.5-blue?label=Spec)](#-compliance--provenance)
32
+ [![Program ID](https://img.shields.io/badge/Program%20ID-Registered-success)](https://adif.org.uk/programids.html)
33
+
34
+ ---
35
+
36
+ ## Overview
37
+
38
+ This package defines the ADIF MCP core engine, with plugins for:
39
+ - LoTW (`adif-mcp-lotw`)
40
+ - eQSL (`adif-mcp-eqsl`)
41
+
42
+ Performs these tasks
43
+ - Validation & normalization of ADIF records
44
+ - Unified schema for consistent QSO storage and exchange
45
+ - MCP-ready tools for safe AI-agent access
46
+ - Foundation for service adapters (e.g., LoTW, eQSL)
47
+
48
+ ---
49
+
50
+ ## Why?
51
+ Every amateur radio logger supports ADIF, but implementations are fragmented.
52
+ `adif-mcp` offers a **single, standards-based interface** to make QSO data portable, auditable, and agent-friendly.
53
+
54
+ ## Next Steps
55
+ - Build `adif-mcp-lotw` and `adif-mcp-eqsl` adapters
56
+ - Expose MCP tools for validation, award tracking, and service sync
57
+ - Support cross-logger interoperability with AI-driven agents
58
+
59
+ ## Compliance & Provenance
60
+
61
+ ADIF-MCP and its plugins follow the [ADIF Specification](https://adif.org.uk) (currently 3.1.5) and use **registered Program IDs** to identify all exports:
62
+
63
+ - `ADIF-MCP` — Core engine
64
+ - `ADIF-MCP-LOTW` — Plugin for ARRL Logbook of The World
65
+ - `ADIF-MCP-EQSL` — Plugin for eQSL.cc
66
+
67
+ To ensure transparency and auditability, the project also uses **APP_ fields** for provenance when augmenting records.
68
+ Examples include:
69
+
70
+ - `APP_ADIF-MCP_OP` → operation performed (`normalize`, `validate`, `merge`)
71
+ - `APP_ADIF-MCP-LOTW_ACTION` → LoTW plugin operation
72
+ - `APP_ADIF-MCP-EQSL_TIME` → timestamp of eQSL merge
73
+
74
+ See the [Program ID & APP_ Field Policy](docs/program-id-policy.md) for full details.
75
+
76
+ ## License
77
+ MIT — open and free for amateur radio use.
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "adif-mcp",
3
+ "version": "1.0.0",
4
+ "description": "ADIF MCP core service",
5
+ "tools": []
6
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "MCP Manifest (minimal v1 subset)",
4
+ "type": "object",
5
+ "required": ["name", "version", "tools"],
6
+ "properties": {
7
+ "name": { "type": "string", "minLength": 1 },
8
+ "version": { "type": "string", "minLength": 1 },
9
+ "description": { "type": "string" },
10
+ "tools": {
11
+ "type": "array",
12
+ "items": {
13
+ "type": "object",
14
+ "required": ["name", "description", "input_schema", "output_schema"],
15
+ "properties": {
16
+ "name": { "type": "string", "minLength": 1 },
17
+ "description": { "type": "string", "minLength": 1 },
18
+ "input_schema": {
19
+ "type": "object",
20
+ "required": ["type", "properties"],
21
+ "properties": {
22
+ "type": { "const": "object" },
23
+ "properties": { "type": "object" },
24
+ "required": { "type": "array", "items": { "type": "string" } },
25
+ "additionalProperties": { "type": "boolean" }
26
+ },
27
+ "additionalProperties": true
28
+ },
29
+ "output_schema": {
30
+ "type": "object",
31
+ "required": ["type", "properties"],
32
+ "properties": {
33
+ "type": { "const": "object" },
34
+ "properties": { "type": "object" },
35
+ "required": { "type": "array", "items": { "type": "string" } },
36
+ "additionalProperties": { "type": "boolean" }
37
+ },
38
+ "additionalProperties": true
39
+ }
40
+ },
41
+ "additionalProperties": true
42
+ }
43
+ }
44
+ },
45
+ "additionalProperties": true
46
+ }
@@ -0,0 +1,134 @@
1
+ [project]
2
+ name = "adif-mcp"
3
+ version = "0.1.5"
4
+ description = "ADIF MCP core engine with plugin support (LoTW, eQSL)"
5
+ authors = [{ name = "KI7MT" }]
6
+ license = { text = "MIT" }
7
+ readme = "README.md"
8
+ requires-python = ">=3.10"
9
+ dependencies = [
10
+ "click",
11
+ "pytest>=8.4.1",
12
+ "requests>=2.32.5",
13
+ "typing-extensions>=4.15.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ "ADIF Spec" = "https://adif.org.uk/315/ADIF_315.htm"
18
+ "Compatibility" = "https://adif-mcp.com/changelog/#adif-compatibility"
19
+
20
+ [tool.adif]
21
+ spec_version = "3.1.5"
22
+ features = ["core QSO model", "band/mode/QSL_RCVD enums"]
23
+
24
+ [project.scripts]
25
+ adif-mcp = "adif_mcp.cli:cli"
26
+
27
+ [tool.uv]
28
+ dev-dependencies = [
29
+ "pytest",
30
+ "mypy",
31
+ "ruff",
32
+ "commitizen",
33
+ "pydantic<2",
34
+ "jsonschema>=4.25.1",
35
+ "interrogate>=1.7.0",
36
+ "types-click>=7.1.8",
37
+ ]
38
+
39
+ [tool.ruff]
40
+ line-length = 88
41
+ target-version = "py311"
42
+ extend-exclude = [
43
+ ".venv",
44
+ ".smoke-venv",
45
+ "dist",
46
+ "build",
47
+ ".eggs",
48
+ ".pytest_cache",
49
+ "site",
50
+ "docs/_site",
51
+ ]
52
+
53
+ [tool.ruff.lint]
54
+ select = ["E", "F", "I"]
55
+ ignore = []
56
+ fixable = ["I"]
57
+
58
+ [tool.ruff.lint.per-file-ignores]
59
+ "scripts/*.py" = ["T201", "E402"]
60
+ "samples/*.py" = ["E402"]
61
+
62
+ [tool.mypy]
63
+ python_version = "3.11"
64
+ plugins = ["pydantic.mypy"]
65
+ strict = true
66
+ disable_error_code = ["annotation-unchecked", "call-arg"] # keep yours
67
+ warn_unused_ignores = true
68
+ warn_redundant_casts = true
69
+ warn_return_any = true
70
+ no_implicit_optional = true
71
+ ignore_missing_imports = true
72
+
73
+ [tool.commitizen]
74
+ name = "cz_conventional_commits"
75
+ version_provider = "pep621"
76
+ tag_format = "v$version"
77
+ update_changelog_on_bump = true
78
+
79
+ [[tool.mypy.overrides]]
80
+ module = "adif_mcp.cli"
81
+ disable_error_code = ["misc"]
82
+ ignore_missing_imports = true
83
+
84
+ [tool.interrogate]
85
+ fail-under = 80
86
+ verbose = 1
87
+ quiet = false
88
+ color = true
89
+ # Count only public API; skip dunders/private
90
+ ignore-init-method = true
91
+ ignore-magic = true
92
+ ignore-module = false
93
+ ignore-private = true
94
+ ignore-nested-functions = true
95
+ # Optional: don’t require docstrings for properties
96
+ ignore-properties = true
97
+ # Only measure our packages
98
+ paths = ["src", "scripts", "test"]
99
+ exclude = [".venv", ".smoke-venv", "dist", "build", "site", "docs/_site"]
100
+
101
+ [build-system]
102
+ requires = ["hatchling>=1.21"]
103
+ build-backend = "hatchling.build"
104
+
105
+ [tool.hatch.build.hooks.custom]
106
+ path = "scripts/build_hooks.py"
107
+
108
+ [tool.hatch.build.targets.wheel]
109
+ packages = ["src/adif_mcp"]
110
+
111
+ [tool.hatch.build.targets.sdist]
112
+ include = [
113
+ "src/**",
114
+ "pyproject.toml",
115
+ "README.md",
116
+ "LICENSE*",
117
+ "mcp/**",
118
+ "scripts/**",
119
+ ]
120
+ exclude = [
121
+ ".venv/**",
122
+ ".smoke-venv/**",
123
+ "dist/**",
124
+ "build/**",
125
+ ".git/**",
126
+ ".github/**",
127
+ "docs/**",
128
+ "site/**",
129
+ ".ruff_cache/**",
130
+ ".mypy_cache/**",
131
+ ".pytest_cache/**",
132
+ "tests/**",
133
+ "test/**",
134
+ ]
@@ -0,0 +1,66 @@
1
+ """
2
+ Custom Hatchling build hook for adif-mcp.
3
+
4
+ This hook runs at build time and copies the `[tool.adif]` table from
5
+ `pyproject.toml` into a generated JSON file at
6
+ `src/adif_mcp/adif_meta.json`.
7
+
8
+ Why:
9
+ - Ensures the ADIF spec version and supported features are baked into
10
+ built wheels and sdists (so the runtime does not need to parse
11
+ `pyproject.toml`).
12
+ - Keeps `pyproject.toml` as the single source of truth for metadata.
13
+ - Provides a consistent fallback mechanism: editable installs read
14
+ directly from `pyproject.toml`, while published artifacts use the
15
+ generated JSON.
16
+
17
+ The generated file is ignored by version control (`.gitignore`) and
18
+ is safe to regenerate on every build.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import json
24
+ import pathlib
25
+ import sys
26
+
27
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
28
+
29
+ try:
30
+ import tomllib # Python 3.11+
31
+ except Exception: # pragma: no cover
32
+ tomllib = None # type: ignore[assignment]
33
+
34
+
35
+ class BuildHook(BuildHookInterface):
36
+ """
37
+ Copy [tool.adif] from pyproject.toml into src/adif_mcp/adif_meta.json
38
+ during build, so wheels contain the ADIF spec metadata.
39
+ """
40
+
41
+ def initialize(self, version: str, build_data: dict) -> None: # noqa: D401
42
+ """Run at build start; write `adif_meta.json` into the package directory."""
43
+ root = pathlib.Path(self.root)
44
+ pyproject = root / "pyproject.toml"
45
+
46
+ meta = {"spec_version": "0", "features": []}
47
+
48
+ if tomllib and pyproject.is_file():
49
+ try:
50
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
51
+ adif = (data.get("tool") or {}).get("adif") or {}
52
+ spec = adif.get("spec_version")
53
+ feats = adif.get("features")
54
+ if isinstance(spec, str):
55
+ meta["spec_version"] = spec
56
+ if isinstance(feats, list):
57
+ meta["features"] = [str(x) for x in feats]
58
+ except Exception as e: # pragma: no cover
59
+ print(
60
+ f"[build-hook] Warning: failed reading [tool.adif]: {e}",
61
+ file=sys.stderr,
62
+ )
63
+
64
+ out = root / "src" / "adif_mcp" / "adif_meta.json"
65
+ out.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
66
+ print(f"[build-hook] wrote {out}")
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/smoke.sh
3
+ # Full local smoke test for adif-mcp using uv + an isolated install check.
4
+
5
+ set -euo pipefail
6
+
7
+ banner() { printf "\n\033[1;36m[smoke]\033[0m %s\n" "$*"; }
8
+
9
+ # Move to repo root (script is expected in ./scripts/)
10
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
11
+ cd "$ROOT_DIR"
12
+
13
+ banner "env info"
14
+ command -v uv >/dev/null || { echo "uv not found. Install with: pip install uv (or: brew install uv)"; exit 1; }
15
+ uv --version || true
16
+ uv run python -V || true
17
+
18
+ banner "sync deps (frozen if lock present)"
19
+ uv sync
20
+
21
+ banner "lint (ruff)"
22
+ uv run ruff check .
23
+
24
+ banner "type-check (mypy)"
25
+ uv run mypy src
26
+
27
+ banner "docstrings (interrogate)"
28
+ uv run interrogate -c pyproject.toml
29
+
30
+ banner "manifest validation"
31
+ uv run python scripts/validate_manifest.py mcp/manifest.json
32
+
33
+ banner "unit tests (pytest)"
34
+ uv run pytest -q
35
+
36
+ banner "build (wheel + sdist)"
37
+ rm -rf dist build *.egg-info
38
+ uv build
39
+ ls -lh dist
40
+
41
+ banner "install built wheel in a fresh, isolated venv"
42
+ VENV=".smoke-venv"
43
+ rm -rf "$VENV"
44
+ uv venv --seed "$VENV"
45
+
46
+ PY="$VENV/bin/python"
47
+ PIP="$VENV/bin/pip"
48
+ BIN="$VENV/bin"
49
+
50
+ echo "[smoke] python: $($PY -V)"
51
+ echo "[smoke] install wheel"
52
+ $PIP install --upgrade pip >/dev/null
53
+ $PIP install dist/*.whl
54
+
55
+ echo "[smoke] sanity: CLI reports version"
56
+ "$BIN/adif-mcp" version
57
+
58
+ echo "[smoke] import/package sanity"
59
+ "$PY" - <<'PY'
60
+ import adif_mcp as m
61
+ print("import OK:", getattr(m, "__version__", "?"), "ADIF", getattr(m, "__adif_spec__", "?"))
62
+ PY
63
+
64
+ echo "[smoke] entrypoints"
65
+ "$BIN/adif-mcp" --help >/dev/null
66
+ "$BIN/adif-mcp" manifest-validate >/dev/null
67
+
68
+ # Optional: clean up the venv created by this smoke run
69
+ rm -rf "$VENV"
70
+
71
+ banner "OK — full smoke test passed"
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """Validate MCP manifest files against a minimal v1 schema.
4
+
5
+ Usage:
6
+ uv run python scripts/validate_manifest.py mcp/manifest.json
7
+ uv run python scripts/validate_manifest.py
8
+ # defaults to mcp/manifest.json if present
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Sequence
17
+
18
+ from jsonschema import validate # type: ignore[import-untyped]
19
+
20
+ SCHEMA_PATH = Path("mcp/schemas/manifest.v1.json")
21
+
22
+
23
+ def load_json(path: Path) -> dict:
24
+ """Read and parse a JSON file into a Python dict."""
25
+ return json.loads(path.read_text(encoding="utf-8"))
26
+
27
+
28
+ def validate_one(path: Path, schema_path: Path = SCHEMA_PATH) -> bool:
29
+ """Validate a single manifest file against the schema and print a short OK line."""
30
+ manifest = load_json(path)
31
+ schema = load_json(schema_path)
32
+ validate(instance=manifest, schema=schema)
33
+ print(f"{path}: OK (jsonschema)")
34
+ return True
35
+
36
+
37
+ def main(argv: Sequence[str]) -> int:
38
+ """CLI entrypoint. Validate one or more manifest paths.
39
+
40
+ If no paths are provided, defaults to `mcp/manifest.json` if present.
41
+ """
42
+ files = (
43
+ [Path(a) for a in argv]
44
+ if argv
45
+ else ([Path("mcp/manifest.json")] if Path("mcp/manifest.json").exists() else [])
46
+ )
47
+ if not files:
48
+ print("No manifest paths provided and default not found.", file=sys.stderr)
49
+ return 2
50
+
51
+ failures = 0
52
+ for p in files:
53
+ try:
54
+ validate_one(p)
55
+ except Exception as e:
56
+ print(f"{p}: INVALID: {e}", file=sys.stderr)
57
+ failures += 1
58
+
59
+ return 0 if failures == 0 else 1
60
+
61
+
62
+ if __name__ == "__main__":
63
+ raise SystemExit(main(sys.argv[1:]))
@@ -0,0 +1,123 @@
1
+ """
2
+ adif_mcp: Core package for ADIF MCP — models, CLI, and manifest tooling.
3
+
4
+ This module exposes:
5
+ - __version__: the installed package version (from package metadata)
6
+ - __adif_spec__: ADIF spec version we target (from pyproject [tool.adif], if available)
7
+ - __adif_features__: short list of notable supported features
8
+ (from pyproject [tool.adif], if available)
9
+
10
+ Design:
11
+ - In dev/editable installs, we read pyproject.toml ([tool.adif]) directly.
12
+ - In built wheels (where pyproject.toml isn’t packaged),
13
+ we fall back to baked-in defaults.
14
+ """
15
+
16
+ import json
17
+ from importlib import resources as _res
18
+ from importlib.metadata import PackageNotFoundError
19
+ from importlib.metadata import version as _pkg_version
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Tuple
22
+
23
+ # -------------------------
24
+ # Package version (metadata)
25
+ # -------------------------
26
+ try:
27
+ __version__ = _pkg_version("adif-mcp")
28
+ except PackageNotFoundError:
29
+ # Editable or not yet built — keep a harmless placeholder
30
+ __version__ = "0.0.0"
31
+
32
+
33
+ # ------------------------------------------
34
+ # Read [tool.adif] from pyproject.toml (dev)
35
+ # ------------------------------------------
36
+ def _find_pyproject(start: Path) -> Path | None:
37
+ """
38
+ Walk upward from `start` to locate a pyproject.toml file.
39
+ Returns the first match or None.
40
+ """
41
+ cur = start.resolve()
42
+ for _ in range(6): # walk up a handful of levels; adjust if needed
43
+ cand = cur / "pyproject.toml"
44
+ if cand.is_file():
45
+ return cand
46
+ if cur.parent == cur:
47
+ break
48
+ cur = cur.parent
49
+ return None
50
+
51
+
52
+ def _load_adif_from_pyproject() -> Tuple[str, List[str]] | None:
53
+ """
54
+ Try to load `[tool.adif]` from a nearby pyproject.toml.
55
+ Returns (spec_version, features) if found, else None.
56
+ """
57
+ try:
58
+ import tomllib # Python 3.11+
59
+ except Exception:
60
+ return None
61
+
62
+ here = Path(__file__).parent
63
+ pyproject = _find_pyproject(here)
64
+ if not pyproject:
65
+ return None
66
+
67
+ try:
68
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
69
+ except Exception:
70
+ return None
71
+
72
+ tool = data.get("tool", {})
73
+ adif: Dict[str, Any] = tool.get("adif", {}) if isinstance(tool, dict) else {}
74
+ spec_version = adif.get("spec_version")
75
+ features = adif.get("features")
76
+
77
+ if isinstance(spec_version, str) and isinstance(features, list):
78
+ # Ensure features are strings
79
+ features_str = [str(x) for x in features]
80
+ return spec_version, features_str
81
+ return None
82
+
83
+
84
+ # --------------------------
85
+ # Fallback baked-in defaults
86
+ # --------------------------
87
+
88
+ # If pyproject.toml isn’t present at runtime (e.g., installed wheel),
89
+ # we still expose meaningful values. Keep these aligned with your releases.
90
+
91
+ _DEFAULT_ADIF_SPEC = "3.1.5"
92
+ _DEFAULT_ADIF_FEATURES = ["core QSO model", "band/mode/QSL_RCVD enums"]
93
+
94
+
95
+ def _adif_meta() -> Tuple[str, List[str]]:
96
+ """
97
+ Resolve ADIF metadata (spec version + features), preferring pyproject.toml.
98
+ """
99
+
100
+ try:
101
+ with (
102
+ _res.files("adif_mcp")
103
+ .joinpath("adif_meta.json")
104
+ .open("r", encoding="utf-8") as fh
105
+ ):
106
+ data = json.load(fh)
107
+ spec = data.get("spec_version")
108
+ feats = data.get("features")
109
+ if isinstance(spec, str) and isinstance(feats, list):
110
+ return spec, [str(x) for x in feats]
111
+ except Exception:
112
+ pass
113
+
114
+ loaded = _load_adif_from_pyproject()
115
+ if loaded:
116
+ return loaded
117
+
118
+ # Optional future enhancement:
119
+ # Try reading a packaged JSON resource like "adif_meta.json" here.
120
+ return _DEFAULT_ADIF_SPEC, _DEFAULT_ADIF_FEATURES
121
+
122
+
123
+ __adif_spec__, __adif_features__ = _adif_meta()
@@ -0,0 +1,55 @@
1
+ """
2
+ Command-line entry points for adif-mcp.
3
+
4
+ Commands:
5
+ - version -> prints package version + ADIF spec version
6
+ - manifest-validate -> quick shape/sanity validation for MCP manifest
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import pathlib
13
+
14
+ import click
15
+
16
+ from adif_mcp import __adif_spec__, __version__
17
+
18
+
19
+ @click.group()
20
+ @click.version_option(version=__version__, prog_name="adif-mcp")
21
+ def cli() -> None:
22
+ """ADIF MCP core CLI."""
23
+ # No-op; subcommands below.
24
+ return
25
+
26
+
27
+ @cli.command("version")
28
+ def version_cmd() -> None:
29
+ """Show package version and ADIF spec compatibility."""
30
+ click.echo(f"adif-mcp {__version__} (ADIF {__adif_spec__} compatible)")
31
+
32
+
33
+ @cli.command("manifest-validate")
34
+ def manifest_validate() -> None:
35
+ """
36
+ Validate the MCP manifest’s basic shape.
37
+
38
+ This is a lightweight check that ensures the file exists and has a top-level
39
+ 'tools' array. For full schema validation, use the repo’s CI workflow or
40
+ the stricter validation script.
41
+ """
42
+ p = pathlib.Path("mcp/manifest.json")
43
+ if not p.exists():
44
+ raise click.ClickException(f"manifest not found: {p}")
45
+
46
+ try:
47
+ data = json.loads(p.read_text(encoding="utf-8"))
48
+ except json.JSONDecodeError as e:
49
+ raise click.ClickException(f"invalid JSON in {p}: {e}") from e
50
+
51
+ tools = data.get("tools")
52
+ if not isinstance(tools, list):
53
+ raise click.ClickException("manifest.tools missing or not a list")
54
+
55
+ click.echo("manifest: OK")
@@ -0,0 +1,124 @@
1
+ """Pydantic models & constrained types for ADIF MCP core.
2
+
3
+ These models define a safe, minimal record shape used for validation and
4
+ normalization of ADIF-style QSO data, plus small batch I/O envelopes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, List, Literal, Optional
10
+
11
+ from pydantic import BaseModel, Field
12
+ from typing_extensions import Annotated # <-- add this
13
+
14
+ # ---- Atomic field types (constrained) ----
15
+ Callsign = Annotated[str, Field(strip_whitespace=True, min_length=3, max_length=20)]
16
+ Band = Literal[
17
+ "160m",
18
+ "80m",
19
+ "60m",
20
+ "40m",
21
+ "30m",
22
+ "20m",
23
+ "17m",
24
+ "15m",
25
+ "12m",
26
+ "10m",
27
+ "6m",
28
+ "4m",
29
+ "2m",
30
+ "70cm",
31
+ "23cm",
32
+ ]
33
+ Mode = Literal[
34
+ "CW",
35
+ "SSB",
36
+ "AM",
37
+ "FM",
38
+ "RTTY",
39
+ "PSK31",
40
+ "FT8",
41
+ "FT4",
42
+ "JT65",
43
+ "JT9",
44
+ "MFSK",
45
+ "OLIVIA",
46
+ "OTHER",
47
+ ]
48
+ RST = Annotated[str, Field(regex=r"^\d{2,3}[A-Z]?$")]
49
+ BoolFlag = Literal["Y", "N"]
50
+ QSLRcvd = Literal["Y", "N", "R", "I", "V", "Q", "E"]
51
+
52
+
53
+ class QsoCore(BaseModel):
54
+ """QSO Base Model that includes:
55
+ station_call `str`
56
+ qso_date `str`
57
+ time_on `str`
58
+ band `str`
59
+ mode `str`
60
+ freq `float`
61
+ rst_sent `int`
62
+ ret_rcvd `int`
63
+ my_gridsquare `str`
64
+ tx_power `float`
65
+ comment 1str`
66
+ """
67
+
68
+ station_call: Callsign
69
+ call: Callsign
70
+ qso_date: Annotated[str, Field(regex=r"^\d{8}$")] # YYYYMMDD
71
+ time_on: Annotated[str, Field(regex=r"^\d{4}(\d{2})?$")] # HHMM[SS]
72
+ band: Band
73
+ mode: Mode
74
+ freq: Optional[Annotated[float, Field(gt=0)]] = None # MHz
75
+ rst_sent: Optional[RST] = None
76
+ rst_rcvd: Optional[RST] = None
77
+ my_gridsquare: Optional[Annotated[str, Field(max_length=8)]] = None
78
+ gridsquare: Optional[Annotated[str, Field(max_length=8)]] = None
79
+ tx_pwr: Optional[Annotated[float, Field(ge=0)]] = None # watts
80
+ comment: Optional[Annotated[str, Field(max_length=200)]] = None
81
+
82
+
83
+ class QslStatus(BaseModel):
84
+ """QSO Status for lotw qsl received and the dates"""
85
+
86
+ lotw_qsl_rcvd: Optional[QSLRcvd] = None
87
+ eqsl_qsl_rcvd: Optional[QSLRcvd] = None
88
+ lotw_qsl_date: Optional[Annotated[str, Field(regex=r"^\d{8}$")]] = None
89
+ eqsl_qsl_date: Optional[Annotated[str, Field(regex=r"^\d{8}$")]] = None
90
+
91
+
92
+ class QsoRecord(QsoCore, QslStatus):
93
+ """Normalized QSO record that merges core ADIF fields with QSL status.
94
+
95
+ `adif_fields` preserves the original name→value map for round-tripping.
96
+ """
97
+
98
+ adif_fields: Dict[str, str] = Field(
99
+ default_factory=dict,
100
+ description="Original ADIF name→value map (optional).",
101
+ )
102
+
103
+
104
+ class ValidateRequest(BaseModel):
105
+ """Batch validation request wrapper."""
106
+
107
+ records: List[Dict[str, str]] # raw ADIF field dicts (pre-model)
108
+ strict: bool = True
109
+
110
+
111
+ class ValidateResult(BaseModel):
112
+ """Batch validation result wrapper."""
113
+
114
+ ok: bool
115
+ errors: List[Dict[str, str]] # {index, field, message}
116
+ normalized: Optional[List[QsoRecord]] = None # present if ok
117
+
118
+
119
+ class EnumList(BaseModel):
120
+ """Enumerations exposed by the MCP (for UI drop-downs / validation)."""
121
+
122
+ modes: List[str]
123
+ bands: List[str]
124
+ qsl_rcvd: List[str]