Skip to content

Dynamic Tools

GR-MCP uses dynamic tool registration to minimize context usage. Runtime and block development tools are only registered when needed, keeping the tool list small during flowgraph design.

The Problem

Many MCP clients include tool descriptions in the system prompt. With 70+ tools, this can consume significant context:

70 tools × ~100 tokens/tool = ~7,000 tokens

For LLM applications, this is wasteful when you only need flowgraph design tools.

The Solution

GR-MCP splits tools into three groups:

Always Available (~38 tools)

Platform tools for flowgraph design:

  • get_blocks, make_block, remove_block
  • connect_blocks, disconnect_blocks
  • validate_flowgraph, generate_code
  • etc.

Plus mode control tools (always registered):

  • get_runtime_mode, enable_runtime_mode, disable_runtime_mode
  • get_block_dev_mode, enable_block_dev_mode, disable_block_dev_mode
  • get_client_capabilities, list_client_roots

Block Dev Tools (~18 tools)

Loaded via enable_block_dev_mode():

  • Generation: generate_sync_block, generate_basic_block, generate_interp_block, generate_decim_block
  • Validation: validate_block_code, parse_block_prompt
  • Testing: test_block_in_docker
  • Protocol: parse_protocol_spec, generate_decoder_chain
  • Signal: analyze_iq_file
  • Export: generate_grc_yaml, generate_oot_skeleton, export_block_to_oot

Runtime Tools (~40 tools)

Loaded via enable_runtime_mode():

  • Container lifecycle: launch_flowgraph, stop_flowgraph
  • XML-RPC: connect, set_variable, get_variable
  • ControlPort: connect_controlport, get_knobs, get_performance_counters
  • Coverage: collect_coverage, generate_coverage_report
  • OOT: install_oot_module, detect_oot_modules

Usage Pattern

  1. Flowgraph Design (~38 always-on tools)

    make_block(block_type="osmosdr_source")
    connect_blocks(...)
    validate_flowgraph()
    generate_code(output_dir="/tmp")
  2. Enable Block Dev (adds ~18 tools)

    enable_block_dev_mode()
    # Generate blocks, analyze protocols, export to OOT
    generate_sync_block(
    name="my_gain", description="Multiply by gain",
    input_signature=[{"dtype": "float", "vlen": 1}],
    output_signature=[{"dtype": "float", "vlen": 1}],
    )
    generate_grc_yaml(source_code="...", block_name="my_gain")
  3. Enable Runtime (adds ~40 more tools)

    enable_runtime_mode()
    # Now runtime tools are available
    launch_flowgraph(flowgraph_path="/tmp/fm.py")
    connect_to_container(name="gr-fm")
    set_variable(name="freq", value=101.1e6)
  4. Scale Down (disable either group independently)

    disable_runtime_mode() # Removes runtime tools
    disable_block_dev_mode() # Removes block dev tools

Implementation

Mode Control Tools

@self._mcp.tool
def enable_runtime_mode() -> RuntimeModeStatus:
"""Enable runtime mode, registering all runtime control tools."""
if self._runtime_enabled:
return RuntimeModeStatus(enabled=True, ...)
self._register_runtime_tools()
self._runtime_enabled = True
return RuntimeModeStatus(enabled=True, tools_registered=[...])

Dynamic Registration

def _register_runtime_tools(self):
"""Dynamically register all runtime tools."""
p = self._provider
# Connection management
self._add_tool("connect", p.connect)
self._add_tool("disconnect", p.disconnect)
self._add_tool("get_status", p.get_status)
# Variable control
self._add_tool("list_variables", p.list_variables)
self._add_tool("get_variable", p.get_variable)
self._add_tool("set_variable", p.set_variable)
# ... more tools ...
# Docker-dependent tools (only if Docker available)
if p._has_docker:
self._add_tool("launch_flowgraph", p.launch_flowgraph)
# ...
def _add_tool(self, name: str, func: Callable):
"""Add a tool and track it for later removal."""
self._mcp.add_tool(func)
self._runtime_tools[name] = func

Dynamic Unregistration

def _unregister_runtime_tools(self):
"""Remove all dynamically registered runtime tools."""
for name in list(self._runtime_tools.keys()):
self._mcp.remove_tool(name)
self._runtime_tools.clear()

Conditional Registration

Some tools depend on external services:

# Docker-dependent tools
if p._has_docker:
self._add_tool("launch_flowgraph", p.launch_flowgraph)
self._add_tool("capture_screenshot", p.capture_screenshot)
# OOT tools require Docker for building
if p._has_oot:
self._add_tool("install_oot_module", p.install_oot_module)

Checking Status

status = get_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=False,
# tools_registered=[],
# docker_available=True,
# oot_available=True
# )
enable_runtime_mode()
status = get_runtime_mode()
# Returns: RuntimeModeStatus(
# enabled=True,
# tools_registered=[
# "connect", "disconnect", "get_status",
# "list_variables", "get_variable", "set_variable",
# "start", "stop", "lock", "unlock",
# "launch_flowgraph", "list_containers",
# ...
# ],
# docker_available=True,
# oot_available=True
# )

Benefits

  1. Reduced Context — ~38 tools instead of ~90+ during design
  2. Faster Responses — Less prompt processing for simple queries
  3. Clear Separation — Design, generation, and runtime are explicit phases
  4. Independent Modes — Enable block dev and runtime separately or together
  5. Graceful Degradation — Missing Docker doesn’t break design or generation tools
  6. Discoveryget_runtime_mode() and get_block_dev_mode() show what’s available