docs-index

Read-only MCP server exposing generated docs reports (docs/public/generated-reports/*.json) and content page metadata

MCP FastMCP 2 deps

Read-only MCP server exposing generated docs reports (docs/public/generated-reports/*.json) and content page metadata

FieldValue
Namemcp-docs-index
Version0.1.0
Python>=3.13
mcp/docs-index/server.py
"""MCP server: docs-index.
Read-only access to `wagents docs generate`'s generated maintainer reports
(`docs/public/generated-reports/*.json`, mirrored from
`docs/src/content/docs/reports/*.mdx`) and lightweight content-page
metadata. No tool in this server mutates the repository.
"""
from __future__ import annotations
import json
from typing import Any
from fastmcp import FastMCP
from wagents.docs_reports import REPORT_SPECS, REPORTS_JSON_DIR, iter_content_pages, read_page_frontmatter
from wagents.mcp_shared.read_only_paths import PathNotAllowedError, read_text_within_allowlist
mcp = FastMCP("Docs Index")
_READ_ONLY = {
"readOnlyHint": True,
"destructiveHint": False,
"idempotentHint": True,
"openWorldHint": False,
}
_REPORT_SLUGS = {spec.slug for spec in REPORT_SPECS}
@mcp.tool(annotations=_READ_ONLY)
def list_reports() -> list[dict[str, str]]:
"""List every registered maintainer report (slug, title, description).
Reports are regenerated by `wagents docs generate` and are available as
both a browsable MDX page (`/reports/<slug>/`) and machine-readable JSON
via `get_report`. Use this to discover available slugs before calling
`get_report`.
"""
return [{"slug": spec.slug, "title": spec.title, "description": spec.description} for spec in REPORT_SPECS]
@mcp.tool(annotations=_READ_ONLY)
def get_report(slug: str) -> dict[str, Any]:
"""Return the generated JSON payload for one report.
*slug* must be one of the values returned by `list_reports` (e.g.
"docs-dependency-drift", "llms-txt-coverage", "site-graph-insights",
"docs-link-check", "docs-graph-snapshot"). Raises if the slug is unknown
or the report has not been generated yet — run
`uv run wagents docs generate` in the repo if the file is missing.
"""
if slug not in _REPORT_SLUGS:
raise ValueError(f"Unknown report slug={slug!r}. Call list_reports() for valid slugs.")
path = REPORTS_JSON_DIR / f"{slug}.json"
if not path.exists():
raise FileNotFoundError(
f"Report {slug!r} has not been generated yet. Run `uv run wagents docs generate` in the repo."
)
return json.loads(path.read_text(encoding="utf-8"))
@mcp.tool(annotations=_READ_ONLY)
def list_content_pages(query: str = "") -> list[dict[str, str]]:
"""List generated docs site content pages (title, description, relative path).
Pass *query* (case-insensitive substring) to filter by relative path,
title, or description; leave empty to list every page under
`docs/src/content/docs/`. Complements `skill-catalog` / `agent-catalog`
for pages outside those two catalogs (reports, harness support, CLI,
contributing, etc.).
"""
needle = query.strip().lower()
from wagents import CONTENT_DIR
results = []
for page in iter_content_pages():
fm = read_page_frontmatter(page)
rel = str(page.relative_to(CONTENT_DIR))
title = str(fm.get("title", ""))
description = str(fm.get("description", ""))
haystack = f"{rel} {title} {description}".lower()
if needle and needle not in haystack:
continue
results.append({"path": rel, "title": title, "description": description})
return sorted(results, key=lambda r: r["path"])
@mcp.tool(annotations=_READ_ONLY)
def read_content_page(relative_path: str, max_bytes: int = 200_000) -> str:
"""Return the raw MDX source of a docs content page.
*relative_path* is repo-root-relative, e.g.
"docs/src/content/docs/reports/site-graph-insights.mdx". Enforced against
the shared MCP read-only allowlist (`wagents.mcp_shared.read_only_paths`);
raises `PathNotAllowedError` for anything outside `docs/src/content/docs/`
and the other allowlisted repo surfaces.
"""
try:
return read_text_within_allowlist(relative_path, max_bytes=max_bytes)
except PathNotAllowedError as exc:
raise ValueError(str(exc)) from exc
if __name__ == "__main__":
mcp.run()

View source on GitHub