90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
"""Documentation synchronization checks for MCP tool catalog."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from pathlib import Path
|
|
|
|
from mcp_server.tools import DocsPath, tool_catalog
|
|
|
|
_REFERENCE_DOC = DocsPath / "reference" / "mcp-server.md"
|
|
|
|
|
|
def check_catalog_parity() -> dict[str, object]:
|
|
"""Compare documented anchors with the live tool catalog and report drift."""
|
|
missing_in_docs: list[str] = []
|
|
missing_in_catalog: list[str] = []
|
|
mismatches: list[dict[str, str]] = []
|
|
|
|
docs_tools = _doc_tool_anchors()
|
|
catalog = tool_catalog()
|
|
catalog_names = {tool.name for tool in catalog}
|
|
for tool in catalog:
|
|
if not _anchor_exists(tool.docs_anchor.path, tool.docs_anchor.anchor):
|
|
missing_in_docs.append(tool.name)
|
|
for anchor_key, tool_name in docs_tools.items():
|
|
if tool_name not in catalog_names:
|
|
missing_in_catalog.append(anchor_key)
|
|
return {
|
|
"status": (
|
|
"ok"
|
|
if not missing_in_docs and not missing_in_catalog and not mismatches
|
|
else "drift_detected"
|
|
),
|
|
"missingInDocs": missing_in_docs,
|
|
"missingInCatalog": missing_in_catalog,
|
|
"mismatches": mismatches,
|
|
}
|
|
|
|
|
|
def _doc_tool_anchors() -> dict[str, str]:
|
|
"""Derive documented tool anchors from the MCP reference page."""
|
|
anchors: dict[str, str] = {}
|
|
for heading in _heading_texts(_REFERENCE_DOC, prefix="### "):
|
|
tool_name = heading
|
|
anchor_id = _slugify_heading(heading)
|
|
anchors[_anchor_key(_REFERENCE_DOC, anchor_id)] = tool_name
|
|
return anchors
|
|
|
|
|
|
def _anchor_exists(path: Path, anchor: str) -> bool:
|
|
"""Check whether a heading anchor exists in the target doc."""
|
|
if not path.exists() or path.is_dir():
|
|
return False
|
|
return anchor in _heading_anchors(path)
|
|
|
|
|
|
def _heading_anchors(path: Path) -> set[str]:
|
|
"""Collect slugified heading anchors from a markdown file."""
|
|
anchors: set[str] = set()
|
|
for heading in _heading_texts(path, prefix="#"):
|
|
anchors.add(_slugify_heading(heading))
|
|
return anchors
|
|
|
|
|
|
def _heading_texts(path: Path, prefix: str) -> list[str]:
|
|
"""Return heading text lines matching a prefix."""
|
|
if not path.exists():
|
|
return []
|
|
headings: list[str] = []
|
|
for line in path.read_text().splitlines():
|
|
if not line.startswith(prefix):
|
|
continue
|
|
heading = line.lstrip("#").strip()
|
|
if heading:
|
|
headings.append(heading)
|
|
return headings
|
|
|
|
|
|
def _slugify_heading(text: str) -> str:
|
|
"""Normalize heading text to a GitHub-style anchor."""
|
|
slug = text.strip().lower()
|
|
slug = re.sub(r"[^a-z0-9\s-]", " ", slug)
|
|
slug = re.sub(r"\s+", "-", slug)
|
|
return re.sub(r"-+", "-", slug).strip("-")
|
|
|
|
|
|
def _anchor_key(path: Path, anchor: str) -> str:
|
|
"""Build a stable key for an anchor path pair."""
|
|
return f"{path}#{anchor}"
|