#!/usr/bin/env python3 """Generate docs/mcp-tools.md from the built Fold MCP schemas.""" from __future__ import annotations import argparse import json import subprocess from pathlib import Path from typing import Any ALIASES = { "fold.get_job_findings": "fold.scan_job_findings", "fold.activate_scan_job": "fold.scan_activate", "fold.scan_project": "fold.scan", "fold.scan_rust": "fold.scan", "fold.scan_swift": "fold.scan", "fold.scan_binary": "fold.scan", "fold.find_duplicates": "fold.scan", "fold.find_dead_code": "fold.scan", "fold.find_duplicate_classes": "fold.scan", "fold.find_security_issues": "fold.scan", "fold.find_binary_security_issues": "fold.scan", } def mcp_tools(fold_binary: Path) -> list[dict[str, Any]]: requests = [ {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}, ] result = subprocess.run( [str(fold_binary), "mcp"], input="\n".join(json.dumps(request) for request in requests) + "\n", text=True, capture_output=True, check=True, ) responses = [ json.loads(line) for line in result.stdout.splitlines() if line.strip() ] listed = next(response for response in responses if response.get("id") == 2) return listed["result"]["tools"] def cell(value: Any) -> str: if value is None: return "" if isinstance(value, (dict, list)): rendered = json.dumps(value, separators=(",", ":")) else: rendered = str(value) return rendered.replace("|", "\\|").replace("\n", " ") def render(tools: list[dict[str, Any]]) -> str: lines = [ "", "# Fold MCP tools", "", "Fold advertises only canonical operations. Deprecated aliases remain callable", "through the next major-version compatibility checkpoint and return structured", "deprecation metadata.", "", "## Canonical operations", "", "| Tool | Description |", "|---|---|", ] for tool in tools: lines.append(f"| `{tool['name']}` | {cell(tool['description'])} |") scan = next(tool for tool in tools if tool["name"] == "fold.scan") properties = scan["inputSchema"].get("properties", {}) lines.extend( [ "", "## Generated scan option schema", "", "`fold.scan` and `fold.scan_start` advertise this same analysis schema.", "Server configuration owns queue depth. `background` is accepted only as a", "deprecated, hidden `fold.scan` call alias for `fold.scan_start`.", "", "| Property | Type | Default | Constraints | Description |", "|---|---|---|---|---|", ] ) for name, schema in sorted(properties.items()): constraints = [] if "enum" in schema: constraints.append("enum=" + cell(schema["enum"])) for key in ( "minimum", "maximum", "minItems", "maxItems", "minLength", "maxLength", ): if key in schema: constraints.append(f"{key}={schema[key]}") lines.append( f"| `{name}` | `{cell(schema.get('type'))}` | {cell(schema.get('default'))} | " f"{cell(', '.join(constraints))} | {cell(schema.get('description'))} |" ) lines.extend( [ "", "## Hidden compatibility aliases", "", "| Deprecated alias | Canonical replacement |", "|---|---|", ] ) for alias, replacement in ALIASES.items(): lines.append(f"| `{alias}` | `{replacement}` |") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--fold", type=Path, default=Path("build/release/src/cli/fold")) parser.add_argument("--output", type=Path, default=Path("docs/mcp-tools.md")) args = parser.parse_args() args.output.write_text(render(mcp_tools(args.fold)), encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())