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.
@@ -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,7 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ uv.lock
@@ -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")