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.
@@ -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.
@@ -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.
@@ -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.