netlogger-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.
- netlogger_mcp-0.1.0/.github/workflows/ci.yml +65 -0
- netlogger_mcp-0.1.0/.github/workflows/publish.yml +70 -0
- netlogger_mcp-0.1.0/.gitignore +7 -0
- netlogger_mcp-0.1.0/CHANGELOG.md +17 -0
- netlogger_mcp-0.1.0/LICENSE +22 -0
- netlogger_mcp-0.1.0/PKG-INFO +159 -0
- netlogger_mcp-0.1.0/README.md +130 -0
- netlogger_mcp-0.1.0/pyproject.toml +46 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/__init__.py +21 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/contract.py +54 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/limiter.py +282 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/netlogger.py +448 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/paths.py +26 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/samples/GetActiveNets.xml +47 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/samples/GetCheckins.xml +55 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/samples/GetPastNetCheckins.xml +29 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/samples/GetPastNets.xml +36 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/samples/__init__.py +1 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/schema/contract.schema.json +92 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/server.py +220 -0
- netlogger_mcp-0.1.0/src/netlogger_mcp/settings.py +43 -0
- netlogger_mcp-0.1.0/tests/conftest.py +21 -0
- netlogger_mcp-0.1.0/tests/test_netlogger.py +444 -0
- netlogger_mcp-0.1.0/tests/test_security.py +104 -0
- netlogger_mcp-0.1.0/tests/test_shared_limits.py +233 -0
- netlogger_mcp-0.1.0/tests/test_tools.py +142 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
branches: [main]
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
# Cancel superseded runs on the same ref (push or PR) so we don't waste
|
|
10
|
+
# minutes when commits land in rapid succession.
|
|
11
|
+
concurrency:
|
|
12
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
13
|
+
cancel-in-progress: true
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
test:
|
|
17
|
+
name: pytest (${{ matrix.os }}, ${{ matrix.python-version }})
|
|
18
|
+
runs-on: ${{ matrix.os }}
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: false
|
|
21
|
+
matrix:
|
|
22
|
+
# Most hams run Windows, and the shared call budget uses a different
|
|
23
|
+
# file lock there (msvcrt) than on Linux and macOS (flock).
|
|
24
|
+
os: [ubuntu-latest]
|
|
25
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
26
|
+
include:
|
|
27
|
+
- os: windows-latest
|
|
28
|
+
python-version: "3.10"
|
|
29
|
+
- os: windows-latest
|
|
30
|
+
python-version: "3.13"
|
|
31
|
+
- os: macos-latest
|
|
32
|
+
python-version: "3.13"
|
|
33
|
+
steps:
|
|
34
|
+
- uses: actions/checkout@v4
|
|
35
|
+
|
|
36
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
37
|
+
uses: actions/setup-python@v5
|
|
38
|
+
with:
|
|
39
|
+
python-version: ${{ matrix.python-version }}
|
|
40
|
+
|
|
41
|
+
- name: Install package + test dependencies
|
|
42
|
+
run: |
|
|
43
|
+
python -m pip install --upgrade pip
|
|
44
|
+
pip install -e .
|
|
45
|
+
pip install -e ".[test]"
|
|
46
|
+
|
|
47
|
+
- name: Run unit tests (L2)
|
|
48
|
+
run: python -m pytest tests --ignore=tests/test_security.py -v
|
|
49
|
+
|
|
50
|
+
- name: Run security tests
|
|
51
|
+
run: python -m pytest tests/test_security.py -v
|
|
52
|
+
|
|
53
|
+
ci-all-green:
|
|
54
|
+
name: ci-all-green
|
|
55
|
+
needs: [test]
|
|
56
|
+
runs-on: ubuntu-latest
|
|
57
|
+
if: always()
|
|
58
|
+
steps:
|
|
59
|
+
- name: Verify required jobs all succeeded
|
|
60
|
+
run: |
|
|
61
|
+
if [[ "${{ needs.test.result }}" != "success" ]]; then
|
|
62
|
+
echo "FAIL: test matrix did not all succeed (result=${{ needs.test.result }})"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
echo "All required CI jobs passed."
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
security:
|
|
10
|
+
name: Security gates
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- uses: actions/setup-python@v5
|
|
16
|
+
with:
|
|
17
|
+
python-version: "3.12"
|
|
18
|
+
|
|
19
|
+
- name: Install test dependencies
|
|
20
|
+
run: pip install -e ".[test]" pip-audit
|
|
21
|
+
|
|
22
|
+
- name: Run security test suite
|
|
23
|
+
run: python -m pytest tests/test_security.py -v
|
|
24
|
+
|
|
25
|
+
- name: Audit dependencies for vulnerabilities
|
|
26
|
+
run: pip-audit --strict --desc 2>&1 || true
|
|
27
|
+
|
|
28
|
+
- name: Grep check — no subprocess/shell
|
|
29
|
+
run: |
|
|
30
|
+
if grep -rn 'subprocess\.\|os\.system\|shell=True' src/; then
|
|
31
|
+
echo "FAIL: subprocess/shell found in source"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
- name: Grep check — no plaintext http://
|
|
36
|
+
run: |
|
|
37
|
+
if grep -rn 'http://' src/ | grep -v 'localhost\|127\.0\.0\.1\|::1'; then
|
|
38
|
+
echo "FAIL: non-HTTPS URL found in source"
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
- name: Grep check — no credential logging
|
|
43
|
+
run: |
|
|
44
|
+
if grep -rn -i 'print.*password\|print.*api_key\|logging.*password\|logging.*api_key' src/; then
|
|
45
|
+
echo "FAIL: credential logging found in source"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
publish:
|
|
50
|
+
name: Build and publish to PyPI
|
|
51
|
+
needs: security
|
|
52
|
+
runs-on: ubuntu-latest
|
|
53
|
+
environment: pypi
|
|
54
|
+
permissions:
|
|
55
|
+
id-token: write # Required for trusted publishing (OIDC)
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v4
|
|
58
|
+
|
|
59
|
+
- uses: actions/setup-python@v5
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.12"
|
|
62
|
+
|
|
63
|
+
- name: Install build dependencies
|
|
64
|
+
run: pip install build
|
|
65
|
+
|
|
66
|
+
- name: Build package
|
|
67
|
+
run: python -m build
|
|
68
|
+
|
|
69
|
+
- name: Publish to PyPI
|
|
70
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-09-28)
|
|
4
|
+
|
|
5
|
+
- Tools for every documented NetLogger API 1.3 call: active nets, live check-ins with the pointer,
|
|
6
|
+
past nets, past check-ins; plus `get_version_info`.
|
|
7
|
+
- Records follow a source-independent contract (`schema/contract.schema.json`, 0.1).
|
|
8
|
+
- NetLogger's call limits enforced before sending; answers cached; a 429 on any call stops all
|
|
9
|
+
calls for the back-off.
|
|
10
|
+
- Every request names the station using it (callsign in the User-Agent); no anonymous mode. The MCP
|
|
11
|
+
asks once on first use and saves it (`netlogger_set_callsign`); the library requires it.
|
|
12
|
+
- Apps built on the library name themselves with ADIF's PROGRAMID and PROGRAMVERSION, which lead
|
|
13
|
+
the User-Agent (`OM-Logger/0.3 netlogger-mcp/0.1.0 (KI7MT; +…)`).
|
|
14
|
+
- The call budget is shared by every copy for the user account (a locked state file), so two AI apps
|
|
15
|
+
can't double the calls. It never fails open, and a copy that fell back rejoins once the file works.
|
|
16
|
+
- Street, ZIP and IP address never returned.
|
|
17
|
+
- `NetLoggerSource` usable as a plain Python library.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
GNU GENERAL PUBLIC LICENSE
|
|
2
|
+
Version 3, 29 June 2007
|
|
3
|
+
|
|
4
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
5
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
6
|
+
of this license document, but changing it is not allowed.
|
|
7
|
+
|
|
8
|
+
Preamble
|
|
9
|
+
|
|
10
|
+
The GNU General Public License is a free, copyleft license for
|
|
11
|
+
software and other kinds of works.
|
|
12
|
+
|
|
13
|
+
The licenses for most software and other practical works are designed
|
|
14
|
+
to take away your freedom to share and change the works. By contrast,
|
|
15
|
+
the GNU General Public License is intended to guarantee your freedom to
|
|
16
|
+
share and change all versions of a program--to make sure it remains free
|
|
17
|
+
software for all its users. We, the Free Software Foundation, use the
|
|
18
|
+
GNU General Public License for most of our software; it applies also to
|
|
19
|
+
any other work released this way by its authors. You can apply it to
|
|
20
|
+
your programs, too.
|
|
21
|
+
|
|
22
|
+
For the full license text, see <https://www.gnu.org/licenses/gpl-3.0.html>
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: netlogger-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for net logging: live and past nets and check-ins, rate-limited to NetLogger's guidelines
|
|
5
|
+
Project-URL: Homepage, https://qso-graph.io
|
|
6
|
+
Project-URL: Repository, https://github.com/qso-graph/netlogger-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/qso-graph/netlogger-mcp/issues
|
|
8
|
+
Author-email: "Greg Beam, KI7MT" <ki7mt@yahoo.com>
|
|
9
|
+
License: GPL-3.0-or-later
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: amateur-radio,check-ins,ham-radio,mcp,model-context-protocol,net-control,netlogger,omiss
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: defusedxml>=0.7
|
|
24
|
+
Requires-Dist: fastmcp>=3.0
|
|
25
|
+
Provides-Extra: test
|
|
26
|
+
Requires-Dist: jsonschema; extra == 'test'
|
|
27
|
+
Requires-Dist: pytest; extra == 'test'
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# netlogger-mcp
|
|
31
|
+
|
|
32
|
+
MCP server and Python library for amateur-radio net logging: which nets are on the air, who has
|
|
33
|
+
checked in, who's up now, and what past nets logged.
|
|
34
|
+
|
|
35
|
+
- **Read-only.** It never writes to a net.
|
|
36
|
+
- **One contract, more than one source.** Every answer uses the same net and check-in records
|
|
37
|
+
([`schema/contract.schema.json`](src/netlogger_mcp/schema/contract.schema.json)), whatever logging
|
|
38
|
+
system is behind them. The first source is [NetLogger](https://www.netlogger.org)'s public XML Data
|
|
39
|
+
Service (API 1.3). The next is the OM-Logger being built for OMISS.
|
|
40
|
+
- **A good neighbour.** NetLogger is a donation-funded service. This server never exceeds
|
|
41
|
+
NetLogger's published call limits, caches every answer, and backs off when told to.
|
|
42
|
+
- **Private details stay out.** Street addresses, ZIP codes and IP addresses in NetLogger's data
|
|
43
|
+
have no place in the contract, so they never reach an AI, a program or a user.
|
|
44
|
+
|
|
45
|
+
Status: 0.1.0, first release.
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
| Tool | NetLogger call | Returns |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `netlogger_active_nets` | `GetActiveNets` | nets on the air: server, name, frequency, band, mode, net control, logger, opened, monitoring count |
|
|
52
|
+
| `netlogger_checkins` | `GetCheckins` | a live net's check-ins, the count, and the **pointer** (the station up now) |
|
|
53
|
+
| `netlogger_past_nets` | `GetPastNets` | closed nets over the last N days, with the net IDs past check-ins need |
|
|
54
|
+
| `netlogger_past_checkins` | `GetPastNetCheckins` | a closed net's check-ins |
|
|
55
|
+
| `get_version_info` | none | server version, NetLogger API version, contract version |
|
|
56
|
+
|
|
57
|
+
These are all the calls NetLogger's API 1.3 documents. `GetPointer` is deprecated; the pointer comes
|
|
58
|
+
with `GetCheckins`, so it's never called.
|
|
59
|
+
|
|
60
|
+
## Call limits
|
|
61
|
+
|
|
62
|
+
| Call | NetLogger's limit | Answers reused for |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| `GetActiveNets` | 1 a minute | 60 s; the name filter is applied locally, so any number of filters cost one call |
|
|
65
|
+
| `GetCheckins` | 3 a minute | 20 s per net |
|
|
66
|
+
| `GetPastNets` | 1 a minute | 60 s per query |
|
|
67
|
+
| `GetPastNetCheckins` | 10 a minute | an hour (a closed net's list doesn't change) |
|
|
68
|
+
|
|
69
|
+
A limit is checked before a request is sent, never after. Over the limit, the answer comes from
|
|
70
|
+
cache with its age (`age_seconds`, `stale`), or the result says when to try again. A
|
|
71
|
+
`429 Too Many Requests` on any call stops **all** calls to NetLogger for at least a minute, longer
|
|
72
|
+
if NetLogger's `Retry-After` asks. NetLogger's anti-flooding is aimed at the client, and every call
|
|
73
|
+
reaches the same server. Past nets older than 7 days need a name filter (NetLogger's rule).
|
|
74
|
+
|
|
75
|
+
**One budget per user account.** Every request carries your callsign, so to NetLogger all your
|
|
76
|
+
copies are one station: Claude Desktop and Claude Code each running the server, a script using the
|
|
77
|
+
library, and so on. They share one budget through a small state file (`limits.json`, beside the
|
|
78
|
+
settings file), updated under a lock the operating system enforces between processes. Another
|
|
79
|
+
account on the same computer has its own folder, and its own callsign. The rules:
|
|
80
|
+
- **A 429 seen by any copy stops them all.**
|
|
81
|
+
- **It never fails open.** If the file is unreadable, it assumes the whole budget was spent and
|
|
82
|
+
waits a full minute. If the file can't be used at all, that copy keeps to the limits on its own,
|
|
83
|
+
starting with a minute's pause, and logs why.
|
|
84
|
+
- **One glitch doesn't strand a copy.** A copy on its own tries the file again after a minute. Once
|
|
85
|
+
the file works, it rejoins the shared budget and carries back the calls it made on its own.
|
|
86
|
+
- **Clock changes don't help.** If the clock goes back, recorded calls count as "now", so they stay
|
|
87
|
+
in the window longer, not shorter.
|
|
88
|
+
|
|
89
|
+
## Install
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
pip install netlogger-mcp
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Claude Code / Claude Desktop:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
"netlogger": { "command": "netlogger-mcp" }
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
No API key or password is needed. **Your callsign is.**
|
|
102
|
+
|
|
103
|
+
## Your callsign
|
|
104
|
+
|
|
105
|
+
Every request tells NetLogger which station is asking, in the User-Agent:
|
|
106
|
+
|
|
107
|
+
```
|
|
108
|
+
netlogger-mcp/0.1.0 (KI7MT; +https://github.com/qso-graph/netlogger-mcp)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
That way, NetLogger can tell users apart. Without it, every install would look like one client, and
|
|
112
|
+
one misbehaving install could get everyone blocked. There is no anonymous mode.
|
|
113
|
+
|
|
114
|
+
- **Nothing to configure.** On first use, the server says it needs your callsign, the AI asks you,
|
|
115
|
+
and it's saved (`netlogger_set_callsign`). You're asked once.
|
|
116
|
+
- **Saved** in a small settings file: `~/.config/netlogger-mcp/settings.json` on Linux,
|
|
117
|
+
`~/Library/Application Support/netlogger-mcp/` on macOS, `%APPDATA%\netlogger-mcp\` on Windows.
|
|
118
|
+
A callsign is public, not a password.
|
|
119
|
+
- **Or set it** with `NETLOGGER_MCP_CALLSIGN=KI7MT`, which overrides the file.
|
|
120
|
+
- Changing the callsign never resets the call limits.
|
|
121
|
+
|
|
122
|
+
For testing without the network, set `NETLOGGER_MCP_MOCK=1` to answer from bundled synthetic samples.
|
|
123
|
+
|
|
124
|
+
## As a library
|
|
125
|
+
|
|
126
|
+
Programs that don't need an AI use the same code directly, with the same limits, cache and contract.
|
|
127
|
+
A library can't ask anyone anything, so it requires the callsign: the program passes in the signed-in
|
|
128
|
+
user's callsign, or the club's for a shared server.
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from netlogger_mcp.netlogger import NetLoggerSource
|
|
132
|
+
|
|
133
|
+
# callsign: required (no valid callsign: NetLoggerError, nothing sent).
|
|
134
|
+
# program_id / program_version: your app, as in ADIF's PROGRAMID and PROGRAMVERSION (optional).
|
|
135
|
+
nl = NetLoggerSource(callsign="KI7MT", program_id="OM-Logger", program_version="0.3")
|
|
136
|
+
# User-Agent: OM-Logger/0.3 netlogger-mcp/0.1.0 (KI7MT; +https://github.com/qso-graph/netlogger-mcp)
|
|
137
|
+
for net in nl.active_nets(name_like="OMISS")["nets"]:
|
|
138
|
+
live = nl.checkins(net["server"], net["name"])
|
|
139
|
+
print(net["name"], "up now:", live["pointer"])
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Programs in other languages can run the server and call its tools over MCP (JSON-RPC on stdio or
|
|
143
|
+
HTTP).
|
|
144
|
+
|
|
145
|
+
## Terms and privacy
|
|
146
|
+
|
|
147
|
+
NetLogger's terms allow API use "in direct support of Radio Communications". This server is for that.
|
|
148
|
+
It sends a User-Agent naming this project and the station using it. Parsing follows the spec: no assumptions about node order
|
|
149
|
+
or count, unknown elements ignored, `<Warning>` messages logged for the developer. XML is parsed with
|
|
150
|
+
`defusedxml`.
|
|
151
|
+
|
|
152
|
+
## Development
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
pip install -e ".[test]"
|
|
156
|
+
pytest
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Part of [qso-graph](https://github.com/qso-graph). Licensed GPL-3.0-or-later.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# netlogger-mcp
|
|
2
|
+
|
|
3
|
+
MCP server and Python library for amateur-radio net logging: which nets are on the air, who has
|
|
4
|
+
checked in, who's up now, and what past nets logged.
|
|
5
|
+
|
|
6
|
+
- **Read-only.** It never writes to a net.
|
|
7
|
+
- **One contract, more than one source.** Every answer uses the same net and check-in records
|
|
8
|
+
([`schema/contract.schema.json`](src/netlogger_mcp/schema/contract.schema.json)), whatever logging
|
|
9
|
+
system is behind them. The first source is [NetLogger](https://www.netlogger.org)'s public XML Data
|
|
10
|
+
Service (API 1.3). The next is the OM-Logger being built for OMISS.
|
|
11
|
+
- **A good neighbour.** NetLogger is a donation-funded service. This server never exceeds
|
|
12
|
+
NetLogger's published call limits, caches every answer, and backs off when told to.
|
|
13
|
+
- **Private details stay out.** Street addresses, ZIP codes and IP addresses in NetLogger's data
|
|
14
|
+
have no place in the contract, so they never reach an AI, a program or a user.
|
|
15
|
+
|
|
16
|
+
Status: 0.1.0, first release.
|
|
17
|
+
|
|
18
|
+
## Tools
|
|
19
|
+
|
|
20
|
+
| Tool | NetLogger call | Returns |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `netlogger_active_nets` | `GetActiveNets` | nets on the air: server, name, frequency, band, mode, net control, logger, opened, monitoring count |
|
|
23
|
+
| `netlogger_checkins` | `GetCheckins` | a live net's check-ins, the count, and the **pointer** (the station up now) |
|
|
24
|
+
| `netlogger_past_nets` | `GetPastNets` | closed nets over the last N days, with the net IDs past check-ins need |
|
|
25
|
+
| `netlogger_past_checkins` | `GetPastNetCheckins` | a closed net's check-ins |
|
|
26
|
+
| `get_version_info` | none | server version, NetLogger API version, contract version |
|
|
27
|
+
|
|
28
|
+
These are all the calls NetLogger's API 1.3 documents. `GetPointer` is deprecated; the pointer comes
|
|
29
|
+
with `GetCheckins`, so it's never called.
|
|
30
|
+
|
|
31
|
+
## Call limits
|
|
32
|
+
|
|
33
|
+
| Call | NetLogger's limit | Answers reused for |
|
|
34
|
+
|---|---|---|
|
|
35
|
+
| `GetActiveNets` | 1 a minute | 60 s; the name filter is applied locally, so any number of filters cost one call |
|
|
36
|
+
| `GetCheckins` | 3 a minute | 20 s per net |
|
|
37
|
+
| `GetPastNets` | 1 a minute | 60 s per query |
|
|
38
|
+
| `GetPastNetCheckins` | 10 a minute | an hour (a closed net's list doesn't change) |
|
|
39
|
+
|
|
40
|
+
A limit is checked before a request is sent, never after. Over the limit, the answer comes from
|
|
41
|
+
cache with its age (`age_seconds`, `stale`), or the result says when to try again. A
|
|
42
|
+
`429 Too Many Requests` on any call stops **all** calls to NetLogger for at least a minute, longer
|
|
43
|
+
if NetLogger's `Retry-After` asks. NetLogger's anti-flooding is aimed at the client, and every call
|
|
44
|
+
reaches the same server. Past nets older than 7 days need a name filter (NetLogger's rule).
|
|
45
|
+
|
|
46
|
+
**One budget per user account.** Every request carries your callsign, so to NetLogger all your
|
|
47
|
+
copies are one station: Claude Desktop and Claude Code each running the server, a script using the
|
|
48
|
+
library, and so on. They share one budget through a small state file (`limits.json`, beside the
|
|
49
|
+
settings file), updated under a lock the operating system enforces between processes. Another
|
|
50
|
+
account on the same computer has its own folder, and its own callsign. The rules:
|
|
51
|
+
- **A 429 seen by any copy stops them all.**
|
|
52
|
+
- **It never fails open.** If the file is unreadable, it assumes the whole budget was spent and
|
|
53
|
+
waits a full minute. If the file can't be used at all, that copy keeps to the limits on its own,
|
|
54
|
+
starting with a minute's pause, and logs why.
|
|
55
|
+
- **One glitch doesn't strand a copy.** A copy on its own tries the file again after a minute. Once
|
|
56
|
+
the file works, it rejoins the shared budget and carries back the calls it made on its own.
|
|
57
|
+
- **Clock changes don't help.** If the clock goes back, recorded calls count as "now", so they stay
|
|
58
|
+
in the window longer, not shorter.
|
|
59
|
+
|
|
60
|
+
## Install
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install netlogger-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Claude Code / Claude Desktop:
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
"netlogger": { "command": "netlogger-mcp" }
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
No API key or password is needed. **Your callsign is.**
|
|
73
|
+
|
|
74
|
+
## Your callsign
|
|
75
|
+
|
|
76
|
+
Every request tells NetLogger which station is asking, in the User-Agent:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
netlogger-mcp/0.1.0 (KI7MT; +https://github.com/qso-graph/netlogger-mcp)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That way, NetLogger can tell users apart. Without it, every install would look like one client, and
|
|
83
|
+
one misbehaving install could get everyone blocked. There is no anonymous mode.
|
|
84
|
+
|
|
85
|
+
- **Nothing to configure.** On first use, the server says it needs your callsign, the AI asks you,
|
|
86
|
+
and it's saved (`netlogger_set_callsign`). You're asked once.
|
|
87
|
+
- **Saved** in a small settings file: `~/.config/netlogger-mcp/settings.json` on Linux,
|
|
88
|
+
`~/Library/Application Support/netlogger-mcp/` on macOS, `%APPDATA%\netlogger-mcp\` on Windows.
|
|
89
|
+
A callsign is public, not a password.
|
|
90
|
+
- **Or set it** with `NETLOGGER_MCP_CALLSIGN=KI7MT`, which overrides the file.
|
|
91
|
+
- Changing the callsign never resets the call limits.
|
|
92
|
+
|
|
93
|
+
For testing without the network, set `NETLOGGER_MCP_MOCK=1` to answer from bundled synthetic samples.
|
|
94
|
+
|
|
95
|
+
## As a library
|
|
96
|
+
|
|
97
|
+
Programs that don't need an AI use the same code directly, with the same limits, cache and contract.
|
|
98
|
+
A library can't ask anyone anything, so it requires the callsign: the program passes in the signed-in
|
|
99
|
+
user's callsign, or the club's for a shared server.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from netlogger_mcp.netlogger import NetLoggerSource
|
|
103
|
+
|
|
104
|
+
# callsign: required (no valid callsign: NetLoggerError, nothing sent).
|
|
105
|
+
# program_id / program_version: your app, as in ADIF's PROGRAMID and PROGRAMVERSION (optional).
|
|
106
|
+
nl = NetLoggerSource(callsign="KI7MT", program_id="OM-Logger", program_version="0.3")
|
|
107
|
+
# User-Agent: OM-Logger/0.3 netlogger-mcp/0.1.0 (KI7MT; +https://github.com/qso-graph/netlogger-mcp)
|
|
108
|
+
for net in nl.active_nets(name_like="OMISS")["nets"]:
|
|
109
|
+
live = nl.checkins(net["server"], net["name"])
|
|
110
|
+
print(net["name"], "up now:", live["pointer"])
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Programs in other languages can run the server and call its tools over MCP (JSON-RPC on stdio or
|
|
114
|
+
HTTP).
|
|
115
|
+
|
|
116
|
+
## Terms and privacy
|
|
117
|
+
|
|
118
|
+
NetLogger's terms allow API use "in direct support of Radio Communications". This server is for that.
|
|
119
|
+
It sends a User-Agent naming this project and the station using it. Parsing follows the spec: no assumptions about node order
|
|
120
|
+
or count, unknown elements ignored, `<Warning>` messages logged for the developer. XML is parsed with
|
|
121
|
+
`defusedxml`.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
pip install -e ".[test]"
|
|
127
|
+
pytest
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Part of [qso-graph](https://github.com/qso-graph). Licensed GPL-3.0-or-later.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "netlogger-mcp"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "MCP server for net logging: live and past nets and check-ins, rate-limited to NetLogger's guidelines"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = {text = "GPL-3.0-or-later"}
|
|
7
|
+
authors = [{name = "Greg Beam, KI7MT", email = "ki7mt@yahoo.com"}]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = [
|
|
10
|
+
"fastmcp>=3.0",
|
|
11
|
+
"defusedxml>=0.7",
|
|
12
|
+
]
|
|
13
|
+
keywords = [
|
|
14
|
+
"amateur-radio", "ham-radio", "netlogger", "net-control", "check-ins",
|
|
15
|
+
"mcp", "model-context-protocol", "omiss",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Communications :: Ham Radio",
|
|
21
|
+
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
test = ["pytest", "jsonschema"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://qso-graph.io"
|
|
35
|
+
Repository = "https://github.com/qso-graph/netlogger-mcp"
|
|
36
|
+
Issues = "https://github.com/qso-graph/netlogger-mcp/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
netlogger-mcp = "netlogger_mcp.server:main"
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/netlogger_mcp"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""MCP server for net logging: live and past nets and check-ins."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
6
|
+
from typing import Final
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
_pkg_version = version("netlogger-mcp")
|
|
10
|
+
except PackageNotFoundError: # local dev / editable installs without dist metadata
|
|
11
|
+
_pkg_version = "0.0.0-dev"
|
|
12
|
+
|
|
13
|
+
__version__: Final[str] = _pkg_version
|
|
14
|
+
|
|
15
|
+
# The NetLogger API this server is built against: "The NetLogger XML Data
|
|
16
|
+
# Service Interface Specification" v1.3 (K0JDD, 2023-12-01).
|
|
17
|
+
__spec_version__: Final[str] = "netlogger-xml-api-1.3"
|
|
18
|
+
|
|
19
|
+
# Version of the records the tools return (schema/contract.schema.json).
|
|
20
|
+
# Every source (NetLogger now, the OM-Logger next) maps onto this.
|
|
21
|
+
__contract_version__: Final[str] = "0.1"
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""The records every tool returns, whatever the source.
|
|
2
|
+
|
|
3
|
+
Each source maps its own format onto these fields and nothing else, so a
|
|
4
|
+
field a source sends that isn't listed here (a street address, a ZIP code, an
|
|
5
|
+
IP address, or anything added later) can never reach a user.
|
|
6
|
+
schema/contract.schema.json is the published form.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
# contract field -> NetLogger element(s), first present wins
|
|
12
|
+
NET_FIELDS: dict[str, tuple[str, ...]] = {
|
|
13
|
+
"name": ("NetName",),
|
|
14
|
+
"current_name": ("AltNetName",),
|
|
15
|
+
"frequency": ("Frequency",),
|
|
16
|
+
"band": ("Band",),
|
|
17
|
+
"mode": ("Mode",),
|
|
18
|
+
"net_control": ("NetControl",),
|
|
19
|
+
"logger": ("Logger",),
|
|
20
|
+
"opened": ("Date",),
|
|
21
|
+
"monitoring": ("SubscriberCount",),
|
|
22
|
+
# past nets only
|
|
23
|
+
"net_id": ("NetID",),
|
|
24
|
+
"closed": ("ClosedAt",),
|
|
25
|
+
"last_activity": ("LastActivity",),
|
|
26
|
+
"aim": ("AIM",),
|
|
27
|
+
"aim_update_ms": ("UpdateInterval",),
|
|
28
|
+
"inactivity_timeout_minutes": ("InactivityTimer", "InactivitytTimer"), # the spec spells it both ways
|
|
29
|
+
"auto_closed": ("Assassinated",),
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
CHECKIN_FIELDS: dict[str, tuple[str, ...]] = {
|
|
33
|
+
"serial": ("SerialNo",),
|
|
34
|
+
"callsign": ("Callsign",),
|
|
35
|
+
"first_name": ("FirstName",),
|
|
36
|
+
"preferred_name": ("PreferredName",),
|
|
37
|
+
"city": ("CityCountry",),
|
|
38
|
+
"county": ("County",),
|
|
39
|
+
"state": ("State",),
|
|
40
|
+
"country": ("Country",),
|
|
41
|
+
"dxcc": ("DXCC",),
|
|
42
|
+
"grid": ("Grid",),
|
|
43
|
+
"status": ("Status",),
|
|
44
|
+
"remarks": ("Remarks",),
|
|
45
|
+
"qsl_info": ("QSLInfo",),
|
|
46
|
+
"member_id": ("MemberID",),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
INT_FIELDS = {"monitoring", "net_id", "aim_update_ms", "inactivity_timeout_minutes", "serial", "dxcc"}
|
|
50
|
+
BOOL_FIELDS = {"aim", "auto_closed"} # Y/N
|
|
51
|
+
TIME_FIELDS = {"opened", "closed", "last_activity"}
|
|
52
|
+
|
|
53
|
+
# Never returned by any tool. Listed so tests can prove it.
|
|
54
|
+
PRIVATE_SOURCE_FIELDS = ("Street", "Zip", "srcIP")
|