""" MkDocs documentation management CLI. This script provides a proper CLI interface to: - Generate MkDocs Markdown files with mkdocstrings directives - Build the documentation site - Serve the documentation site locally All operations are performed by calling MkDocs as a Python library (no shell command invocation). Requirements: - mkdocs - mkdocs-material - mkdocstrings[python] Usage: python manage_docs.py generate python manage_docs.py build python manage_docs.py serve python manage_docs.py build_mcp Optional flags: --docs-dir PATH Path to docs directory (default: ./docs) --package-root NAME Root Python package name (default: mail_intake) """ from __future__ import annotations import argparse import json import re from pathlib import Path from typing import Iterable from mkdocs.commands import build as mkdocs_build from mkdocs.commands import serve as mkdocs_serve from mkdocs.config import load_config PROJECT_ROOT = Path(__file__).resolve().parent DEFAULT_DOCS_DIR = PROJECT_ROOT / "docs" DEFAULT_PACKAGE_ROOT = "mail_intake" MKDOCS_YML = PROJECT_ROOT / "mkdocs.yml" DEFAULT_MCP_DIR = PROJECT_ROOT / "mcp" MKDOCSTRINGS_DIRECTIVE = re.compile(r"^:::\s+([a-zA-Z0-9_.]+)", re.MULTILINE) # ------------------------- # Existing functionality # ------------------------- def generate_docs_from_nav( project_root: Path, docs_root: Path, package_root: str, ) -> None: """ Create and populate MkDocs Markdown files with mkdocstrings directives. This function: - Walks the Python package structure - Mirrors it under the docs directory - Creates missing .md files - Creates index.md for packages (__init__.py) - Overwrites content with ::: package.module Examples: mail_intake/__init__.py -> docs/mail_intake/index.md mail_intake/config.py -> docs/mail_intake/config.md mail_intake/adapters/__init__.py -> docs/mail_intake/adapters/index.md mail_intake/adapters/base.py -> docs/mail_intake/adapters/base.md """ package_dir = project_root / package_root if not package_dir.exists(): raise FileNotFoundError(f"Package not found: {package_dir}") docs_root.mkdir(parents=True, exist_ok=True) for py_file in package_dir.rglob("*.py"): rel = py_file.relative_to(project_root) if py_file.name == "__init__.py": # Package → index.md module_path = ".".join(rel.parent.parts) md_path = docs_root / rel.parent / "index.md" title = rel.parent.name.replace("_", " ").title() else: # Regular module → .md module_path = ".".join(rel.with_suffix("").parts) md_path = docs_root / rel.with_suffix(".md") title = md_path.stem.replace("_", " ").title() md_path.parent.mkdir(parents=True, exist_ok=True) content = f"""# {title} ::: {module_path} """ md_path.write_text(content, encoding="utf-8") def load_mkdocs_config(): if not MKDOCS_YML.exists(): raise FileNotFoundError("mkdocs.yml not found at project root") return load_config(str(MKDOCS_YML)) def cmd_generate(args: argparse.Namespace) -> None: generate_docs_from_nav( project_root=PROJECT_ROOT, docs_root=args.docs_dir, package_root=args.package_root, ) def cmd_build(_: argparse.Namespace) -> None: config = load_mkdocs_config() mkdocs_build.build(config) def cmd_serve(_: argparse.Namespace) -> None: mkdocs_serve.serve( config_file=str(MKDOCS_YML) ) # ------------------------- # MCP generation # ------------------------- def iter_markdown_files(docs_root: Path) -> Iterable[Path]: yield from docs_root.rglob("*.md") def extract_modules(md_file: Path) -> list[str]: content = md_file.read_text(encoding="utf-8") return MKDOCSTRINGS_DIRECTIVE.findall(content) def cmd_build_mcp(args: argparse.Namespace) -> None: docs_root = args.docs_dir mcp_root = args.mcp_dir modules_dir = mcp_root / "modules" modules_dir.mkdir(parents=True, exist_ok=True) nav = [] modules = [] for md in iter_markdown_files(docs_root): rel = md.relative_to(docs_root) module_refs = extract_modules(md) if not module_refs: continue nav.append({ "page": str(rel), "modules": module_refs, }) for module in module_refs: module_entry = { "module": module, "doc_page": str(rel), } modules.append(module_entry) out = modules_dir / f"{module}.json" out.parent.mkdir(parents=True, exist_ok=True) out.write_text( json.dumps(module_entry, indent=2), encoding="utf-8", ) mcp_root.mkdir(parents=True, exist_ok=True) (mcp_root / "nav.json").write_text( json.dumps(nav, indent=2), encoding="utf-8", ) (mcp_root / "index.json").write_text( json.dumps( { "project": "Aetoskia Mail Intake", "type": "docs-only", "modules_count": len(modules), }, indent=2, ), encoding="utf-8", ) print(f"MCP artifacts written to: {mcp_root}") # ------------------------- # CLI # ------------------------- def main() -> None: parser = argparse.ArgumentParser( prog="manage_docs.py", description="Manage MkDocs documentation and MCP exports for the project", ) parser.add_argument( "--docs-dir", type=Path, default=DEFAULT_DOCS_DIR, help="Path to the docs directory", ) parser.add_argument( "--package-root", default=DEFAULT_PACKAGE_ROOT, help="Root Python package name", ) parser.add_argument( "--mcp-dir", type=Path, default=DEFAULT_MCP_DIR, help="Output directory for MCP artifacts", ) subparsers = parser.add_subparsers(dest="command", required=True) subparsers.add_parser( "generate", help="Generate Markdown files with mkdocstrings directives", ).set_defaults(func=cmd_generate) subparsers.add_parser( "build", help="Build the MkDocs site", ).set_defaults(func=cmd_build) subparsers.add_parser( "serve", help="Serve the MkDocs site locally", ).set_defaults(func=cmd_serve) subparsers.add_parser( "build_mcp", help="Generate MCP artifacts (docs-only)", ).set_defaults( func=cmd_build_mcp ) args = parser.parse_args() args.func(args) if __name__ == "__main__": main()