"""
Generate `docs/TOOLS.md` from the authoritative tool definitions in `app/tools/`.
"""
from __future__ import annotations
import ast
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
TOOLS_DIR = ROOT / "app" / "tools"
OUT = ROOT / "docs" / "TOOLS.md"
def _is_tool(node: ast.FunctionDef) -> bool:
for dec in node.decorator_list:
if isinstance(dec, ast.Call):
if isinstance(dec.func, ast.Attribute) and dec.func.attr == "tool":
return True
elif isinstance(dec, ast.Attribute) and dec.attr == "tool":
return True
return False
def _format_default(expr: ast.AST) -> str:
try:
if isinstance(expr, ast.Constant):
return repr(expr.value)
return ast.unparse(expr)
except Exception:
return "…"
def _signature(fn: ast.FunctionDef) -> str:
args = fn.args
parts: list[str] = []
# Simple positional args
for a in args.args:
parts.append(a.arg)
# Defaults
defaults = list(args.defaults)
if defaults:
for i in range(1, len(defaults) + 1):
arg_name = args.args[-i].arg
try:
idx = parts.index(arg_name)
parts[idx] = f"{arg_name}={_format_default(defaults[-i])}"
except ValueError:
pass
return f"{fn.name}({', '.join(parts)})"
CATEGORY_ORDER = [
("Market Data", ["get_stock_price", "get_multiple_prices", "fetch_ohlcv"]),
("Trading & Execution", ["place_market_order", "place_limit_order", "place_stock_order", "start_brokerage_private_ws"]),
("Intelligence", ["get_market_sentiment", "get_market_news", "fetch_rss_news", "get_social_sentiment", "get_financial_news"]),
("Research & Evaluation", ["run_backtest_simulation", "run_synthetic_stress_test", "get_market_regime", "post_market_insight", "get_latest_insights"]),
("Safety & Governance", ["validate_trade_risk", "reset_paper_wallet", "deposit_paper_funds"]),
]
def main():
tools = {}
for py_file in TOOLS_DIR.glob("*.py"):
if py_file.name == "__init__.py": continue
tree = ast.parse(py_file.read_text(encoding="utf-8"))
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# Include it if it's explicitly in our category order list
for section, names in CATEGORY_ORDER:
if node.name in names:
tools[node.name] = node
lines = ["# ReadyTrader-Stocks MCP Tool Catalog", "", "This file is automatically generated from the tool definitions in `app/tools/`.", ""]
used = set()
for section, names in CATEGORY_ORDER:
present = [n for n in names if n in tools]
if not present:
continue
used.update(present)
lines.extend([f"## {section}", "", "| Tool Name | Description |", "| :--- | :--- |"])
for name in present:
fn = tools[name]
doc = (ast.get_docstring(fn) or "").strip()
first = doc.splitlines()[0].strip() if doc else "No description."
lines.append(f"| [`{name}`](#{name.replace('_', '-')}) | {first} |")
lines.append("")
for name in present:
fn = tools[name]
sig = _signature(fn)
doc = (ast.get_docstring(fn) or "").strip()
lines.extend([f"### `{name}`", "", f"**Signature:** `{sig}`", ""])
if doc:
lines.extend([f"```text\n{doc}\n```", ""])
lines.extend(["---", ""])
OUT.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
print(f"Wrote {OUT.relative_to(ROOT)} ({len(tools)} tools)")
if __name__ == "__main__":
main()