From c793208932a6e734bfba2a02b590f3ce674878e4 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Wed, 28 Jan 2026 20:46:11 -0700 Subject: [PATCH] runtime: handle GRC servers without XML-RPC introspection GRC's SimpleXMLRPCServer uses register_instance() which doesn't expose system.listMethods. Wrap the connectivity check in a try/except so a Fault is treated as "connected" while ConnectionRefusedError still propagates. --- src/gnuradio_mcp/middlewares/thrift.py | 8 ++++---- src/gnuradio_mcp/middlewares/xmlrpc.py | 12 ++++++++++-- tests/unit/test_thrift_middleware.py | 4 +++- tests/unit/test_xmlrpc_middleware.py | 15 +++++++++++++++ 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/gnuradio_mcp/middlewares/thrift.py b/src/gnuradio_mcp/middlewares/thrift.py index 804e4e2..2d675ee 100644 --- a/src/gnuradio_mcp/middlewares/thrift.py +++ b/src/gnuradio_mcp/middlewares/thrift.py @@ -235,7 +235,9 @@ class ThriftMiddleware: units=prop.units if hasattr(prop, "units") else None, min_value=prop.min.value if prop.min else None, max_value=prop.max.value if prop.max else None, - default_value=prop.defaultvalue.value if prop.defaultvalue else None, + default_value=( + prop.defaultvalue.value if prop.defaultvalue else None + ), knob_type=knob_type, ) ) @@ -287,9 +289,7 @@ class ThriftMiddleware: avg_work_time_us=metrics.get("avg work time", 0.0), total_work_time_us=metrics.get("total work time", 0.0), avg_nproduced=metrics.get("avg nproduced", 0.0), - input_buffer_pct=self._to_list( - metrics.get("avg input % full", []) - ), + input_buffer_pct=self._to_list(metrics.get("avg input % full", [])), output_buffer_pct=self._to_list( metrics.get("avg output % full", []) ), diff --git a/src/gnuradio_mcp/middlewares/xmlrpc.py b/src/gnuradio_mcp/middlewares/xmlrpc.py index a2c12f2..bf1dc43 100644 --- a/src/gnuradio_mcp/middlewares/xmlrpc.py +++ b/src/gnuradio_mcp/middlewares/xmlrpc.py @@ -31,8 +31,16 @@ class XmlRpcMiddleware: transport = xmlrpc.client.Transport() transport.timeout = XMLRPC_TIMEOUT proxy = xmlrpc.client.ServerProxy(url, transport=transport) - # Verify connectivity - proxy.system.listMethods() + # Verify connectivity — GRC's SimpleXMLRPCServer uses + # register_instance() which doesn't enable system.listMethods. + # A Fault means the server responded (connected); only network + # errors like ConnectionRefused indicate no server. + try: + proxy.system.listMethods() + except ConnectionRefusedError: + raise # actual connectivity failure — propagate + except Exception as e: + logger.debug("Introspection unavailable (normal for GRC servers): %s", e) logger.info("Connected to XML-RPC at %s", url) return cls(proxy, url) diff --git a/tests/unit/test_thrift_middleware.py b/tests/unit/test_thrift_middleware.py index 5620908..a765814 100644 --- a/tests/unit/test_thrift_middleware.py +++ b/tests/unit/test_thrift_middleware.py @@ -125,7 +125,9 @@ class TestThriftMiddlewareVariables: """list_variables excludes performance counter knobs.""" mock_client.getKnobs.return_value = { "sig_source0::frequency": MockKnob("sig_source0::frequency", 1e6, 5), - "null_sink0::avg throughput": MockKnob("null_sink0::avg throughput", 1e9, 5), + "null_sink0::avg throughput": MockKnob( + "null_sink0::avg throughput", 1e9, 5 + ), } variables = thrift_middleware.list_variables() diff --git a/tests/unit/test_xmlrpc_middleware.py b/tests/unit/test_xmlrpc_middleware.py index 3593009..2339b15 100644 --- a/tests/unit/test_xmlrpc_middleware.py +++ b/tests/unit/test_xmlrpc_middleware.py @@ -53,6 +53,21 @@ class TestConnect: with pytest.raises(ConnectionRefusedError): XmlRpcMiddleware.connect("http://localhost:8080") + def test_connect_without_introspection(self): + """GRC servers don't enable system.listMethods — connect should still succeed.""" + from xmlrpc.client import Fault + + with patch("gnuradio_mcp.middlewares.xmlrpc.xmlrpc.client") as mock_xmlrpc: + mock_proxy = MagicMock() + mock_proxy.system.listMethods.side_effect = Fault( + 1, "method 'system.listMethods' is not supported" + ) + mock_xmlrpc.ServerProxy.return_value = mock_proxy + mock_xmlrpc.Transport.return_value = MagicMock() + + mw = XmlRpcMiddleware.connect("http://localhost:8080") + assert mw is not None + class TestConnectionInfo: def test_get_connection_info(self, xmlrpc_mw, mock_proxy):