gr4-mcp
# gr4-mcp
> **Beta release.** gr4-mcp is under active development. Tools, file formats and
> behaviour may still change, and it has only been tested on one setup (see below).
> Feedback and bug reports are welcome.
An MCP server (Python, [FastMCP](https://gofastmcp.com)) that lets an AI client build,
run and tune GNU Radio 4 flowgraphs through the GR4 control plane (`gr4cp_server`).
## About
gr4-mcp is part of an attempt to make amateur television (ATV) more accessible to the
public: instead of wiring signal-processing blocks by hand, you can describe what you
want to an AI assistant and let it build, run and tune the GNU Radio 4 flowgraph.
More about the project: [curtat.be](https://curtat.be) and
[atv.curtat.be](https://atv.curtat.be).
## Installing GNU Radio 4
gr4-mcp drives an existing GNU Radio 4 installation; it does not install GNU Radio itself.
Follow the official instructions:
- GNU Radio 4 workspace: <https://github.com/gnuradio/gnuradio4>
- Build guide: <https://github.com/gnuradio/gnuradio4/blob/maint-4.0/docs/building.md>
gr4-mcp needs the complete workspace build (the `full` preset), which includes the
control plane (`gr4cp_server`) and gr4-studio:
```sh
git clone https://github.com/gnuradio/gnuradio4.git
cd gnuradio4
cmake --preset full
cmake --build --preset full
```
This installs into `gnuradio4/install/full`; that folder is what gr4-mcp needs
(see [Where gr4-mcp finds GNU Radio 4](#where-gr4-mcp-finds-gnu-radio-4)).
**Tested on Ubuntu 26.04 LTS** with Python 3.14. Other platforms may work but are untested.
## How it works
`gr4cp_server` has no graph-editing API: a session is created from one complete GRC
YAML document, and only block *settings* can change while it runs. gr4-mcp therefore
keeps the graph being built client-side, validates each edit against the backend's
block catalog, and submits the whole graph on `deploy_graph`.
Validation matters because the backend accepts these mistakes silently:
| Mistake | Backend behaviour | gr4-mcp |
|---|---|---|
| Unknown parameter (`frequency` vs `tone_frequency`) | ignored | rejected with suggestions |
| Unknown block type | accepted, fails at start | rejected with type variants |
| Bare dynamic port (`in` instead of `in#0`) | accepted, **no data flows** | mapped to next free `in#N`, `n_inputs` kept in sync |
| Port type mismatch | fails at start | rejected at `connect` |
| Studio sink without `transport`/`payload_format` | fails at start | filled in automatically (an HTTP transport the sink supports) |
The server owns its backend: on startup it spawns `gr4cp_server` on a free loopback
port (the same `GR4CP_PORT=0` / `GR4CP_PORT_FILE` handshake gr4-studio uses) and stops
it when the MCP server exits, including on SIGKILL (Linux `PR_SET_PDEATHSIG`).
It is independent of any backend gr4-studio is running. The backend log is
`<prefix>/var/logs/gr4cp_server-mcp.log`.
## Setup
```sh
cd gr4-mcp
python3 -m venv --without-pip .venv # python3-venv/ensurepip is not required
pip3 --python .venv/bin/python install -e '.[test]'
```
### Where gr4-mcp finds GNU Radio 4
gr4-mcp needs the GNU Radio 4 install folder, the one containing `bin/gr4cp_server`
(for this workspace: `/home/peter/gnuradio4/install/full`). It looks, in order, at:
1. a folder given with `gr4-mcp --prefix PATH` or registered with the `set_gr4_install` tool,
2. the `GR4_PREFIX` environment variable,
3. the folder saved in `~/.config/gr4-mcp/config.json` (`set_gr4_install` writes it;
`GR4_MCP_CONFIG` overrides the file location),
4. `gr4cp_server` on `PATH`,
5. `../install/full` relative to this repo (the workspace `full` preset).
Each accepts the install prefix, its `bin/` folder or the `gr4cp_server` binary. If none
is found, `backend_status` says so and the client can ask you for the folder and call
`set_gr4_install(path)`, which restarts the backend from it and saves the choice.
### Using an existing backend (e.g. gr4-studio's)
By default gr4-mcp runs its own private backend. To share one with gr4-studio, so graphs
deployed by the AI show up as Studio sessions and Studio's sessions can be read and tuned,
attach to it:
- the `attach_backend` tool: `attach_backend("http://127.0.0.1:37945")`, or
`attach_backend("studio")` to find a running gr4-studio backend from the
`gr4cp_server*.log` files in the install's `var/logs`; `attach_backend()` switches back
to a private backend;
- at startup: `gr4-mcp --backend-url URL` (or `--backend-url studio`), or the
`GR4_BACKEND_URL` environment variable.
Attaching stops gr4-mcp's private backend (its sessions are lost). gr4-mcp never stops a
backend it attached to, and leaves sessions it deployed there running when it switches
away. The URL is not saved, because backend ports change on every launch.
Register with Claude Code:
```sh
claude mcp add gnuradio4 -- /home/peter/gnuradio4/gr4-mcp/.venv/bin/gr4-mcp
# or pin the install explicitly:
claude mcp add gnuradio4 -- /home/peter/gnuradio4/gr4-mcp/.venv/bin/gr4-mcp --prefix /home/peter/gnuradio4/install/full
```
## Tools
| Group | Tools |
|---|---|
| Backend / catalog | `backend_status`, `set_gr4_install`, `attach_backend`, `list_categories`, `search_blocks`, `describe_block` |
| Graph editing | `new_graph`, `show_graph`, `add_block`, `remove_block`, `set_parameters`, `connect`, `disconnect`, `validate_graph` |
| Files | `save_graph`, `load_graph` (`.gr4s` = gr4-studio document, `.gr4c` = GRC YAML) |
| Sessions | `deploy_graph`, `list_sessions`, `session_status`, `control_session`, `get_block_settings`, `update_block_settings` |
| Data | `read_stream` |
A `hello_world` prompt walks the client through the example below.
## Hello world
```
new_graph("hello_world")
add_block("gr::blocks::basic::SignalGenerator<float32>", "src",
{"signal_type": "Sin", "sample_rate": 1000, "tone_frequency": 5})
add_block("gr::blocks::basic::Throttle<float32>", "throttle", {"sample_rate": 1000})
add_block("gr::studio::StudioSeriesSink<float32>", "scope")
connect("src", "throttle")
connect("throttle", "scope") # -> [throttle, out, scope, in#0]
deploy_graph() # session running, stream "scope" ready
read_stream() # scope: rms ~0.707, dominant_period_samples ~200
update_block_settings("src", {"tone_frequency": 10})
save_graph("hello_world.gr4s") # open in gr4-studio
```
## Reading stream data
`read_stream` reads what the `gr::studio::*` sinks currently hold, through the
backend's `/sessions/{id}/streams/{id}/http` endpoint. Stream ids are the sink block
names. Results are summarized to keep them small:
| Sink | Payload format | Summary |
|---|---|---|
| `StudioSeriesSink` | `series-window-json-v1` | per channel: min/max/mean/rms/std, `dominant_period_samples`, downsampled samples |
| `StudioPowerSpectrumSink`, `StudioDataSetSink` | `dataset-xy-json-v1` | strongest `peaks` (x, y), x range, y stats, downsampled points |
| `StudioWaterfallSink` | `waterfall-spectrum-json-v1` | stats and peaks of the latest row |
| `Studio2DSeriesSink` | `series2d-xy-json-v1` | x/y stats, downsampled points |
| `StudioScalarSink`, `StudioStatusSink` | `scalar-status-json-v1` | values as-is |
`max_points` sets the downsampling (default 64); `raw=True` returns one stream's full payload.
`StudioAudioSink` only supports the `websocket` transport and cannot be read this way.
A power spectrum needs `fft_size * num_averages` samples (8192 by default) before its first frame.
`examples/` holds the hello world with added noise as `.gr4s` (open in gr4-studio) and `.gr4c`.
## Tests
```sh
.venv/bin/pytest -q
```
`tests/test_graph.py` runs offline against a catalog snapshot.
`tests/test_hello_world.py` drives the real server over MCP stdio: it builds the graph
above, checks via `read_stream` that a unit sine with the right period arrives at the
scope, changes the frequency live and sees it in the stream, saves and reloads both file
formats, checks spectrum/scalar/websocket sinks, and checks the backend is stopped afterwards.
`tests/test_search.py` checks `search_blocks` ranking against the full catalog (e.g. "noise"
finds AwgnChannel and PhaseNoise first). `tests/test_attach_backend.py` attaches to a separately started backend via
`attach_backend("studio")`, deploys and reads a stream there, switches back, and checks the
attached backend and its session were left alone. `tests/test_streams.py` covers the payload summaries offline, `tests/test_install.py` the
install lookup order. `tests/test_register_install.py` starts gr4-mcp where no install can
be found, registers one with `set_gr4_install`, and checks a fresh server reuses the saved folder.
## Known backend limitations
- A graph whose blocks all finished still reports `running`, and its blocks stop
answering settings requests (timeouts).
- `GET /blocks` reports a mangled `category` for templated types containing `::`;
gr4-mcp derives categories from the block id instead.
- Live settings reject arrays.
## Not yet implemented
- Studio expressions other than a plain variable name are dropped on `.gr4s` load.
## License
gr4-mcp is released under the [MIT License](LICENSE).
GNU Radio 4 itself, and the GR4 logo used in `docs/social-preview.png`, belong to the
GNU Radio project and are covered by their own licenses.
TDQS
Scored across 23 tools
Most tools have a clearly distinct target: catalog browsing (search_blocks, describe_block, list_categories), graph editing (add_block, connect, set_parameters), and runtime control (deploy_graph, control_session, read_stream). The one risky pair is set_parameters vs update_block_settings and get_block_settings vs show_graph, but their descriptions explicitly contrast graph-edit vs live-session semantics. Only minor boundary friction remains.
The dominant pattern is snake_case verb_noun (list_categories, add_block, remove_block, save_graph, deploy_graph, read_stream). Deviations are minor and readable: noun-first status tools (backend_status, session_status) and a few verbless/bare-verb names (new_graph, connect, disconnect). No casing mixing.
23 tools is on the heavy side, but the domain genuinely spans four sub-areas (block catalog, graph editing, deployment/sessions, backend install management) and each tool covers a real operation. Nothing looks redundant enough to remove, though a couple of status tools could conceivably be merged.
The lifecycle is well covered end to end: discover blocks, build/edit a graph, validate, save/load, deploy, control the session, and read live data, plus backend install and attach management. Small gaps exist around exposing graph export/serialization details or bulk session cleanup, but agents can work around them with existing tools.