eme-mcp 0.1.0__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.
- eme_mcp-0.1.0/.github/workflows/ci.yml +92 -0
- eme_mcp-0.1.0/.github/workflows/publish.yml +246 -0
- eme_mcp-0.1.0/.gitignore +8 -0
- eme_mcp-0.1.0/CHANGELOG.md +27 -0
- eme_mcp-0.1.0/LICENSE +674 -0
- eme_mcp-0.1.0/NOTICE +24 -0
- eme_mcp-0.1.0/PKG-INFO +106 -0
- eme_mcp-0.1.0/README.md +79 -0
- eme_mcp-0.1.0/pyproject.toml +80 -0
- eme_mcp-0.1.0/server.json +24 -0
- eme_mcp-0.1.0/src/eme_mcp/__init__.py +14 -0
- eme_mcp-0.1.0/src/eme_mcp/eme.py +122 -0
- eme_mcp-0.1.0/src/eme_mcp/grid.py +79 -0
- eme_mcp-0.1.0/src/eme_mcp/moon.py +227 -0
- eme_mcp-0.1.0/src/eme_mcp/server.py +332 -0
- eme_mcp-0.1.0/tests/test_horizons_oracle.py +103 -0
- eme_mcp-0.1.0/tests/test_security.py +133 -0
- eme_mcp-0.1.0/tests/test_tools.py +159 -0
- eme_mcp-0.1.0/uv.lock +2478 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main, develop]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main, develop]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
11
|
+
cancel-in-progress: true
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
# main takes a release PR from develop, or a security branch, and nothing else:
|
|
18
|
+
# merging into main publishes. Required on main by its branch protection.
|
|
19
|
+
release-source:
|
|
20
|
+
name: Release PR source
|
|
21
|
+
if: github.event_name == 'pull_request' && github.base_ref == 'main'
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
steps:
|
|
24
|
+
- run: |
|
|
25
|
+
case "${{ github.head_ref }}" in
|
|
26
|
+
develop|security/*) echo "ok: ${{ github.head_ref }} -> main" ;;
|
|
27
|
+
*) echo "main takes a release PR from develop, or a security/ branch"; exit 1 ;;
|
|
28
|
+
esac
|
|
29
|
+
|
|
30
|
+
# Lint and types, once rather than per Python: both read the source, and
|
|
31
|
+
# neither answer changes with the interpreter running them (qso-graph-devel#66).
|
|
32
|
+
lint:
|
|
33
|
+
name: ruff and mypy
|
|
34
|
+
runs-on: ubuntu-latest
|
|
35
|
+
steps:
|
|
36
|
+
- uses: actions/checkout@v5
|
|
37
|
+
|
|
38
|
+
- uses: astral-sh/setup-uv@v6
|
|
39
|
+
with:
|
|
40
|
+
python-version: "3.11"
|
|
41
|
+
enable-cache: true
|
|
42
|
+
|
|
43
|
+
- name: Install (locked)
|
|
44
|
+
run: uv sync --group dev --frozen
|
|
45
|
+
|
|
46
|
+
- name: Lint (ruff)
|
|
47
|
+
run: uv run ruff check .
|
|
48
|
+
|
|
49
|
+
- name: Type-check (mypy)
|
|
50
|
+
run: uv run mypy src
|
|
51
|
+
|
|
52
|
+
test:
|
|
53
|
+
name: pytest (${{ matrix.python-version }})
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
strategy:
|
|
56
|
+
fail-fast: false
|
|
57
|
+
matrix:
|
|
58
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
59
|
+
steps:
|
|
60
|
+
- uses: actions/checkout@v5
|
|
61
|
+
|
|
62
|
+
- uses: astral-sh/setup-uv@v6
|
|
63
|
+
with:
|
|
64
|
+
python-version: ${{ matrix.python-version }}
|
|
65
|
+
enable-cache: true
|
|
66
|
+
|
|
67
|
+
- name: Install (locked)
|
|
68
|
+
run: uv sync --group dev --frozen
|
|
69
|
+
|
|
70
|
+
- name: Unit tests
|
|
71
|
+
run: uv run pytest tests --ignore=tests/test_security.py -v
|
|
72
|
+
|
|
73
|
+
- name: Security tests
|
|
74
|
+
run: uv run pytest tests/test_security.py -v
|
|
75
|
+
|
|
76
|
+
ci-all-green:
|
|
77
|
+
name: ci-all-green
|
|
78
|
+
needs: [lint, test]
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
if: always()
|
|
81
|
+
steps:
|
|
82
|
+
- name: Verify required jobs all succeeded
|
|
83
|
+
run: |
|
|
84
|
+
if [[ "${{ needs.lint.result }}" != "success" ]]; then
|
|
85
|
+
echo "FAIL: ruff or mypy did not pass (result=${{ needs.lint.result }})"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
if [[ "${{ needs.test.result }}" != "success" ]]; then
|
|
89
|
+
echo "FAIL: test matrix did not all succeed (result=${{ needs.test.result }})"
|
|
90
|
+
exit 1
|
|
91
|
+
fi
|
|
92
|
+
echo "All required CI jobs passed."
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# The release flow (qso-graph/.github TEMPLATES.md): work lands on `develop`; a release is a
|
|
4
|
+
# PR from `develop` into `main`, and merging it publishes. `main` is always the released code.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [ main ]
|
|
8
|
+
|
|
9
|
+
concurrency:
|
|
10
|
+
group: publish
|
|
11
|
+
cancel-in-progress: false
|
|
12
|
+
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
security:
|
|
18
|
+
name: Security gates
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
outputs:
|
|
21
|
+
version: ${{ steps.version.outputs.version }}
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v4
|
|
24
|
+
|
|
25
|
+
# main changes only by a release PR, so main's version must be new. PyPI publishes
|
|
26
|
+
# pyproject's version and the Registry server.json's, so they must agree too.
|
|
27
|
+
# Computed once; every later job uses this value (needs.security.outputs.version).
|
|
28
|
+
# The gate fails closed: anything it cannot confirm stops the release.
|
|
29
|
+
- name: The version on main is new and consistent
|
|
30
|
+
id: version
|
|
31
|
+
run: |
|
|
32
|
+
VERSION="$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)"
|
|
33
|
+
PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
|
|
34
|
+
echo "pyproject=${VERSION} server.json=$(jq -r '.version' server.json)/$(jq -r '.packages[0].version' server.json)"
|
|
35
|
+
if [ "$(jq -r '.version' server.json)" != "$VERSION" ] || [ "$(jq -r '.packages[0].version' server.json)" != "$VERSION" ]; then
|
|
36
|
+
echo "FAIL: server.json does not match pyproject.toml version ${VERSION}"
|
|
37
|
+
exit 1
|
|
38
|
+
fi
|
|
39
|
+
# Only a 404 means "not published yet"; unreachable or 5xx is not a yes.
|
|
40
|
+
CODE="$(curl -s -o /dev/null -w '%{http_code}' "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json")"
|
|
41
|
+
case "$CODE" in
|
|
42
|
+
404) echo "PyPI: ${PACKAGE} ${VERSION} is new" ;;
|
|
43
|
+
200) echo "FAIL: ${PACKAGE} ${VERSION} is already on PyPI. main changes only by a release PR with a new version."; exit 1 ;;
|
|
44
|
+
*) echo "FAIL: PyPI returned ${CODE}; cannot confirm ${VERSION} is new"; exit 1 ;;
|
|
45
|
+
esac
|
|
46
|
+
# git ls-remote --exit-code: 2 means no such tag; anything else (0, 128) stops.
|
|
47
|
+
set +e
|
|
48
|
+
git ls-remote --exit-code --tags origin "refs/tags/v${VERSION}" > /dev/null
|
|
49
|
+
RC=$?
|
|
50
|
+
set -e
|
|
51
|
+
case "$RC" in
|
|
52
|
+
2) echo "tag v${VERSION} does not exist yet" ;;
|
|
53
|
+
0) echo "FAIL: tag v${VERSION} already exists"; exit 1 ;;
|
|
54
|
+
*) echo "FAIL: could not check tags (git ls-remote exited ${RC})"; exit 1 ;;
|
|
55
|
+
esac
|
|
56
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
- uses: actions/setup-python@v5
|
|
61
|
+
with:
|
|
62
|
+
python-version: "3.12"
|
|
63
|
+
|
|
64
|
+
- name: Install test dependencies
|
|
65
|
+
run: pip install pytest pip-audit
|
|
66
|
+
|
|
67
|
+
- name: Run security test suite
|
|
68
|
+
run: python -m pytest tests/test_security.py -v
|
|
69
|
+
|
|
70
|
+
- name: Audit dependencies for vulnerabilities
|
|
71
|
+
# Audits this package's declared dependencies, and can fail: a known CVE stops the release.
|
|
72
|
+
# A finding with no fix yet is excepted only explicitly: --ignore-vuln ID, with a reason and date.
|
|
73
|
+
run: pip-audit --strict --desc on .
|
|
74
|
+
|
|
75
|
+
- name: Grep check — no subprocess/shell
|
|
76
|
+
run: |
|
|
77
|
+
if grep -rn 'subprocess\.\|os\.system\|shell=True' src/; then
|
|
78
|
+
echo "FAIL: subprocess/shell found in source"
|
|
79
|
+
exit 1
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
- name: Grep check — no plaintext http://
|
|
83
|
+
run: |
|
|
84
|
+
if grep -rn 'http://' src/ | grep -v 'localhost\|127\.0\.0\.1\|::1'; then
|
|
85
|
+
echo "FAIL: non-HTTPS URL found in source"
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
- name: Grep check — no credential logging
|
|
90
|
+
run: |
|
|
91
|
+
if grep -rn -i 'print.*password\|print.*api_key\|logging.*password\|logging.*api_key' src/; then
|
|
92
|
+
echo "FAIL: credential logging found in source"
|
|
93
|
+
exit 1
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
publish:
|
|
97
|
+
name: Build and publish to PyPI
|
|
98
|
+
needs: security
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
environment: pypi
|
|
101
|
+
permissions:
|
|
102
|
+
id-token: write # Required for trusted publishing (OIDC)
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v4
|
|
105
|
+
|
|
106
|
+
- uses: actions/setup-python@v5
|
|
107
|
+
with:
|
|
108
|
+
python-version: "3.12"
|
|
109
|
+
|
|
110
|
+
- name: Install build dependencies
|
|
111
|
+
run: pip install build
|
|
112
|
+
|
|
113
|
+
- name: Build package
|
|
114
|
+
run: python -m build
|
|
115
|
+
|
|
116
|
+
- name: Publish to PyPI
|
|
117
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Registry-publish — pushes server.json to the Official MCP Registry after
|
|
121
|
+
# PyPI publish succeeds, so discovery surfaces stay in sync with PyPI.
|
|
122
|
+
# Canonical template: https://github.com/qso-graph/.github/blob/main/TEMPLATES.md
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
registry-publish:
|
|
125
|
+
name: Publish to MCP Registry
|
|
126
|
+
needs: [security, publish] # waits for PyPI publish to succeed
|
|
127
|
+
runs-on: ubuntu-latest
|
|
128
|
+
permissions:
|
|
129
|
+
id-token: write # GitHub OIDC
|
|
130
|
+
contents: read
|
|
131
|
+
|
|
132
|
+
steps:
|
|
133
|
+
- uses: actions/checkout@v5
|
|
134
|
+
|
|
135
|
+
- name: Install jq (server.json bump)
|
|
136
|
+
run: sudo apt-get update && sudo apt-get install -y jq
|
|
137
|
+
|
|
138
|
+
- name: Install mcp-publisher
|
|
139
|
+
run: |
|
|
140
|
+
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
|
|
141
|
+
./mcp-publisher --help > /dev/null # smoke test
|
|
142
|
+
|
|
143
|
+
- name: Set server.json to the released version
|
|
144
|
+
run: |
|
|
145
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
146
|
+
echo "Setting server.json to ${VERSION}"
|
|
147
|
+
jq --arg v "$VERSION" \
|
|
148
|
+
'.version = $v | .packages[0].version = $v' \
|
|
149
|
+
server.json > /tmp/server.json
|
|
150
|
+
mv /tmp/server.json server.json
|
|
151
|
+
cat server.json
|
|
152
|
+
|
|
153
|
+
# The Registry checks PyPI itself and refuses a version PyPI isn't
|
|
154
|
+
# serving yet, which can lag the upload by a minute or more.
|
|
155
|
+
- name: Wait until PyPI serves this version
|
|
156
|
+
run: |
|
|
157
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
158
|
+
PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
|
|
159
|
+
for i in $(seq 1 40); do
|
|
160
|
+
if curl -fsS "https://pypi.org/pypi/${PACKAGE}/${VERSION}/json" > /dev/null; then
|
|
161
|
+
echo "PyPI serves ${PACKAGE} ${VERSION}"
|
|
162
|
+
exit 0
|
|
163
|
+
fi
|
|
164
|
+
echo "attempt ${i}: PyPI doesn't serve ${PACKAGE} ${VERSION} yet"
|
|
165
|
+
sleep 15
|
|
166
|
+
done
|
|
167
|
+
echo "FAIL: PyPI never served ${PACKAGE} ${VERSION}"
|
|
168
|
+
exit 1
|
|
169
|
+
|
|
170
|
+
- name: Authenticate to MCP Registry (GitHub OIDC)
|
|
171
|
+
run: ./mcp-publisher login github-oidc
|
|
172
|
+
|
|
173
|
+
# Retries ride out the Registry's own transient failures. Before each
|
|
174
|
+
# retry, check whether an earlier attempt landed despite the error.
|
|
175
|
+
- name: Publish to MCP Registry
|
|
176
|
+
run: |
|
|
177
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
178
|
+
NAME="$(jq -r '.name' server.json)"
|
|
179
|
+
REGISTRY="https://registry.modelcontextprotocol.io/v0/servers?search=${NAME}&version=latest"
|
|
180
|
+
for i in 1 2 3 4 5; do
|
|
181
|
+
if ./mcp-publisher publish; then
|
|
182
|
+
exit 0
|
|
183
|
+
fi
|
|
184
|
+
echo "attempt ${i} failed; retrying in $((i * 30)) s"
|
|
185
|
+
sleep $((i * 30))
|
|
186
|
+
REG="$(curl -fsS "$REGISTRY" | jq -r --arg n "$NAME" '[.servers[] | select(.server.name == $n) | .server.version][0] // empty' || true)"
|
|
187
|
+
if [ "$REG" = "$VERSION" ]; then
|
|
188
|
+
echo "The Registry already has ${NAME} ${VERSION}; an earlier attempt landed"
|
|
189
|
+
exit 0
|
|
190
|
+
fi
|
|
191
|
+
done
|
|
192
|
+
echo "FAIL: the MCP Registry refused the publish 5 times"
|
|
193
|
+
exit 1
|
|
194
|
+
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
# Verify — a release is complete only when PyPI AND the MCP Registry both
|
|
197
|
+
# serve the tagged version. If either doesn't within 10 minutes, this run
|
|
198
|
+
# fails: a green run means published everywhere, a red one means not done.
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
verify:
|
|
201
|
+
name: Verify PyPI and MCP Registry
|
|
202
|
+
needs: [security, registry-publish]
|
|
203
|
+
runs-on: ubuntu-latest
|
|
204
|
+
steps:
|
|
205
|
+
- uses: actions/checkout@v5
|
|
206
|
+
|
|
207
|
+
- name: PyPI and Registry serve the released version
|
|
208
|
+
run: |
|
|
209
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
210
|
+
PACKAGE="$(jq -r '.packages[0].identifier' server.json)"
|
|
211
|
+
NAME="$(jq -r '.name' server.json)"
|
|
212
|
+
REGISTRY="https://registry.modelcontextprotocol.io/v0/servers?search=${NAME}&version=latest"
|
|
213
|
+
for i in $(seq 1 40); do
|
|
214
|
+
PYPI="$(curl -fsS "https://pypi.org/pypi/${PACKAGE}/json" | jq -r '.info.version' || true)"
|
|
215
|
+
REG="$(curl -fsS "$REGISTRY" | jq -r --arg n "$NAME" '[.servers[] | select(.server.name == $n) | .server.version][0] // empty' || true)"
|
|
216
|
+
echo "attempt ${i}: PyPI=${PYPI:-none} Registry=${REG:-none} want=${VERSION}"
|
|
217
|
+
if [ "$PYPI" = "$VERSION" ] && [ "$REG" = "$VERSION" ]; then
|
|
218
|
+
echo "Published everywhere: ${PACKAGE} ${VERSION} on PyPI and ${NAME} ${VERSION} in the MCP Registry"
|
|
219
|
+
exit 0
|
|
220
|
+
fi
|
|
221
|
+
sleep 15
|
|
222
|
+
done
|
|
223
|
+
echo "FAIL: release incomplete. PyPI=${PYPI:-none} Registry=${REG:-none}, expected ${VERSION}"
|
|
224
|
+
exit 1
|
|
225
|
+
|
|
226
|
+
# ---------------------------------------------------------------------------
|
|
227
|
+
# Tag the release, once it is published everywhere. The tag marks the commit on
|
|
228
|
+
# main that was released; nothing is triggered by it. If this job fails after a
|
|
229
|
+
# green verify, the release IS published: re-running this job is safe.
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
tag:
|
|
232
|
+
name: Tag the release
|
|
233
|
+
needs: [security, verify]
|
|
234
|
+
runs-on: ubuntu-latest
|
|
235
|
+
permissions:
|
|
236
|
+
contents: write
|
|
237
|
+
steps:
|
|
238
|
+
- uses: actions/checkout@v5
|
|
239
|
+
|
|
240
|
+
- name: Create and push vX.Y.Z
|
|
241
|
+
run: |
|
|
242
|
+
VERSION="${{ needs.security.outputs.version }}"
|
|
243
|
+
git config user.name "github-actions[bot]"
|
|
244
|
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
|
245
|
+
git tag -a "v${VERSION}" -m "${GITHUB_REPOSITORY#*/} ${VERSION}" "$GITHUB_SHA"
|
|
246
|
+
git push origin "v${VERSION}"
|
eme_mcp-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `eme-mcp` are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] — 2026-10-11
|
|
9
|
+
|
|
10
|
+
- First release. EME planning: moon windows for a grid square, mutual windows between two
|
|
11
|
+
stations, path loss on 2m/70cm/23cm, degradation, and the least degraded days ahead
|
|
12
|
+
(qso-graph-devel#52).
|
|
13
|
+
- **The moon is computed locally** from the abridged ELP-2000/82 series as Meeus tabulates it
|
|
14
|
+
(*Astronomical Algorithms*, ch. 47), so the server makes **no network call at run time**.
|
|
15
|
+
KI7MT and Bob settled this on 2026-10-11, against an earlier suggestion of calling JPL
|
|
16
|
+
Horizons: moonrise and moonset are per-location, so a service-backed answer means a request
|
|
17
|
+
per grid per query, and a planning tool that stalls when a remote service does.
|
|
18
|
+
- **JPL Horizons is the test oracle instead.** Recorded Horizons responses are fixtures, so
|
|
19
|
+
every run checks these numbers against NASA's rather than against themselves. Measured for a
|
|
20
|
+
Boise grid: azimuth within 0.01°, elevation within 0.003°, range within 9 km of 392,000.
|
|
21
|
+
- Echo path loss lands on the figures EME literature carries — about 252 dB on 2m, 262 on
|
|
22
|
+
70cm, 271 on 23cm at mean distance — and is returned beside round-trip loss, which ignores
|
|
23
|
+
the moon and is not what a station is budgeted with.
|
|
24
|
+
- **Sky noise is not included yet.** The galactic background behind the moon needs the Haslam
|
|
25
|
+
408 MHz map, which is not yet carried and pinned, so `eme_degradation` and `eme_calendar`
|
|
26
|
+
report the distance term and say so in every answer rather than returning a number that
|
|
27
|
+
looks complete.
|