Files
mail-intake/manage_docs.py
Vishesh 'ironeagle' Bangotra 6a4aece659 feat(docs): add MCP artifact generation to manage_docs CLI
- Introduced `build_mcp` command to generate docs-only MCP artifacts
- Reuses existing MkDocs + mkdocstrings pipeline (no code introspection)
- Extracts `:::` mkdocstrings directives from generated Markdown
- Emits structured MCP output:
  - `mcp/index.json` (project metadata)
  - `mcp/nav.json` (page → module mapping)
  - `mcp/modules/*.json` (per-module references)
- Preserves all existing commands and behavior (`generate`, `build`, `serve`)
- Avoids source code exposure; MCP output is derived solely from documentation

This enables a clean docs → MCP → MCP server workflow suitable for AI IDE integration.
2026-01-19 18:22:59 +05:30

262 lines
6.6 KiB
Python

"""
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 → <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()