Skip to content

Architecture

GR-MCP follows a Middleware + Provider pattern that abstracts GNU Radio’s internal objects into clean, serializable models suitable for the MCP protocol.

High-Level Overview

┌─────────────────────────────────────────────────────────────────┐
│ MCP Client │
│ (Claude, Cursor, etc.) │
└─────────────────────────────────────────────────────────────────┘
│ MCP Protocol (stdio/SSE)
┌─────────────────────────────────────────────────────────────────┐
│ FastMCP App │
│ (server.py) │
└─────────────────────────────────────────────────────────────────┘
┌───────────────┴───────────────┐
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ McpPlatformProvider │ │ McpBlockDevProvider │ │ McpRuntimeProvider │
│ (~30 tools, always) │ │ (~18 tools, dynamic) │ │ (~40 tools, dynamic) │
│ │ │ │ │ │
│ • get_blocks │ │ enable_block_dev_mode │ │ enable_runtime_mode │
│ • make_block │ │ generate_sync_block │ │ launch_flowgraph │
│ • connect_blocks │ │ validate_block_code │ │ connect_to_container │
│ • validate_flowgraph │ │ generate_grc_yaml │ │ set_variable │
│ • generate_code │ │ export_block_to_oot │ │ install_oot_module │
│ • ... │ │ ... │ │ ... │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ PlatformProvider │ │ BlockDevProvider │ │ RuntimeProvider │
│ (Business Logic) │ │ (Business Logic) │ │ (Business Logic) │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ Middlewares │ │ Middlewares │ │ Middlewares │
│ │ │ │ │ │
│ • PlatformMiddleware │ │ • BlockGenerator │ │ • DockerMiddleware │
│ • FlowGraphMiddleware │ │ • ProtocolAnalyzer │ │ • XmlRpcMiddleware │
│ • BlockMiddleware │ │ • OOTExporter │ │ • ThriftMiddleware │
│ │ │ │ │ • OOTInstaller │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘
│ │ │
▼ ▼ ▼
┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐
│ GNU Radio Objects │ │ Code Generation │ │ External Services │
│ │ │ │ │ │
│ • Platform │ │ • AST validation │ │ • Docker daemon │
│ • FlowGraph │ │ • Prompt templates │ │ • XML-RPC servers │
│ • Block │ │ • GRC YAML templates │ │ • Thrift servers │
│ • Connections │ │ • OOT scaffolding │ │ • Container processes │
└───────────────────────┘ └───────────────────────┘ └───────────────────────┘

Layer Responsibilities

MCP Layer (FastMCP)

  • Handles MCP protocol communication
  • Registers tools and resources
  • Manages the server lifecycle

Provider Layer

Business logic that doesn’t know about MCP:

  • PlatformProvider: All flowgraph operations (create, edit, validate, save)
  • BlockDevProvider: Block generation, protocol analysis, OOT export
  • RuntimeProvider: All runtime operations (launch, connect, control)

Middleware Layer

Wraps external systems with validation and normalization:

  • PlatformMiddleware: Wraps GNU Radio’s Platform class
  • FlowGraphMiddleware: Wraps FlowGraph with block/connection management
  • BlockMiddleware: Wraps Block with parameter/port access
  • BlockGeneratorMiddleware: Code generation and validation for custom blocks
  • ProtocolAnalyzerMiddleware: Protocol parsing, decoder chain generation, IQ analysis
  • OOTExporterMiddleware: OOT skeleton creation, block export, GRC YAML generation
  • DockerMiddleware: Wraps Docker SDK operations
  • XmlRpcMiddleware: Wraps XML-RPC client
  • ThriftMiddleware: Wraps ControlPort Thrift client
  • OOTInstallerMiddleware: Manages OOT module builds

Models Layer

Pydantic models for serialization:

# Examples
class BlockModel(BaseModel):
name: str
key: str
state: str
class ParamModel(BaseModel):
key: str
value: str
name: str
dtype: str
class PortModel(BaseModel):
parent: str
key: str
name: str
dtype: str
direction: str

Data Flow

Flowgraph Operations

Tool Call: make_block(block_type="osmosdr_source")
McpPlatformProvider.make_block()
PlatformProvider.make_block(block_name="osmosdr_source")
FlowGraphMiddleware.add_block("osmosdr_source")
│ Creates GNU Radio Block object
│ Generates unique name
BlockMiddleware(block)
Return: "osmosdr_source_0" (str)

Runtime Operations

Tool Call: set_variable(name="freq", value=101.1e6)
McpRuntimeProvider._runtime_tools["set_variable"]
RuntimeProvider.set_variable(name="freq", value=101.1e6)
XmlRpcMiddleware.set_variable("freq", 101.1e6)
│ XML-RPC call to running flowgraph
Return: True

Dynamic Tool Registration

Both runtime and block dev tools are registered dynamically to minimize context usage:

# At startup: ~38 always-on tools (platform + mode control)
get_runtime_mode() # Check runtime status
enable_runtime_mode() # Register runtime tools
disable_runtime_mode() # Unregister runtime tools
get_block_dev_mode() # Check block dev status
enable_block_dev_mode() # Register block dev tools
disable_block_dev_mode() # Unregister block dev tools
get_client_capabilities() # Debug client info
list_client_roots() # Debug client roots
# After enable_block_dev_mode(): ~18 additional tools
generate_sync_block()
validate_block_code()
generate_grc_yaml()
export_block_to_oot()
# ...etc
# After enable_runtime_mode(): ~40 additional tools
launch_flowgraph()
connect_to_container()
set_variable()
# ...etc

Key Design Decisions

Why Middlewares?

GNU Radio objects have complex internal state and non-serializable references. Middlewares:

  1. Provide a clean interface with Pydantic models
  2. Handle validation and error formatting
  3. Manage resource lifecycle (connections, processes)

Why Providers?

Providers separate business logic from MCP registration:

  1. Testable without MCP infrastructure
  2. Reusable in non-MCP contexts
  3. Clear dependency injection

Why Dynamic Registration?

Many MCP clients include all tool descriptions in the system prompt. With 70+ tools, this wastes significant context. Dynamic registration:

  1. Starts with minimal tools for flowgraph design
  2. Expands when runtime control is needed
  3. Can contract back when done

File Organization

src/gnuradio_mcp/
├── server.py # FastMCP app entry point + Platform init
├── models.py # All Pydantic models
├── utils.py # Helper functions
├── oot_catalog.py # OOT module catalog
├── middlewares/
│ ├── platform.py # PlatformMiddleware
│ ├── flowgraph.py # FlowGraphMiddleware
│ ├── block.py # BlockMiddleware
│ ├── block_generator.py # BlockGeneratorMiddleware
│ ├── protocol_analyzer.py # ProtocolAnalyzerMiddleware
│ ├── oot_exporter.py # OOTExporterMiddleware
│ ├── docker.py # DockerMiddleware
│ ├── xmlrpc.py # XmlRpcMiddleware
│ ├── thrift.py # ThriftMiddleware
│ └── oot.py # OOTInstallerMiddleware
├── prompts/ # LLM prompt templates for block generation
└── providers/
├── base.py # PlatformProvider
├── mcp.py # McpPlatformProvider
├── block_dev.py # BlockDevProvider
├── mcp_block_dev.py # McpBlockDevProvider
├── runtime.py # RuntimeProvider
└── mcp_runtime.py # McpRuntimeProvider