Repository navigation
Releases
v0.6.2
Fixed
- ADIF is read by
qso_graph_lib.adif, the fleet's one ADI reader, which reads ADI as
ADIF 3.1.7 defines it and passes ADIF's own test files. This server's scanner read a
Header's fields as the first QSO's, soADIF_VERandPROGRAMID(with trailing spaces)
reached tool results in that QSO'sadiffields, and a length that ran past the next tag
returned two QSOs as one. New dependency:qso-graph-lib>=0.2.0(standard library only).
v0.6.1
Changed
-
Requests to eQSL.cc are now spaced. Where a provider states a request
limit this server honours that limit; eQSL.cc states none, so the gap is a
recorded judgement rather than a published figure — 200 ms, matching what
sota-mcpandiota-mcpalready use, with the reasoning written beside the
constant so it can be revisited rather than rediscovered. A volunteer or public
service with no stated limit still deserves restraint (KI7MT, 2026-10-11).This is not throttling the user. It limits what this server sends to somebody
else's service; it does not limit the person asking. The response cache does the
larger share of the work by removing requests rather than delaying them, and the
spacing covers what a cache cannot: a model calling tools in a loop over inputs
that each miss. -
This server also sent no
User-Agentat all, so eQSL could not tell who
was calling or ask us to ease off. It now identifies itself by name and version.
v0.6.0
Added
- A
429from eQSL is now honoured. eQSL asking this client to stop is the
most explicit form a request limit takes, and it was previously invisible:
every failure became "eQSL request failed — check network and credentials", so
a rate limit read as a wrong password. Now theRetry-Afterinterval is read
and bounded to between a minute and an hour, no further request goes out until
it has passed, and the tool says how long to wait rather than sleeping through
it. The AG-list download shares that block — the limit is eQSL's view of this
client, not of one URL.
v0.5.0
Changed
-
Python 3.11 is the minimum. 3.10 reached end of life in October 2026
(PEP 619), so it gets no further security
releases on any platform. On Windows it was worse than unsupported:uv's 3.10
is built against OpenSSL 1.1.1w, end of life since September 2023, so HTTPS
from this server ran over an unmaintained TLS library.certifidoes not help
with that — it supplies the root store, not the implementation.3.10 support was already nominal. This package has linted with
ruff target-version = "py311"and type-checked withmypy python_version = "3.11"for some time, so nothing ever verified the 3.10 claim.uv.lockis regenerated and the diff is large, but nothing is upgraded —
it is the 3.10 resolution branch disappearing. Packages that carried two pins,
one per Python branch, now carry the single 3.11+ pin they already resolved to.
An install on 3.11 or later is unchanged.
v0.4.3
Changed
--transport streamable-httpnow says where it bound, and the help text says
it too. It binds127.0.0.1, and FastMCP takes no host option, so it was
never reachable from another machine or from outside a container — and nothing
said so. The bind address is unchanged: loopback-only is deliberate for a
server that holds credentials and has no authentication of its own.
v0.4.2
Fixed
--versionand--helpnow print and exit. They were ignored, so the
command started the MCP server on stdio and waited for JSON-RPC — leaving a
FastMCP banner on screen and an apparently hung terminal. Seven servers in
this fleet already behaved correctly; this brings the rest in line, and a
test asserts it.
v0.4.1
- A release should appear on the Releases page
- Move every Node 20 action to its first Node 24 major
- The locked-dependency audit blocked every release
- Four upgrades arrived as 57 pull requests
- Nothing was auditing the lockfile
- Close every open security advisory in the lockfile
- A release is a workflow, not a merge
- Make the publish gate's http:// check portable
- Assert that certifi is declared, not merely imported
v0.4.0
Upgrading: a credential failure now returns isError: true instead of a successful result whose
payload contained an error key. Anything reading result["error"] should read the error flag.
Other failures — a network error, eQSL refusing — already reached a client as errors and are
unchanged.
Security-relevant: eQSL's own response body was returned as the error text, and this server's
requests put the password in the query string. A body echoing the request used to carry the
password into the message. That text is now withheld, with a reason, when it contains the secret or
any URL.
-
E501is adopted (#22, qso-graph-devel#70). It reported nothing — this source was
already within 95 columns, so adoption was the config change alone with no reflow, and the repo
carries nonoqa. That the rule is enforced rather than inert was checked rather than assumed: a
planted 117-character line is reported, and removed again the tree is clean. -
certifiis carried, and both fetch paths verify against it (#21, qso-graph-devel#73). They
usedurllibwith no context, so they trusted whichever root store the running Python was built
against. On macOS that is/etc/ssl/cert.pem, a deliberately reduced OpenSSL file — 128 roots
against the system keychain's 158, and current rather than stale. eqsl.cc verifies fine against
it today, so this was latent rather than an outage. -
It matters most on the credentialed path: eQSL's query interface puts the operator's password
in the query string, sossl.CERT_NONEthere would mean handing the password to whoever
presented a certificate. That is why verification is never disabled, rather than merely why it
should not be. -
The context lives in its own module,
_tls.py, because the calls are in two files —client.py
andag_cache.py— and the "everyurlopenverifies" check scans the whole package. A check
reading one file would have left the other unverified, and which one is worse depends only on
which file it happened to read. -
tests/test_release_gate.pyasserts the gate suite imports nothing but the standard library
and pytest. solar-mcp's 0.3.0 publish failed on exactly that, from the same change. -
Credential failures are flagged
isError(#23). Three tools caughtCredentialErrorand
returned{"error": "..."}, which reaches a client as a successful call whose payload happens
to contain the word "error". They now raiseToolError. No decision changed — only how it is
reported. -
Measured while doing it: this server was already half-right, by accident. Its tools caught
onlyCredentialError, so every other failure — a network error, eQSL refusing — propagated
uncaught, andfastmcpalready reported those asisErrorwith the message intact. That
behaviour was being relied on with nothing asserting it, so a later tidy-up adding a blanket
except Exception: return {...}would have silently undone it. It is now pinned by a test, which
also refuses any{"error": ...}return creeping back intoserver.py. -
Fixed: eQSL's own response body could carry the password into the message.
last_upload_date
stripped the tags off whatever eQSL returned and handed that back, and this server's requests put
the password in the query string. Measured, not assumed: given a body echoing the request the
secret came straight through. That text is now withheld, with a reason, when it carries the
secret or anything URL-shaped, and capped — while an ordinaryInvalid user namestill reaches
the caller. Same shape as the fix inlotw-mcp. -
Every tool declares all four annotation hints (#23). All six are read-only, non-destructive
and idempotent;openWorldHintis true for the five that reach eqsl.cc — three with the
operator's credentials, two on public endpoints — and false forget_version_info. -
tests/test_mcp_conformance.pyasserts all of the above.
v0.3.8
- ruff and mypy run in CI (qso-graph-devel#66), as a job the
ci-all-greengate requires.
Settings followadif-mcp, the reference for every qso-graph Python repo, rather than a style of
this repo's own. They run once rather than per Python version: both read the source, and neither
answer changes with the interpreter. - Five tool functions were annotated
-> dictwith no type arguments, so nothing checked what
they return. They saydict[str, Any]now. E501is deferred rather than adopted (qso-graph-devel#70): what it reports in these repos are
widths, not defects, and some lines are long because they name a publisher's field exactly.mcp.runis given the literal fastmcp asks for rather than astrthat happens to hold the
right word.fastmcpis bounded:>=4.0,<5(qso-graph-devel#60). It was>=3.0with no upper bound, and
these servers are run withuvx, which resolves fresh — so afastmcp5.0 would have reached
every user automatically, before anything here had been run against it. The floor rises to 4.0
because that is what is actually tested: every lock in the fleet held a 4.x, and nothing in CI
has ever exercised 3.x. A claim of 3.x support that no test backs is not support.fastmcpis locked at 4.1.0, the current release, so CI runs against what a new install gets.- The published contact is
maintainers@qso-graph.io(qso-graph-devel#69). Theauthorsfield
carried a personal address, and that field is what PyPI shows on the package page. Everything in
qso-graph is open source and open to contribution, so the contact is the project's.