wsjtx-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.
- wsjtx_mcp-0.1.0/.github/workflows/ci.yml +30 -0
- wsjtx_mcp-0.1.0/.gitignore +33 -0
- wsjtx_mcp-0.1.0/.mcpbignore +21 -0
- wsjtx_mcp-0.1.0/CHANGELOG.md +39 -0
- wsjtx_mcp-0.1.0/CONTRIBUTING.md +40 -0
- wsjtx_mcp-0.1.0/LICENSE +21 -0
- wsjtx_mcp-0.1.0/PKG-INFO +150 -0
- wsjtx_mcp-0.1.0/README.md +128 -0
- wsjtx_mcp-0.1.0/docs/INSTALL.md +76 -0
- wsjtx_mcp-0.1.0/docs/REMOTE-HOST.md +89 -0
- wsjtx_mcp-0.1.0/docs/WSJTX-API-SPEC.md +159 -0
- wsjtx_mcp-0.1.0/docs/WSJTX-API.md +105 -0
- wsjtx_mcp-0.1.0/docs/WSJTX-API.pdf +191 -0
- wsjtx_mcp-0.1.0/icon.png +0 -0
- wsjtx_mcp-0.1.0/make_icon.py +81 -0
- wsjtx_mcp-0.1.0/manifest.json +94 -0
- wsjtx_mcp-0.1.0/pyproject.toml +63 -0
- wsjtx_mcp-0.1.0/server.json +62 -0
- wsjtx_mcp-0.1.0/smoke_test.py +109 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/__init__.py +18 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/client.py +303 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/config.py +57 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/diag.py +91 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/methods.py +147 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/protocol.py +400 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/qdatastream.py +286 -0
- wsjtx_mcp-0.1.0/src/wsjtx_mcp/server.py +502 -0
- wsjtx_mcp-0.1.0/tests/test_client.py +153 -0
- wsjtx_mcp-0.1.0/tests/test_golden.py +59 -0
- wsjtx_mcp-0.1.0/tests/test_methods.py +62 -0
- wsjtx_mcp-0.1.0/tests/test_protocol.py +191 -0
- wsjtx_mcp-0.1.0/tests/test_qdatastream.py +122 -0
- wsjtx_mcp-0.1.0/tests/test_server.py +149 -0
- wsjtx_mcp-0.1.0/tools/udp_relay.py +152 -0
- wsjtx_mcp-0.1.0/uv.lock +1120 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
with:
|
|
21
|
+
python-version: ${{ matrix.python-version }}
|
|
22
|
+
|
|
23
|
+
- name: Sync dependencies
|
|
24
|
+
run: uv sync
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: uv run ruff check .
|
|
28
|
+
|
|
29
|
+
- name: Test
|
|
30
|
+
run: uv run pytest
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
|
|
9
|
+
# Virtual environments / uv
|
|
10
|
+
.venv/
|
|
11
|
+
venv/
|
|
12
|
+
|
|
13
|
+
# Tooling caches
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.pytest_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
|
|
18
|
+
# OS / editor
|
|
19
|
+
.DS_Store
|
|
20
|
+
*.swp
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
|
|
24
|
+
# Packaged desktop-extension build artifact (attach to a GitHub Release instead)
|
|
25
|
+
*.mcpb
|
|
26
|
+
|
|
27
|
+
# Generated icon size variants (only icon.png is tracked)
|
|
28
|
+
icon_*.png
|
|
29
|
+
|
|
30
|
+
# Local live-test scratch scripts (never committed)
|
|
31
|
+
livecheck.py
|
|
32
|
+
probe.py
|
|
33
|
+
captures/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Files excluded from the packaged .mcpb bundle
|
|
2
|
+
.git/
|
|
3
|
+
.github/
|
|
4
|
+
.venv/
|
|
5
|
+
venv/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
.ruff_cache/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.mypy_cache/
|
|
11
|
+
tests/
|
|
12
|
+
*.egg-info/
|
|
13
|
+
build/
|
|
14
|
+
dist/
|
|
15
|
+
.DS_Store
|
|
16
|
+
icon_*.png
|
|
17
|
+
livecheck.py
|
|
18
|
+
probe.py
|
|
19
|
+
smoke_test.py
|
|
20
|
+
make_icon.py
|
|
21
|
+
captures/
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **wsjtx-mcp** are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/), and the project aims to follow
|
|
5
|
+
[Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] - 2026-06-26
|
|
8
|
+
|
|
9
|
+
Initial **experimental** release.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Standard-library Qt `QDataStream` codec (schema 3 / Qt_5_4, big-endian,
|
|
13
|
+
double-precision floats) covering ints, bool, double, utf8/QByteArray
|
|
14
|
+
(null vs. empty), `QTime`, `QDateTime`, and `QColor`.
|
|
15
|
+
- Full WSJT-X message catalog (types 0–15): decoders for the broadcast messages
|
|
16
|
+
(Heartbeat, Status, Decode, Clear, QSOLogged, WSPRDecode, LoggedADIF, Close)
|
|
17
|
+
and builders for the control messages (Heartbeat, Clear, Reply, Close, Replay,
|
|
18
|
+
HaltTx, FreeText, Location, HighlightCallsign, SwitchConfiguration, Configure).
|
|
19
|
+
- Background UDP listener tracking the latest Status, a Decode ring buffer with
|
|
20
|
+
drain-since-last-poll semantics, completed QSOs (QSOLogged paired with ADIF),
|
|
21
|
+
and a per-instance registry (Id → source address) for targeting control.
|
|
22
|
+
- Grouped MCP tools: `status`, `diagnostics`, `decodes`, `log`, `reply`,
|
|
23
|
+
`free_text`, `transmit`, `configure`, `clear`, `highlight`, `location`,
|
|
24
|
+
`switch_config`, and the `wsjtx_call` escape hatch.
|
|
25
|
+
- **Callsign transmit gate**: with `WSJTX_CALLSIGN` blank the server is
|
|
26
|
+
receive-only and refuses every transmit-initiating message.
|
|
27
|
+
- `udp_relay.py` (mcp-host-bridge, UDP edition) for reaching a WSJT-X on another
|
|
28
|
+
host from a loopback-only MCP client.
|
|
29
|
+
- Verified live against WSJT-X (reported version 3.0.2, schema 2 header / max
|
|
30
|
+
schema 3): receive + decode of real Status/Heartbeat datagrams, target
|
|
31
|
+
resolution, and an accepted control round-trip (Replay). Golden byte fixtures
|
|
32
|
+
from that session are checked in.
|
|
33
|
+
|
|
34
|
+
### Known limitations
|
|
35
|
+
- No dial-frequency control over UDP (QSY is a rig-control concern).
|
|
36
|
+
- Cannot "Enable Tx" over UDP — transmission is initiated by `reply` or
|
|
37
|
+
`free_text` send, and only halted via `transmit`.
|
|
38
|
+
- `Configure` cannot express "no change" for its two booleans (`fast_mode`,
|
|
39
|
+
`generate_messages`), so they are always sent.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Thanks for your interest in **wsjtx-mcp**.
|
|
4
|
+
|
|
5
|
+
## Development setup
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv sync
|
|
9
|
+
uv run ruff check .
|
|
10
|
+
uv run pytest
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The protocol layer (`qdatastream.py`, `protocol.py`, `methods.py`) is pure
|
|
14
|
+
standard library and unit-tested against byte fixtures — including **golden
|
|
15
|
+
fixtures captured from a live WSJT-X** (`tests/test_golden.py`). The tests need no
|
|
16
|
+
running WSJT-X.
|
|
17
|
+
|
|
18
|
+
To verify against your own station, run `uv run python smoke_test.py 22 --save`:
|
|
19
|
+
it binds the UDP port, decodes whatever WSJT-X broadcasts, and (with `--save`)
|
|
20
|
+
writes raw datagrams to `captures/` you can turn into new fixtures.
|
|
21
|
+
|
|
22
|
+
## House style
|
|
23
|
+
|
|
24
|
+
- Python 3.10+, `uv`, FastMCP, `src/` layout.
|
|
25
|
+
- `ruff` with `line-length = 100` (run it before pushing).
|
|
26
|
+
- Grouped-tools pattern: one tool per functional area taking an `operation`
|
|
27
|
+
argument, plus the `wsjtx_call` escape hatch.
|
|
28
|
+
- Standard-library-only at runtime where possible (the only dependency is `mcp`).
|
|
29
|
+
|
|
30
|
+
## Safety
|
|
31
|
+
|
|
32
|
+
Anything that can key a transmitter must stay behind the **callsign transmit
|
|
33
|
+
gate**. New transmit-initiating paths must call `_require_tx(...)` and be covered
|
|
34
|
+
by a test that proves they are refused when `WSJTX_CALLSIGN` is blank.
|
|
35
|
+
|
|
36
|
+
## Reporting protocol discrepancies
|
|
37
|
+
|
|
38
|
+
If a field decodes wrong against your WSJT-X build, please open an issue with a
|
|
39
|
+
captured datagram (hex or base64) and your WSJT-X version — that is the fastest
|
|
40
|
+
path to a fix, and may become a new golden fixture.
|
wsjtx_mcp-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stefan Brunner (AE5VG)
|
|
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.
|
wsjtx_mcp-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wsjtx-mcp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An MCP server that controls WSJT-X (FT8/FT4/etc.) over its UDP message protocol.
|
|
5
|
+
Project-URL: Homepage, https://github.com/sbrunner-atx/wsjtx-mcp
|
|
6
|
+
Project-URL: Repository, https://github.com/sbrunner-atx/wsjtx-mcp
|
|
7
|
+
Project-URL: Issues, https://github.com/sbrunner-atx/wsjtx-mcp/issues
|
|
8
|
+
Author-email: "Stefan Brunner (AE5VG)" <me@stefanbrunner.org>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: amateur-radio,ft4,ft8,ham-radio,mcp,model-context-protocol,weak-signal,wsjtx
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
18
|
+
Classifier: Topic :: Communications :: Ham Radio
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: mcp[cli]>=1.2.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
<!-- mcp-name: io.github.sbrunner-atx/wsjtx-mcp -->
|
|
24
|
+
|
|
25
|
+
# wsjtx-mcp
|
|
26
|
+
|
|
27
|
+
An [MCP](https://modelcontextprotocol.io) server that controls **WSJT-X**
|
|
28
|
+
(FT8/FT4/JT65/MSK144/Q65/WSPR…) from MCP clients such as Claude Desktop and the
|
|
29
|
+
MCP Inspector.
|
|
30
|
+
|
|
31
|
+
It is the weak-signal leg of an "operate → log" trio for amateur radio:
|
|
32
|
+
|
|
33
|
+
- **[fldigi-mcp](https://github.com/sbrunner-atx/fldigi-mcp)** — operate broad
|
|
34
|
+
digital modes via fldigi (XML-RPC).
|
|
35
|
+
- **[contest-mcp](https://github.com/sbrunner-atx/contest-mcp)** — log QSOs to
|
|
36
|
+
N3FJP (TCP API).
|
|
37
|
+
- **wsjtx-mcp** *(this one)* — operate the FT8/FT4 weak-signal world via WSJT-X
|
|
38
|
+
(UDP message protocol).
|
|
39
|
+
|
|
40
|
+
> ⚠️ **Experimental (v0.1).** Transmit is gated behind your callsign and you keep
|
|
41
|
+
> the operator in command. Read the [Transmit safety](#transmit-safety) section.
|
|
42
|
+
|
|
43
|
+
## How it is different
|
|
44
|
+
|
|
45
|
+
WSJT-X does **not** offer a request/response API. It *broadcasts* state over UDP
|
|
46
|
+
(`Status`, `Decode`, `QSOLogged`, `Heartbeat`, …) and honours a small set of
|
|
47
|
+
inbound *control* messages. So this server runs a **background UDP listener** that
|
|
48
|
+
continuously parses datagrams and keeps the latest status, a buffer of decodes,
|
|
49
|
+
and completed QSOs — you read those, and nudge WSJT-X with control messages.
|
|
50
|
+
|
|
51
|
+
Two consequences worth knowing up front:
|
|
52
|
+
|
|
53
|
+
- **No dial-frequency control over UDP.** You can read the dial frequency from
|
|
54
|
+
`Status`, and set mode/sub-mode/Rx DF/T-R period via `configure`, but **QSY is a
|
|
55
|
+
rig-control concern** (Hamlib/CAT or the UI), not this server.
|
|
56
|
+
- **You can halt Tx but not "Enable Tx".** Transmission is *started* by answering
|
|
57
|
+
a CQ (`reply`) or by `free_text` with `send=true`; it is *stopped* by
|
|
58
|
+
`transmit halt`. There is no UDP command to press "Enable Tx".
|
|
59
|
+
|
|
60
|
+
## Requirements
|
|
61
|
+
|
|
62
|
+
- WSJT-X 2.x (verified against the 2.7 message schema, **schema 3 / Qt_5_4**).
|
|
63
|
+
- In WSJT-X: **Settings → Reporting → UDP Server**
|
|
64
|
+
- **UDP Server** = the host running this server (default `127.0.0.1`), **port
|
|
65
|
+
`2237`**.
|
|
66
|
+
- **Accept UDP requests** = **ON** to allow *control* (it is OFF by default).
|
|
67
|
+
Observing decodes/status works without it; commanding does not.
|
|
68
|
+
|
|
69
|
+
## Install (Claude Desktop)
|
|
70
|
+
|
|
71
|
+
Download the `wsjtx-mcp.mcpb` from the
|
|
72
|
+
[latest release](https://github.com/sbrunner-atx/wsjtx-mcp/releases) and
|
|
73
|
+
double-click it, or drag it onto Claude Desktop → Settings → Extensions. Fill in
|
|
74
|
+
the settings form (callsign, host, port). See [docs/INSTALL.md](docs/INSTALL.md).
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
| Variable | Default | Purpose |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `WSJTX_HOST` | `127.0.0.1` | UDP address to **bind/listen** on. |
|
|
81
|
+
| `WSJTX_PORT` | `2237` | WSJT-X UDP Server port. |
|
|
82
|
+
| `WSJTX_CALLSIGN` | _(empty)_ | Operator callsign — **the single transmit gate**. Blank = receive-only. |
|
|
83
|
+
| `WSJTX_MULTICAST` | _(off)_ | Optional multicast group to join (coexist with other UDP consumers). |
|
|
84
|
+
| `WSJTX_INSTANCE` | _(auto)_ | Target a specific WSJT-X `Id` when several instances broadcast. |
|
|
85
|
+
|
|
86
|
+
Host/port are **where this server listens**; control replies are sent back to the
|
|
87
|
+
address each datagram arrived from.
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
| Tool | Kind | What it does |
|
|
92
|
+
| --- | --- | --- |
|
|
93
|
+
| `status` | observe | Latest `Status` snapshot + listener/instance health. |
|
|
94
|
+
| `diagnostics` | observe | Host/network + bind status + datagram counts + gate state. |
|
|
95
|
+
| `decodes` | observe/nudge | `read` / `drain` (poll new) / `clear_local` / `replay`. The RX plane. |
|
|
96
|
+
| `log` | observe | Buffered completed QSOs (`QSOLogged` + `LoggedADIF`) → feed N3FJP. |
|
|
97
|
+
| `reply` | **transmit** | Answer a buffered CQ/QRZ decode (auto-sequences the QSO). |
|
|
98
|
+
| `free_text` | **transmit** if `send` | Set the Tx5 free-text message; `send=true` keys the radio. |
|
|
99
|
+
| `transmit` | control | `halt` / `halt_auto` — stop transmitting (UDP can't *enable* Tx). |
|
|
100
|
+
| `configure` | control | Mode/sub-mode/Rx DF/T-R period/freq-tol/DX call+grid. **No dial freq.** |
|
|
101
|
+
| `clear` | control | Clear the Band Activity / Rx Frequency windows. |
|
|
102
|
+
| `highlight` | control | Colour or clear a callsign in Band Activity. |
|
|
103
|
+
| `location` | control | Override the session Maidenhead grid. |
|
|
104
|
+
| `switch_config` | control | Switch to a named WSJT-X configuration. |
|
|
105
|
+
| `wsjtx_call` | escape hatch | Build & send any message type by name (gate still applies). |
|
|
106
|
+
|
|
107
|
+
## Transmit safety
|
|
108
|
+
|
|
109
|
+
The **callsign is the single transmit gate**, exactly as in fldigi-mcp. With
|
|
110
|
+
`WSJTX_CALLSIGN` blank the server is **receive-only**: it refuses every
|
|
111
|
+
transmit-initiating message — `reply`, `free_text` with `send=true`, and any
|
|
112
|
+
keying message via `wsjtx_call`. `transmit halt`, `clear`, `configure`,
|
|
113
|
+
`highlight`, `location`, `replay`, and all reads are always available (they don't
|
|
114
|
+
put you on the air; halt takes you *off*).
|
|
115
|
+
|
|
116
|
+
Beyond that gate:
|
|
117
|
+
|
|
118
|
+
- Per-transmit approval comes from the Claude Desktop tool-permission prompt —
|
|
119
|
+
lean on it for human-in-the-loop control.
|
|
120
|
+
- WSJT-X's own **Tx Watchdog** and the `Tx Enabled` / `Transmitting` flags
|
|
121
|
+
(surfaced in `status`) are extra safety signals.
|
|
122
|
+
- Operating under **Part 97 automatic/remote control** is the operator's
|
|
123
|
+
responsibility: ensure station identification and a control operator who can
|
|
124
|
+
intervene.
|
|
125
|
+
|
|
126
|
+
## Running alongside other UDP tools
|
|
127
|
+
|
|
128
|
+
Only one process can normally own UDP `2237` on a host. If JTAlert, GridTracker,
|
|
129
|
+
or N1MM already consume it, either point WSJT-X's *secondary* UDP server here, use
|
|
130
|
+
a **multicast** group (`WSJTX_MULTICAST`) so several listeners coexist, or run
|
|
131
|
+
this server on a different host. See [docs/REMOTE-HOST.md](docs/REMOTE-HOST.md)
|
|
132
|
+
for reaching a WSJT-X on another machine (it requires a small UDP forwarder
|
|
133
|
+
because sandboxed MCP clients reach only loopback).
|
|
134
|
+
|
|
135
|
+
## Development
|
|
136
|
+
|
|
137
|
+
```sh
|
|
138
|
+
uv sync
|
|
139
|
+
uv run ruff check .
|
|
140
|
+
uv run pytest
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The protocol codec is pure standard library and unit-tested against byte
|
|
144
|
+
fixtures, so the tests need no running WSJT-X. A `smoke_test.py` proves a live
|
|
145
|
+
WSJT-X is reachable receive-only. The field-tested message reference lives in
|
|
146
|
+
[docs/WSJTX-API.md](docs/WSJTX-API.md).
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
MIT © 2026 Stefan Brunner (AE5VG)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<!-- mcp-name: io.github.sbrunner-atx/wsjtx-mcp -->
|
|
2
|
+
|
|
3
|
+
# wsjtx-mcp
|
|
4
|
+
|
|
5
|
+
An [MCP](https://modelcontextprotocol.io) server that controls **WSJT-X**
|
|
6
|
+
(FT8/FT4/JT65/MSK144/Q65/WSPR…) from MCP clients such as Claude Desktop and the
|
|
7
|
+
MCP Inspector.
|
|
8
|
+
|
|
9
|
+
It is the weak-signal leg of an "operate → log" trio for amateur radio:
|
|
10
|
+
|
|
11
|
+
- **[fldigi-mcp](https://github.com/sbrunner-atx/fldigi-mcp)** — operate broad
|
|
12
|
+
digital modes via fldigi (XML-RPC).
|
|
13
|
+
- **[contest-mcp](https://github.com/sbrunner-atx/contest-mcp)** — log QSOs to
|
|
14
|
+
N3FJP (TCP API).
|
|
15
|
+
- **wsjtx-mcp** *(this one)* — operate the FT8/FT4 weak-signal world via WSJT-X
|
|
16
|
+
(UDP message protocol).
|
|
17
|
+
|
|
18
|
+
> ⚠️ **Experimental (v0.1).** Transmit is gated behind your callsign and you keep
|
|
19
|
+
> the operator in command. Read the [Transmit safety](#transmit-safety) section.
|
|
20
|
+
|
|
21
|
+
## How it is different
|
|
22
|
+
|
|
23
|
+
WSJT-X does **not** offer a request/response API. It *broadcasts* state over UDP
|
|
24
|
+
(`Status`, `Decode`, `QSOLogged`, `Heartbeat`, …) and honours a small set of
|
|
25
|
+
inbound *control* messages. So this server runs a **background UDP listener** that
|
|
26
|
+
continuously parses datagrams and keeps the latest status, a buffer of decodes,
|
|
27
|
+
and completed QSOs — you read those, and nudge WSJT-X with control messages.
|
|
28
|
+
|
|
29
|
+
Two consequences worth knowing up front:
|
|
30
|
+
|
|
31
|
+
- **No dial-frequency control over UDP.** You can read the dial frequency from
|
|
32
|
+
`Status`, and set mode/sub-mode/Rx DF/T-R period via `configure`, but **QSY is a
|
|
33
|
+
rig-control concern** (Hamlib/CAT or the UI), not this server.
|
|
34
|
+
- **You can halt Tx but not "Enable Tx".** Transmission is *started* by answering
|
|
35
|
+
a CQ (`reply`) or by `free_text` with `send=true`; it is *stopped* by
|
|
36
|
+
`transmit halt`. There is no UDP command to press "Enable Tx".
|
|
37
|
+
|
|
38
|
+
## Requirements
|
|
39
|
+
|
|
40
|
+
- WSJT-X 2.x (verified against the 2.7 message schema, **schema 3 / Qt_5_4**).
|
|
41
|
+
- In WSJT-X: **Settings → Reporting → UDP Server**
|
|
42
|
+
- **UDP Server** = the host running this server (default `127.0.0.1`), **port
|
|
43
|
+
`2237`**.
|
|
44
|
+
- **Accept UDP requests** = **ON** to allow *control* (it is OFF by default).
|
|
45
|
+
Observing decodes/status works without it; commanding does not.
|
|
46
|
+
|
|
47
|
+
## Install (Claude Desktop)
|
|
48
|
+
|
|
49
|
+
Download the `wsjtx-mcp.mcpb` from the
|
|
50
|
+
[latest release](https://github.com/sbrunner-atx/wsjtx-mcp/releases) and
|
|
51
|
+
double-click it, or drag it onto Claude Desktop → Settings → Extensions. Fill in
|
|
52
|
+
the settings form (callsign, host, port). See [docs/INSTALL.md](docs/INSTALL.md).
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
| Variable | Default | Purpose |
|
|
57
|
+
| --- | --- | --- |
|
|
58
|
+
| `WSJTX_HOST` | `127.0.0.1` | UDP address to **bind/listen** on. |
|
|
59
|
+
| `WSJTX_PORT` | `2237` | WSJT-X UDP Server port. |
|
|
60
|
+
| `WSJTX_CALLSIGN` | _(empty)_ | Operator callsign — **the single transmit gate**. Blank = receive-only. |
|
|
61
|
+
| `WSJTX_MULTICAST` | _(off)_ | Optional multicast group to join (coexist with other UDP consumers). |
|
|
62
|
+
| `WSJTX_INSTANCE` | _(auto)_ | Target a specific WSJT-X `Id` when several instances broadcast. |
|
|
63
|
+
|
|
64
|
+
Host/port are **where this server listens**; control replies are sent back to the
|
|
65
|
+
address each datagram arrived from.
|
|
66
|
+
|
|
67
|
+
## Tools
|
|
68
|
+
|
|
69
|
+
| Tool | Kind | What it does |
|
|
70
|
+
| --- | --- | --- |
|
|
71
|
+
| `status` | observe | Latest `Status` snapshot + listener/instance health. |
|
|
72
|
+
| `diagnostics` | observe | Host/network + bind status + datagram counts + gate state. |
|
|
73
|
+
| `decodes` | observe/nudge | `read` / `drain` (poll new) / `clear_local` / `replay`. The RX plane. |
|
|
74
|
+
| `log` | observe | Buffered completed QSOs (`QSOLogged` + `LoggedADIF`) → feed N3FJP. |
|
|
75
|
+
| `reply` | **transmit** | Answer a buffered CQ/QRZ decode (auto-sequences the QSO). |
|
|
76
|
+
| `free_text` | **transmit** if `send` | Set the Tx5 free-text message; `send=true` keys the radio. |
|
|
77
|
+
| `transmit` | control | `halt` / `halt_auto` — stop transmitting (UDP can't *enable* Tx). |
|
|
78
|
+
| `configure` | control | Mode/sub-mode/Rx DF/T-R period/freq-tol/DX call+grid. **No dial freq.** |
|
|
79
|
+
| `clear` | control | Clear the Band Activity / Rx Frequency windows. |
|
|
80
|
+
| `highlight` | control | Colour or clear a callsign in Band Activity. |
|
|
81
|
+
| `location` | control | Override the session Maidenhead grid. |
|
|
82
|
+
| `switch_config` | control | Switch to a named WSJT-X configuration. |
|
|
83
|
+
| `wsjtx_call` | escape hatch | Build & send any message type by name (gate still applies). |
|
|
84
|
+
|
|
85
|
+
## Transmit safety
|
|
86
|
+
|
|
87
|
+
The **callsign is the single transmit gate**, exactly as in fldigi-mcp. With
|
|
88
|
+
`WSJTX_CALLSIGN` blank the server is **receive-only**: it refuses every
|
|
89
|
+
transmit-initiating message — `reply`, `free_text` with `send=true`, and any
|
|
90
|
+
keying message via `wsjtx_call`. `transmit halt`, `clear`, `configure`,
|
|
91
|
+
`highlight`, `location`, `replay`, and all reads are always available (they don't
|
|
92
|
+
put you on the air; halt takes you *off*).
|
|
93
|
+
|
|
94
|
+
Beyond that gate:
|
|
95
|
+
|
|
96
|
+
- Per-transmit approval comes from the Claude Desktop tool-permission prompt —
|
|
97
|
+
lean on it for human-in-the-loop control.
|
|
98
|
+
- WSJT-X's own **Tx Watchdog** and the `Tx Enabled` / `Transmitting` flags
|
|
99
|
+
(surfaced in `status`) are extra safety signals.
|
|
100
|
+
- Operating under **Part 97 automatic/remote control** is the operator's
|
|
101
|
+
responsibility: ensure station identification and a control operator who can
|
|
102
|
+
intervene.
|
|
103
|
+
|
|
104
|
+
## Running alongside other UDP tools
|
|
105
|
+
|
|
106
|
+
Only one process can normally own UDP `2237` on a host. If JTAlert, GridTracker,
|
|
107
|
+
or N1MM already consume it, either point WSJT-X's *secondary* UDP server here, use
|
|
108
|
+
a **multicast** group (`WSJTX_MULTICAST`) so several listeners coexist, or run
|
|
109
|
+
this server on a different host. See [docs/REMOTE-HOST.md](docs/REMOTE-HOST.md)
|
|
110
|
+
for reaching a WSJT-X on another machine (it requires a small UDP forwarder
|
|
111
|
+
because sandboxed MCP clients reach only loopback).
|
|
112
|
+
|
|
113
|
+
## Development
|
|
114
|
+
|
|
115
|
+
```sh
|
|
116
|
+
uv sync
|
|
117
|
+
uv run ruff check .
|
|
118
|
+
uv run pytest
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The protocol codec is pure standard library and unit-tested against byte
|
|
122
|
+
fixtures, so the tests need no running WSJT-X. A `smoke_test.py` proves a live
|
|
123
|
+
WSJT-X is reachable receive-only. The field-tested message reference lives in
|
|
124
|
+
[docs/WSJTX-API.md](docs/WSJTX-API.md).
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT © 2026 Stefan Brunner (AE5VG)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Installing wsjtx-mcp
|
|
2
|
+
|
|
3
|
+
## 1. Enable WSJT-X's UDP Server
|
|
4
|
+
|
|
5
|
+
In WSJT-X: **Settings → Reporting → UDP Server**.
|
|
6
|
+
|
|
7
|
+
- **UDP Server**: the host running wsjtx-mcp. For a local setup leave it at
|
|
8
|
+
`127.0.0.1`.
|
|
9
|
+
- **UDP Server port number**: `2237` (the default).
|
|
10
|
+
- **Accept UDP requests**: **tick this** if you want wsjtx-mcp to *control*
|
|
11
|
+
WSJT-X (answer CQs, set free text, configure, etc.). It is **off by default**.
|
|
12
|
+
Observing decodes and status works without it; commanding does not.
|
|
13
|
+
|
|
14
|
+
> Tip: if JTAlert / GridTracker / N1MM already use port 2237, see
|
|
15
|
+
> [Running alongside other UDP tools](#running-alongside-other-udp-tools).
|
|
16
|
+
|
|
17
|
+
## 2a. Install as a Claude Desktop extension (.mcpb)
|
|
18
|
+
|
|
19
|
+
1. Download `wsjtx-mcp.mcpb` from the
|
|
20
|
+
[latest release](https://github.com/sbrunner-atx/wsjtx-mcp/releases).
|
|
21
|
+
2. If you previously installed an older build, **remove it first** (Settings →
|
|
22
|
+
Extensions → uninstall, and wait for its tile to disappear) so the swap takes
|
|
23
|
+
effect.
|
|
24
|
+
3. Double-click the `.mcpb`, or drag it onto Claude Desktop → Settings →
|
|
25
|
+
Extensions.
|
|
26
|
+
4. Fill in the settings form:
|
|
27
|
+
- **Operator callsign** — your licensed callsign. Leave **blank for
|
|
28
|
+
receive-only**; set it to enable transmit.
|
|
29
|
+
- **Listen host / port** — usually `127.0.0.1` / `2237`.
|
|
30
|
+
5. **Quit and reopen Claude Desktop** (Cmd-Q) so the new tools load.
|
|
31
|
+
|
|
32
|
+
## 2b. Install from PyPI (any MCP client)
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
uvx wsjtx-mcp # or: pipx run wsjtx-mcp
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Add it to your client's MCP config with the environment variables from the
|
|
39
|
+
[README configuration table](../README.md#configuration), e.g.:
|
|
40
|
+
|
|
41
|
+
```json
|
|
42
|
+
{
|
|
43
|
+
"mcpServers": {
|
|
44
|
+
"wsjtx-mcp": {
|
|
45
|
+
"command": "uvx",
|
|
46
|
+
"args": ["wsjtx-mcp"],
|
|
47
|
+
"env": { "WSJTX_CALLSIGN": "", "WSJTX_HOST": "127.0.0.1", "WSJTX_PORT": "2237" }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 3. Verify
|
|
54
|
+
|
|
55
|
+
Ask your client to run the **`status`** tool. You should see the current dial
|
|
56
|
+
frequency, mode, and the discovered instance within ~15 seconds (WSJT-X sends a
|
|
57
|
+
Heartbeat every 15 s plus a Status on each state change). If nothing appears, run
|
|
58
|
+
**`diagnostics`** — it reports whether the UDP listener bound and how many
|
|
59
|
+
datagrams have arrived.
|
|
60
|
+
|
|
61
|
+
## Running alongside other UDP tools
|
|
62
|
+
|
|
63
|
+
Only one process can own UDP `2237` on a host. Options:
|
|
64
|
+
|
|
65
|
+
- Point WSJT-X's **secondary** UDP server at wsjtx-mcp's host:port.
|
|
66
|
+
- Use a **multicast** group: set the same group in WSJT-X's UDP Server and in
|
|
67
|
+
`WSJTX_MULTICAST`, so several listeners coexist.
|
|
68
|
+
- Run wsjtx-mcp on a different host (see [REMOTE-HOST.md](REMOTE-HOST.md)).
|
|
69
|
+
|
|
70
|
+
## Transmit safety
|
|
71
|
+
|
|
72
|
+
The **callsign is the single transmit gate**. With it blank, wsjtx-mcp is
|
|
73
|
+
receive-only and refuses every transmit-initiating message. Per-transmit approval
|
|
74
|
+
also comes from your client's tool-permission prompt. You — the licensed control
|
|
75
|
+
operator — remain responsible for lawful operation (station ID, ability to
|
|
76
|
+
intervene, Part 97 automatic/remote-control rules).
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Reaching a WSJT-X on another computer (UDP host bridge)
|
|
2
|
+
|
|
3
|
+
Sandboxed MCP clients — notably Claude Desktop — let their connector subprocess
|
|
4
|
+
reach only `127.0.0.1` (loopback), **not LAN IP addresses**, even with macOS
|
|
5
|
+
*Privacy & Security → Local Network* toggled on. So if WSJT-X runs on a different
|
|
6
|
+
machine than wsjtx-mcp, the server cannot talk to it directly.
|
|
7
|
+
|
|
8
|
+
The fix is a small **UDP relay** that runs *outside* the sandbox on the
|
|
9
|
+
wsjtx-mcp host, bridges the LAN to loopback, and routes WSJT-X's broadcasts in
|
|
10
|
+
and your control replies back out. WSJT-X's protocol is connectionless and
|
|
11
|
+
*bidirectional*, so this needs a UDP-aware relay (`tools/udp_relay.py` in this
|
|
12
|
+
repo, the UDP sibling of the TCP `mcp-host-bridge` relay used by fldigi-mcp /
|
|
13
|
+
contest-mcp).
|
|
14
|
+
|
|
15
|
+
## Topology
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
remote WSJT-X bridge host (Mac) wsjtx-mcp
|
|
19
|
+
Settings → Reporting udp_relay.py (loopback only)
|
|
20
|
+
UDP Server = ──► --listen 0.0.0.0:2237 ──► --deliver 127.0.0.1:2238
|
|
21
|
+
<bridge-LAN-IP> : 2237 (LAN socket A) WSJTX_HOST=127.0.0.1
|
|
22
|
+
(loopback socket B) WSJTX_PORT=2238
|
|
23
|
+
control replies ◄── B → A → back to the remote WSJT-X
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- WSJT-X (remote) sends its UDP datagrams to the **bridge host's LAN IP**, port
|
|
27
|
+
`2237`.
|
|
28
|
+
- The relay learns the remote peer from the first datagram and forwards
|
|
29
|
+
everything to wsjtx-mcp on `127.0.0.1:2238`.
|
|
30
|
+
- wsjtx-mcp's control replies go back to the relay's loopback socket, which sends
|
|
31
|
+
them on to the remote WSJT-X — exactly the address each datagram came from.
|
|
32
|
+
|
|
33
|
+
## Run it
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
python3 ~/.mcp-host-bridge/udp_relay.py run \
|
|
37
|
+
--listen 0.0.0.0:2237 \
|
|
38
|
+
--deliver 127.0.0.1:2238
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Then set wsjtx-mcp's config to `WSJTX_HOST=127.0.0.1`, `WSJTX_PORT=2238`, and in
|
|
42
|
+
the remote WSJT-X set **UDP Server** to the bridge host's LAN IP, port `2237`
|
|
43
|
+
(and tick **Accept UDP requests** for control).
|
|
44
|
+
|
|
45
|
+
## Run it under launchd (macOS, auto-start)
|
|
46
|
+
|
|
47
|
+
Save as `~/Library/LaunchAgents/com.mcp-host-bridge.wsjtx.plist` and
|
|
48
|
+
`launchctl load` it (mirrors the existing TCP bridge agents; uses the system
|
|
49
|
+
`/usr/bin/python3`, so no venv/PATH dependency):
|
|
50
|
+
|
|
51
|
+
```xml
|
|
52
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
54
|
+
<plist version="1.0">
|
|
55
|
+
<dict>
|
|
56
|
+
<key>Label</key><string>com.mcp-host-bridge.wsjtx</string>
|
|
57
|
+
<key>ProgramArguments</key>
|
|
58
|
+
<array>
|
|
59
|
+
<string>/usr/bin/python3</string>
|
|
60
|
+
<string>/Users/YOU/.mcp-host-bridge/udp_relay.py</string>
|
|
61
|
+
<string>run</string>
|
|
62
|
+
<string>--listen</string>
|
|
63
|
+
<string>0.0.0.0:2237</string>
|
|
64
|
+
<string>--deliver</string>
|
|
65
|
+
<string>127.0.0.1:2238</string>
|
|
66
|
+
</array>
|
|
67
|
+
<key>RunAtLoad</key><true/>
|
|
68
|
+
<key>KeepAlive</key><true/>
|
|
69
|
+
<key>StandardOutPath</key><string>/tmp/mcp-host-bridge-wsjtx.log</string>
|
|
70
|
+
<key>StandardErrorPath</key><string>/tmp/mcp-host-bridge-wsjtx.err</string>
|
|
71
|
+
</dict>
|
|
72
|
+
</plist>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
launchctl load ~/Library/LaunchAgents/com.mcp-host-bridge.wsjtx.plist
|
|
77
|
+
# to update args later:
|
|
78
|
+
launchctl unload ~/Library/LaunchAgents/com.mcp-host-bridge.wsjtx.plist && \
|
|
79
|
+
launchctl load ~/Library/LaunchAgents/com.mcp-host-bridge.wsjtx.plist
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- Run the relay on a **different loopback port** (`2238`) than the LAN port
|
|
85
|
+
(`2237`) so the relay's LAN socket and wsjtx-mcp's listener don't collide.
|
|
86
|
+
- For a **local** WSJT-X (same machine), you don't need the relay at all — point
|
|
87
|
+
wsjtx-mcp straight at `127.0.0.1:2237`.
|
|
88
|
+
- Multicast is an alternative to the relay when every consumer is on the same
|
|
89
|
+
LAN segment: set `WSJTX_MULTICAST` and WSJT-X's UDP Server to the same group.
|