fold/scripts/generate_mcp_docs.py
trvon b0b51f1e71
feat: converge quality and scan interfaces
- fail closed on MCP input and compile database policy
- add shared scan catalogs, finding queries, and tiered gates
2026-07-29 00:57:33 -04:00

132 lines
4.2 KiB
Python

#!/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 = [
"<!-- Generated by scripts/generate_mcp_docs.py; do not edit manually. -->",
"# 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())