main - feat: Make some initial MCP server work

This commit is contained in:
Yoel Bassin 2025-04-26 04:18:02 +03:00
parent 61309d9b57
commit 9f96402e02
7 changed files with 1014 additions and 2 deletions

View File

@ -22,3 +22,21 @@ python -m grc
## Current Status
Currently, the project is only a port of GNURadio Companion (v3.10.12.0)
## Wanted API
1. Resources
- List available blocks
- List block params
- List current blocks
- List current connections
2. Tools
- Add block
- Remove block
- Set block params
- Add connection
- Remove connection
-[ ] use pydantic

265
fm_radio_receiver.grc Normal file
View File

@ -0,0 +1,265 @@
options:
parameters:
author: GNU Radio
catch_exceptions: 'True'
category: '[GRC Hier Blocks]'
cmake_opt: ''
comment: ''
copyright: ''
description: RTL-SDR FM Radio Receiver with frequency and volume controls
gen_cmake: 'On'
gen_linking: dynamic
generate_options: qt_gui
hier_block_src_path: '.:'
id: default
max_nouts: '0'
output_language: python
placement: (0,0)
qt_qss_theme: ''
realtime_scheduling: ''
run: 'True'
run_command: '{python} -u {filename}'
run_options: prompt
sizing_mode: fixed
thread_safe_setters: ''
title: FM Radio Receiver
window_size: 1280, 1024
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [8, 8]
rotation: 0
state: enabled
blocks:
- name: samp_rate
id: variable
parameters:
comment: ''
value: '2400000'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [200, 12]
rotation: 0
state: enabled
- name: variable_qtgui_range_0
id: variable_qtgui_range
parameters:
comment: ''
gui_hint: ''
label: Frequency
min_len: '200'
orient: QtCore.Qt.Horizontal
rangeType: float
start: '87500000'
step: '100000'
stop: '108000000'
value: '95500000'
widget: counter_slider
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: variable_qtgui_range_1
id: variable_qtgui_range
parameters:
comment: ''
gui_hint: ''
label: Volume
min_len: '200'
orient: QtCore.Qt.Horizontal
rangeType: float
start: '0'
step: '0.1'
stop: '2'
value: '1'
widget: slider
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: analog_wfm_rcv_0
id: analog_wfm_rcv
parameters:
affinity: ''
alias: ''
audio_decimation: '10'
comment: ''
maxoutbuf: '0'
minoutbuf: '0'
quad_rate: '480000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: audio_sink_0
id: audio_sink
parameters:
affinity: ''
alias: ''
comment: ''
device_name: ''
num_inputs: '1'
ok_to_block: 'True'
samp_rate: '48000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: blocks_multiply_const_vxx_0
id: blocks_multiply_const_vxx
parameters:
affinity: ''
alias: ''
comment: ''
const: variable_qtgui_range_1
maxoutbuf: '0'
minoutbuf: '0'
type: float
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: low_pass_filter_0
id: low_pass_filter
parameters:
affinity: ''
alias: ''
beta: '6.76'
comment: ''
cutoff_freq: '100000'
decim: '5'
gain: '1'
interp: '1'
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: '2400000'
type: fir_filter_ccf
width: '30000'
win: window.WIN_HAMMING
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: qtgui_freq_sink_x_0
id: qtgui_freq_sink_x
parameters:
affinity: ''
alias: ''
alpha1: '1.0'
alpha10: '1.0'
alpha2: '1.0'
alpha3: '1.0'
alpha4: '1.0'
alpha5: '1.0'
alpha6: '1.0'
alpha7: '1.0'
alpha8: '1.0'
alpha9: '1.0'
autoscale: 'False'
average: '1.0'
axislabels: 'True'
bw: '2400000'
color1: '"blue"'
color10: '"dark blue"'
color2: '"red"'
color3: '"green"'
color4: '"black"'
color5: '"cyan"'
color6: '"magenta"'
color7: '"yellow"'
color8: '"dark red"'
color9: '"dark green"'
comment: ''
ctrlpanel: 'False'
fc: variable_qtgui_range_0
fftsize: '1024'
freqhalf: 'True'
grid: 'False'
gui_hint: ''
label: Relative Gain
label1: ''
label10: ''''''
label2: ''''''
label3: ''''''
label4: ''''''
label5: ''''''
label6: ''''''
label7: ''''''
label8: ''''''
label9: ''''''
legend: 'True'
maxoutbuf: '0'
minoutbuf: '0'
name: RF Spectrum
nconnections: '1'
norm_window: 'False'
showports: 'False'
tr_chan: '0'
tr_level: '0.0'
tr_mode: qtgui.TRIG_MODE_FREE
tr_tag: '""'
type: complex
units: dB
update_time: '0.10'
width1: '1'
width10: '1'
width2: '1'
width3: '1'
width4: '1'
width5: '1'
width6: '1'
width7: '1'
width8: '1'
width9: '1'
wintype: window.WIN_BLACKMAN_hARRIS
ymax: '10'
ymin: '-140'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: soapy_rtlsdr_source_0
id: soapy_rtlsdr_source
parameters:
affinity: ''
agc: 'True'
alias: ''
bias: 'False'
bufflen: '16384'
center_freq: variable_qtgui_range_0
comment: ''
dev_args: ''
freq_correction: '0'
gain: '20'
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: '2400000'
type: fc32
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
connections:
- [analog_wfm_rcv_0, '0', blocks_multiply_const_vxx_0, '0']
- [blocks_multiply_const_vxx_0, '0', audio_sink_0, '0']
- [low_pass_filter_0, '0', analog_wfm_rcv_0, '0']
- [soapy_rtlsdr_source_0, '0', low_pass_filter_0, '0']
- [soapy_rtlsdr_source_0, '0', qtgui_freq_sink_x_0, '0']
metadata:
file_format: 1
grc_version: 3.10.12.0

View File

@ -0,0 +1,288 @@
options:
parameters:
author: ''
catch_exceptions: 'True'
category: '[GRC Hier Blocks]'
cmake_opt: ''
comment: ''
copyright: ''
description: ''
gen_cmake: 'On'
gen_linking: dynamic
generate_options: qt_gui
hier_block_src_path: '.:'
id: default
max_nouts: '0'
output_language: python
placement: (0,0)
qt_qss_theme: ''
realtime_scheduling: ''
run: 'True'
run_command: '{python} -u {filename}'
run_options: prompt
sizing_mode: fixed
thread_safe_setters: ''
title: Not titled yet
window_size: (1000,1000)
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [296, 4.0]
rotation: 0
state: enabled
blocks:
- name: freq
id: variable_qtgui_range
parameters:
comment: ''
gui_hint: ''
label: Frequency
min_len: '200'
orient: QtCore.Qt.Horizontal
rangeType: float
start: '88000000'
step: '100000'
stop: '108000000'
value: '95700000'
widget: counter_slider
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [48, 44.0]
rotation: 0
state: enabled
- name: rf_gain
id: variable_qtgui_range
parameters:
comment: ''
gui_hint: ''
label: RF Gain
min_len: '200'
orient: QtCore.Qt.Horizontal
rangeType: float
start: '0'
step: '1'
stop: '47'
value: '20'
widget: counter_slider
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [568, 116.0]
rotation: 0
state: enabled
- name: samp_rate
id: variable
parameters:
comment: ''
value: '32000'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [200, 12]
rotation: 0
state: enabled
- name: variable_qtgui_range_0
id: variable_qtgui_range
parameters:
comment: ''
gui_hint: ''
label: ''
min_len: '200'
orient: QtCore.Qt.Horizontal
rangeType: float
start: '0'
step: '1'
stop: '100'
value: '50'
widget: counter_slider
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [184, 104.0]
rotation: 0
state: enabled
- name: volume
id: variable
parameters:
comment: ''
value: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [384, 128.0]
rotation: 0
state: enabled
- name: analog_wfm_rcv_1
id: analog_wfm_rcv
parameters:
affinity: ''
alias: ''
audio_decimation: '8'
comment: ''
maxoutbuf: '0'
minoutbuf: '0'
quad_rate: '2048000'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [400, 240.0]
rotation: 0
state: enabled
- name: audio_sink_0
id: audio_sink
parameters:
affinity: ''
alias: ''
comment: ''
device_name: ''
num_inputs: '1'
ok_to_block: 'True'
samp_rate: samp_rate
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [848, 204.0]
rotation: 0
state: enabled
- name: blocks_complex_to_mag_squared_0
id: blocks_complex_to_mag_squared
parameters:
affinity: ''
alias: ''
comment: ''
maxoutbuf: '0'
minoutbuf: '0'
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [456, 484.0]
rotation: 0
state: enabled
- name: blocks_multiply_const_vxx_0
id: blocks_multiply_const_vxx
parameters:
affinity: ''
alias: ''
comment: ''
const: volume
maxoutbuf: '0'
minoutbuf: '0'
type: float
vlen: '1'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [696, 220.0]
rotation: 0
state: enabled
- name: qtgui_number_sink_0
id: qtgui_number_sink
parameters:
affinity: ''
alias: ''
autoscale: 'False'
avg: '0'
color1: ("black", "black")
color10: ("black", "black")
color2: ("black", "black")
color3: ("black", "black")
color4: ("black", "black")
color5: ("black", "black")
color6: ("black", "black")
color7: ("black", "black")
color8: ("black", "black")
color9: ("black", "black")
comment: ''
factor1: '1'
factor10: '1'
factor2: '1'
factor3: '1'
factor4: '1'
factor5: '1'
factor6: '1'
factor7: '1'
factor8: '1'
factor9: '1'
graph_type: qtgui.NUM_GRAPH_HORIZ
gui_hint: ''
label1: ''
label10: ''
label2: ''
label3: ''
label4: ''
label5: ''
label6: ''
label7: ''
label8: ''
label9: ''
max: '1'
min: '-1'
name: '""'
nconnections: '1'
type: float
unit1: ''
unit10: ''
unit2: ''
unit3: ''
unit4: ''
unit5: ''
unit6: ''
unit7: ''
unit8: ''
unit9: ''
update_time: '0.10'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [856, 320.0]
rotation: 0
state: enabled
- name: soapy_rtlsdr_source_1
id: soapy_rtlsdr_source
parameters:
affinity: ''
agc: 'False'
alias: ''
bias: 'False'
bufflen: '16384'
center_freq: freq
comment: ''
dev_args: ''
freq_correction: '0'
gain: rf_gain
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: '2048000'
type: fc32
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [88, 352.0]
rotation: 0
state: enabled
connections:
- [analog_wfm_rcv_1, '0', blocks_multiply_const_vxx_0, '0']
- [blocks_complex_to_mag_squared_0, '0', qtgui_number_sink_0, '0']
- [blocks_multiply_const_vxx_0, '0', audio_sink_0, '0']
- [soapy_rtlsdr_source_1, '0', analog_wfm_rcv_1, '0']
- [soapy_rtlsdr_source_1, '0', blocks_complex_to_mag_squared_0, '0']
metadata:
file_format: 1
grc_version: 3.10.12.0

126
fm_receiver.grc Normal file
View File

@ -0,0 +1,126 @@
options:
parameters:
author: ''
catch_exceptions: 'True'
category: '[GRC Hier Blocks]'
cmake_opt: ''
comment: ''
copyright: ''
description: ''
gen_cmake: 'On'
gen_linking: dynamic
generate_options: qt_gui
hier_block_src_path: '.:'
id: default
max_nouts: '0'
output_language: python
placement: (0,0)
qt_qss_theme: ''
realtime_scheduling: ''
run: 'True'
run_command: '{python} -u {filename}'
run_options: prompt
sizing_mode: fixed
thread_safe_setters: ''
title: Not titled yet
window_size: (1000,1000)
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [8, 8]
rotation: 0
state: enabled
blocks:
- name: samp_rate
id: variable
parameters:
comment: ''
value: '32000'
states:
bus_sink: false
bus_source: false
bus_structure: null
coordinate: [200, 12]
rotation: 0
state: enabled
- name: analog_wfm_rcv_0
id: analog_wfm_rcv
parameters:
affinity: ''
alias: ''
audio_decimation: '10'
comment: ''
maxoutbuf: '0'
minoutbuf: '0'
quad_rate: '480000'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: audio_sink_0
id: audio_sink
parameters:
affinity: ''
alias: ''
comment: ''
device_name: ''
num_inputs: '1'
ok_to_block: 'True'
samp_rate: '44100'
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: rational_resampler_xxx_0
id: rational_resampler_xxx
parameters:
affinity: ''
alias: ''
comment: ''
decim: '480'
fbw: '0'
interp: '441'
maxoutbuf: '0'
minoutbuf: '0'
taps: '[]'
type: fff
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
- name: soapy_rtlsdr_source_0
id: soapy_rtlsdr_source
parameters:
affinity: ''
agc: 'False'
alias: ''
bias: 'False'
bufflen: '16384'
center_freq: '95500000'
comment: ''
dev_args: ''
freq_correction: '0'
gain: '20'
maxoutbuf: '0'
minoutbuf: '0'
samp_rate: '2457600'
type: fc32
states:
bus_sink: false
bus_source: false
bus_structure: null
state: enabled
connections:
- [analog_wfm_rcv_0, '0', rational_resampler_xxx_0, '0']
- [rational_resampler_xxx_0, '0', audio_sink_0, '0']
- [soapy_rtlsdr_source_0, '0', analog_wfm_rcv_0, '0']
metadata:
file_format: 1
grc_version: 3.10.12.0

View File

@ -69,6 +69,71 @@ log = logging.getLogger('grc')
# The default console logging should be WARNING
log.setLevel(logging.DEBUG)
def run_api(args, log):
from .core.platform import Platform
from .core.FlowGraph import FlowGraph
platform = Platform(
version=gr.version(),
version_parts=(gr.major_version(), gr.api_version(), gr.minor_version()),
prefs=gr.prefs(),
# install_prefix=gr.prefix()
)
platform.build_library()
from .mcp.mcp import CoreMiddleware
mcp = CoreMiddleware(platform, "")
from fastmcp import FastMCP
app = FastMCP("Demo 🚀")
app.tool()(mcp.list_all_blocks)
app.tool()(mcp.list_block_input_data)
app.tool()(mcp.list_block_output_data)
app.tool()(mcp.list_block_parameters_data)
app.tool()(mcp.get_placed_blocks)
app.tool()(mcp.get_placed_connections)
app.tool()(mcp.get_placed_block_params)
app.tool()(mcp.validate_block)
app.tool()(mcp.validate_connection)
app.tool()(mcp.validate_flowgraph)
app.tool()(mcp.get_all_errors)
app.tool()(mcp.add_block)
app.tool()(mcp.remove_block)
app.tool()(mcp.connect_blocks)
app.tool()(mcp.update_block_params)
app.tool()(mcp.save_flowgraph)
app.run(transport='sse')
# print(mcp.flowgraph_blocks())
# print(mcp.flowgraph_connections())
# initial_state = platform.parse_flow_graph("")
# fg = FlowGraph(platform)
# fg.import_data(initial_state)
# source = fg.new_block('analog_const_source_x')
# source.params['id'].set_value('test1')
# sink = fg.new_block('virtual_sink')
# sink.params['id'].set_value('test2')
# conn = fg.connect(source.sources[0], sink.sinks[0])
# platform.save_flow_graph('test.grc', fg)
# mcp.add_block('analog_const_source_x')
# mcp.add_block('virtual_sink')
# mcp.connect_blocks('analog_const_source_x_0', 0, 'virtual_sink_0', 0)
def run_gtk(args, log):
''' Runs the GTK version of GNU Radio Companion '''
@ -182,7 +247,6 @@ def run_qt(args, log):
install_prefix=gr.prefix()
)
model.build_library()
# Launch GRC
app = grc.Application(settings, model, args.flow_graphs)
sys.exit(app.run())
@ -285,6 +349,8 @@ def main():
help="GNU Radio Companion (QT)")
gui_group_exclusive.add_argument("--gtk", dest='framework', action='store_const', const='gtk',
help="GNU Radio Companion (GTK)")
gui_group_exclusive.add_argument("--api", dest='framework', action='store_const', const='api',
help="GNU Radio Companion (API)")
# Default options if not already set with add_argument()
args = parser.parse_args()
@ -316,6 +382,8 @@ def main():
run_qt(args, log)
elif args.framework == 'gtk':
run_gtk(args, log)
elif args.framework == 'api':
run_api(args, log)
else: # args.framework == None
if grc_version_from_config == 'grc_qt':
run_qt(args, log)

0
grc/mcp/__init__.py Normal file
View File

247
grc/mcp/mcp.py Normal file
View File

@ -0,0 +1,247 @@
from itertools import count
import re
from typing import Any, Dict, List, Optional, Set
from grc.core.FlowGraph import FlowGraph
from grc.core.blocks.block import Block
from grc.core.params.param import Param
from grc.core.Connection import Connection
from grc.core.platform import Platform
from grc.core.ports.port import Port
class CoreMiddleware:
def __init__(self, platform: Platform, filepath: Optional[str] = ""):
self._platform = platform
initial_state = platform.parse_flow_graph(filepath)
self._flowgraph = FlowGraph(platform)
self._flowgraph.import_data(initial_state)
##############################################
# Static Data
##############################################
def list_all_blocks(self) -> List[Dict[str, Any]]:
blocks_data = []
for block in self._platform.blocks.values():
blocks_data.append(
{
"key": block.key,
"label": block.label,
}
)
return blocks_data
def list_block_parameters_data(self, block_key: str) -> List[str]:
block = self._platform.blocks[block_key]
return block.parameters_data
def list_block_output_data(self, block_key: str) -> List[Dict[str, Any]]:
block = self._platform.blocks[block_key]
return block.outputs_data
def list_block_input_data(self, block_key: str) -> List[Dict[str, Any]]:
block = self._platform.blocks[block_key]
return block.inputs_data
##############################################
# Flowgraph Data
##############################################
def get_placed_blocks(self) -> List[Dict[str, Any]]:
blocks_data = []
for block in self._flowgraph.blocks:
blocks_data.append(
{
"key": block.key,
"name": block.params["id"].get_value(),
}
)
return blocks_data
def get_placed_block_params(self, block_name: str) -> Dict[str, Any]:
block = self._flowgraph.get_block(block_name)
return {param.key: param.get_value() for param in block.params.values()}
def get_placed_connections(self) -> Set[Dict[str, Any]]:
connections_data = []
for connection in self._flowgraph.connections:
connection: Connection = connection
source_port: Port = connection.source_port
sink_port: Port = connection.sink_port
connections_data.append(
{
"src_block": source_port.parent.key,
"src_port": source_port.key,
"dst_block": sink_port.parent.key,
"dst_port": sink_port.key,
}
)
return connections_data
##############################################
# Flowgraph Operations
##############################################
def add_block(self, block_key: str, name: Optional[str] = None) -> Block:
name = name or self._get_unique_id(block_key)
block = self._flowgraph.new_block(block_key)
block.params["id"].set_value(name)
return "success"
def remove_block(self, block_name: str) -> None:
block = self._flowgraph.get_block(block_name)
self._flowgraph.remove_element(block)
return "success"
def connect_blocks(
self,
src_block_name: str,
src_port_index: int,
dst_block_name: str,
dst_port_index: int,
) -> Connection:
src_block = self._flowgraph.get_block(src_block_name)
dst_block = self._flowgraph.get_block(dst_block_name)
self._flowgraph.connect(
src_block.sources[src_port_index], dst_block.sinks[dst_port_index]
)
return "success"
def disconnect_blocks(
self,
src_block_name: str,
src_port_index: int,
dst_block_name: str,
dst_port_index: int,
) -> None:
src_block = self._flowgraph.get_block(src_block_name)
dst_block = self._flowgraph.get_block(dst_block_name)
self._flowgraph.disconnect(
src_block.sources[src_port_index], dst_block.sinks[dst_port_index]
)
return "success"
def update_block_params(self, block_name: str, params: Dict[str, Any]) -> None:
block = self._flowgraph.get_block(block_name)
for param_name, param_value in params.items():
block.params[param_name].set_value(param_value)
return "success"
##############################################
# Flowgraph Validation
##############################################
def validate_block(self, block_name: str) -> bool:
self._flowgraph.rewrite()
block = self._flowgraph.get_block(block_name)
block.validate()
return block.is_valid()
def validate_connection(
self,
src_block_name: str,
src_port_index: int,
dst_block_name: str,
dst_port_index: int,
) -> bool:
self._flowgraph.rewrite()
connections: Set[Connection] = self._flowgraph.connections
for connection in connections:
if (
connection.source_port.parent.key == src_block_name
and connection.sink_port.parent.key == dst_block_name
and connection.source_port.key == src_port_index
and connection.sink_port.key == dst_port_index
):
connection.validate()
return connection.is_valid()
raise ValueError("Connection not found")
def validate_flowgraph(self) -> bool:
self._flowgraph.rewrite()
self._flowgraph.validate()
return self._flowgraph.is_valid()
def get_all_errors(self) -> List[Dict[str, Any]]:
self._flowgraph.rewrite()
self._flowgraph.validate()
errors = []
for elem, msg in self._flowgraph.iter_error_messages():
msg = re.sub("[^A-Za-z0-9]+", " ", msg).strip()
if isinstance(elem, Connection):
connection: Connection = elem
source_port: Port = elem.source_port
sink_port: Port = elem.sink_port
errors.append(
{
"type": "connection",
"key": {
"src_block": source_port.parent.key,
"src_port": source_port.key,
"dst_block": sink_port.parent.key,
"dst_port": sink_port.key,
},
"message": msg,
}
)
if isinstance(elem, Param):
errors.append(
{
"type": "param",
"key": f"{elem.parent.params["id"].get_value()}:{elem.key}",
"message": msg,
}
)
if isinstance(elem, Port):
errors.append(
{
"type": "port",
"key": f"{elem.parent.params["id"].get_value()}:{elem.key}",
"message": msg,
}
)
if isinstance(elem, Block):
errors.append(
{
"type": "block",
"key": elem.params["id"].get_value(),
"message": msg,
}
)
return errors
##############################################
# Misc
##############################################
def save_flowgraph(self, filepath: str) -> None:
self._platform.save_flow_graph(filepath, self._flowgraph)
return "success"
##############################################
# Helper Functions
##############################################
def _get_unique_id(self, base_id=""):
"""
Get a unique id starting with the base id.
Args:
base_id: the id starts with this and appends a count
Returns:
a unique id
"""
block_ids = set(b.name for b in self._flowgraph.blocks)
for index in count():
block_id = "{}_{}".format(base_id, index)
if block_id not in block_ids:
break
return block_id