gr-mcp/docs/scripts/generate-api-docs.py
Ryan Malloy 8800d35fd4 add Starlight docs site, LoRa examples, and clean up .gitignore
- Starlight docs: 28 pages covering getting started, guides, tool
  reference, concepts (architecture, dynamic tools, runtime comms)
- LoRa examples: channel scanner, quality analyzer, multi-SF receiver
  with both .grc and .py forms, plus ADSB+LoRa combo test
- .gitignore: exclude generated artifacts (*_patched_*.py, *.wav,
  docs build cache, tests/scratch/)
- Add .mcp.json for local MCP server config
- Sync uv.lock with date-based version
2026-02-24 09:34:50 -07:00

356 lines
10 KiB
Python

#!/usr/bin/env python3
"""Generate MDX documentation from GR-MCP tool docstrings.
Usage:
cd docs
python scripts/generate-api-docs.py
This script introspects the PlatformProvider and RuntimeProvider classes
to extract tool signatures and docstrings, then generates MDX files for
the Starlight documentation site.
"""
import inspect
import sys
from pathlib import Path
from typing import get_type_hints
# Add project root to path
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# Import providers
from gnuradio_mcp.providers.base import PlatformProvider
from gnuradio_mcp.providers.runtime import RuntimeProvider
OUTPUT_DIR = Path(__file__).parent.parent / "src/content/docs/reference/tools"
# Tool categorization
TOOL_CATEGORIES = {
"flowgraph": {
"title": "Flowgraph Tools",
"description": "Tools for managing flowgraph structure: blocks, connections, save/load.",
"tools": [
"get_blocks",
"make_block",
"remove_block",
"save_flowgraph",
"load_flowgraph",
"get_flowgraph_options",
"set_flowgraph_options",
"export_flowgraph_data",
"import_flowgraph_data",
],
},
"blocks": {
"title": "Block Tools",
"description": "Tools for block parameter and port management.",
"tools": [
"get_block_params",
"set_block_params",
"get_block_sources",
"get_block_sinks",
"bypass_block",
"unbypass_block",
],
},
"connections": {
"title": "Connection Tools",
"description": "Tools for wiring blocks together.",
"tools": [
"get_connections",
"connect_blocks",
"disconnect_blocks",
],
},
"validation": {
"title": "Validation Tools",
"description": "Tools for checking flowgraph validity.",
"tools": [
"validate_block",
"validate_flowgraph",
"get_all_errors",
],
},
"platform": {
"title": "Platform Tools",
"description": "Tools for discovering available blocks and managing OOT paths.",
"tools": [
"get_all_available_blocks",
"search_blocks",
"get_block_categories",
"load_oot_blocks",
"add_block_path",
"get_block_paths",
],
},
"codegen": {
"title": "Code Generation",
"description": "Tools for generating Python code and evaluating expressions.",
"tools": [
"generate_code",
"evaluate_expression",
"create_embedded_python_block",
],
},
"runtime-mode": {
"title": "Runtime Mode",
"description": "Tools for enabling/disabling runtime features and checking client capabilities.",
"tools": [
"get_runtime_mode",
"enable_runtime_mode",
"disable_runtime_mode",
"get_client_capabilities",
"list_client_roots",
],
},
"docker": {
"title": "Docker Tools",
"description": "Tools for container lifecycle management.",
"tools": [
"launch_flowgraph",
"list_containers",
"stop_flowgraph",
"remove_flowgraph",
"capture_screenshot",
"get_container_logs",
],
},
"xmlrpc": {
"title": "XML-RPC Tools",
"description": "Tools for XML-RPC connection and variable control.",
"tools": [
"connect",
"connect_to_container",
"disconnect",
"get_status",
"list_variables",
"get_variable",
"set_variable",
"start",
"stop",
"lock",
"unlock",
],
},
"controlport": {
"title": "ControlPort Tools",
"description": "Tools for ControlPort/Thrift connection and monitoring.",
"tools": [
"connect_controlport",
"connect_to_container_controlport",
"disconnect_controlport",
"get_knobs",
"set_knobs",
"get_knob_properties",
"get_performance_counters",
"post_message",
],
},
"coverage": {
"title": "Coverage Tools",
"description": "Tools for collecting Python code coverage from containers.",
"tools": [
"collect_coverage",
"generate_coverage_report",
"combine_coverage",
"delete_coverage",
],
},
"oot": {
"title": "OOT Tools",
"description": "Tools for OOT module detection and installation.",
"tools": [
"detect_oot_modules",
"install_oot_module",
"list_oot_images",
"remove_oot_image",
"build_multi_oot_image",
"list_combo_images",
"remove_combo_image",
],
},
}
def get_method_info(method):
"""Extract signature and docstring from a method."""
sig = inspect.signature(method)
doc = inspect.getdoc(method) or "No description available."
# Parse docstring sections
lines = doc.split("\n")
description = []
args = []
returns = ""
example = []
section = "description"
for line in lines:
stripped = line.strip()
if stripped.startswith("Args:"):
section = "args"
continue
elif stripped.startswith("Returns:"):
section = "returns"
continue
elif stripped.startswith("Example:") or stripped.startswith("Examples:"):
section = "example"
continue
if section == "description":
description.append(line)
elif section == "args":
args.append(line)
elif section == "returns":
returns += line + "\n"
elif section == "example":
example.append(line)
# Get parameter info from signature
params = []
for name, param in sig.parameters.items():
if name == "self":
continue
param_type = ""
if param.annotation != inspect.Parameter.empty:
param_type = str(param.annotation).replace("typing.", "")
default = ""
if param.default != inspect.Parameter.empty:
default = repr(param.default)
params.append({
"name": name,
"type": param_type,
"default": default,
})
# Get return type
return_type = ""
try:
hints = get_type_hints(method)
if "return" in hints:
return_type = str(hints["return"]).replace("typing.", "")
except Exception:
pass
return {
"name": method.__name__,
"description": "\n".join(description).strip(),
"params": params,
"returns": returns.strip(),
"return_type": return_type,
"args_doc": "\n".join(args).strip(),
"example": "\n".join(example).strip(),
}
def get_all_methods():
"""Get all tool methods from both providers."""
methods = {}
# Get PlatformProvider methods
for name, method in inspect.getmembers(PlatformProvider, predicate=inspect.isfunction):
if not name.startswith("_"):
methods[name] = get_method_info(method)
# Get RuntimeProvider methods
for name, method in inspect.getmembers(RuntimeProvider, predicate=inspect.isfunction):
if not name.startswith("_"):
methods[name] = get_method_info(method)
return methods
def generate_mdx(category_key: str, category: dict, methods: dict) -> str:
"""Generate MDX content for a category."""
lines = [
"---",
f'title: {category["title"]}',
f'description: {category["description"]}',
"---",
"",
f'{category["description"]}',
"",
]
for tool_name in category["tools"]:
if tool_name not in methods:
lines.append(f"## `{tool_name}`")
lines.append("")
lines.append("*Documentation pending.*")
lines.append("")
continue
info = methods[tool_name]
lines.append(f"## `{tool_name}`")
lines.append("")
lines.append(info["description"])
lines.append("")
# Parameters table
if info["params"]:
lines.append("### Parameters")
lines.append("")
lines.append("| Name | Type | Default | Description |")
lines.append("|------|------|---------|-------------|")
for param in info["params"]:
default = param["default"] if param["default"] else "-"
ptype = param["type"] if param["type"] else "-"
# Extract description from args_doc if available
desc = "-"
if info["args_doc"]:
for arg_line in info["args_doc"].split("\n"):
if arg_line.strip().startswith(f"{param['name']}:"):
desc = arg_line.split(":", 1)[1].strip()
break
lines.append(f"| `{param['name']}` | `{ptype}` | `{default}` | {desc} |")
lines.append("")
# Returns
if info["returns"] or info["return_type"]:
lines.append("### Returns")
lines.append("")
if info["return_type"]:
lines.append(f"**Type:** `{info['return_type']}`")
lines.append("")
if info["returns"]:
lines.append(info["returns"])
lines.append("")
# Example
if info["example"]:
lines.append("### Example")
lines.append("")
lines.append("```python")
lines.append(info["example"])
lines.append("```")
lines.append("")
lines.append("---")
lines.append("")
return "\n".join(lines)
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
print("Extracting methods from providers...")
methods = get_all_methods()
print(f"Found {len(methods)} methods")
for category_key, category in TOOL_CATEGORIES.items():
print(f"Generating {category_key}.mdx...")
content = generate_mdx(category_key, category, methods)
output_path = OUTPUT_DIR / f"{category_key}.mdx"
output_path.write_text(content)
print(f" Wrote {output_path}")
print("\nDone! Generated MDX files in:")
print(f" {OUTPUT_DIR}")
if __name__ == "__main__":
main()