Replace ue-api skill with MCP server, support multi-directory input

- Add ue_mcp_server.py: 5 MCP tools (search_types, get_class_overview,
  get_member, get_file, search_source) for item-granularity doc lookups
- Add .mcp.json: registers server with Claude Code via stdio transport
- Remove .claude/skills/ue-api/SKILL.md: superseded by MCP tools
- Update generate.py: accept multiple input dirs/files, last arg is output;
  track per-file base path for correct relative output paths
- Remove samples/: headers deleted (corpus regenerated externally)
- Add possess_flow.png: sequence diagram of UE Possess flow with server/client split

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 08:20:04 -05:00
parent 904277e9e5
commit 241b246e9d
8 changed files with 271 additions and 1795 deletions

View File

@@ -3,9 +3,10 @@
generate.py — CLI for UnrealDocGenerator.
Usage:
python generate.py <input> <output_dir>
python generate.py <input> [input2 ...] <output_dir>
<input> can be a single .h file or a directory (processed recursively).
Each <input> can be a single .h file or a directory (processed recursively).
The last argument is always the output directory.
Two-pass pipeline:
Pass 1 — parse every header, build a corpus-wide type index
@@ -20,24 +21,36 @@ from ue_parser import parse_header, ParsedHeader
from ue_markdown import render_header
# ---------------------------------------------------------------------------
# Input collection
# ---------------------------------------------------------------------------
def collect_headers(input_arg: Path) -> list[tuple[Path, Path]]:
"""
Returns a list of (header_path, base_path) pairs for the given input.
base_path is used to compute relative output paths.
"""
if input_arg.is_file():
return [(input_arg, input_arg.parent)]
elif input_arg.is_dir():
return [(h, input_arg) for h in sorted(input_arg.rglob('*.h'))]
else:
print(f"Error: {input_arg} is not a file or directory", file=sys.stderr)
return []
# ---------------------------------------------------------------------------
# Type index
# ---------------------------------------------------------------------------
def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]],
input_base: Path) -> dict[str, str]:
def build_type_index(parsed_list: list[tuple[Path, Path, ParsedHeader]]) -> dict[str, str]:
"""
Returns {TypeName: md_path_relative_to_docs_root} for every
class, struct, enum, and delegate in the corpus.
"""
index: dict[str, str] = {}
for h, parsed in parsed_list:
try:
rel = h.relative_to(input_base)
except ValueError:
rel = Path(h.name)
md_rel = str(rel.with_suffix('.md'))
for h, base, parsed in parsed_list:
md_rel = _md_rel(h, base)
for ci in parsed.classes:
index[ci.name] = md_rel
for ei in parsed.enums:
@@ -48,6 +61,15 @@ def build_type_index(parsed_list: list[tuple[Path, ParsedHeader]],
return index
def _md_rel(h: Path, base: Path) -> str:
"""Relative .md path for header h given its input base."""
try:
rel = h.relative_to(base)
except ValueError:
rel = Path(h.name)
return str(rel.with_suffix('.md'))
# ---------------------------------------------------------------------------
# Type index file
# ---------------------------------------------------------------------------
@@ -72,51 +94,43 @@ def write_type_index(type_index: dict[str, str], output_dir: Path) -> None:
def main():
if len(sys.argv) < 3:
print("Usage: python generate.py <input> <output_dir>", file=sys.stderr)
print("Usage: python generate.py <input> [input2 ...] <output_dir>", file=sys.stderr)
sys.exit(1)
input_arg = Path(sys.argv[1])
output_dir = Path(sys.argv[2])
*input_args, output_arg = sys.argv[1:]
output_dir = Path(output_arg)
output_dir.mkdir(parents=True, exist_ok=True)
# Collect input files
if input_arg.is_file():
headers = [input_arg]
input_base = input_arg.parent
elif input_arg.is_dir():
headers = sorted(input_arg.rglob('*.h'))
input_base = input_arg
else:
print(f"Error: {input_arg} is not a file or directory", file=sys.stderr)
sys.exit(1)
# Collect (header, base) pairs from all inputs
header_pairs: list[tuple[Path, Path]] = []
for arg in input_args:
pairs = collect_headers(Path(arg))
if not pairs:
print(f"Warning: no .h files found in {arg}", file=sys.stderr)
header_pairs.extend(pairs)
if not headers:
if not header_pairs:
print("No .h files found.", file=sys.stderr)
sys.exit(1)
# --- Pass 1: parse all ---
parsed_list: list[tuple[Path, ParsedHeader]] = []
for h in headers:
parsed_list: list[tuple[Path, Path, ParsedHeader]] = []
for h, base in header_pairs:
print(f"Parsing {h} ...")
try:
parsed = parse_header(str(h))
parsed_list.append((h, parsed))
parsed_list.append((h, base, parsed))
except Exception as exc:
print(f" ERROR parsing {h}: {exc}", file=sys.stderr)
# --- Build corpus-wide type index ---
type_index = build_type_index(parsed_list, input_base)
type_index = build_type_index(parsed_list)
# --- Pass 2: render all ---
success = 0
for h, parsed in parsed_list:
for h, base, parsed in parsed_list:
print(f"Rendering {h} ...")
try:
rel = h.relative_to(input_base)
except ValueError:
rel = Path(h.name)
current_md = str(rel.with_suffix('.md'))
current_md = _md_rel(h, base)
out_path = output_dir / current_md
out_path.parent.mkdir(parents=True, exist_ok=True)