from __future__ import annotations
import json
import os
from pathlib import Path
from typing import Any, Literal
from mcp.server.fastmcp import FastMCP
from .config import Settings
from .docs_repository import DocsRepository
from .freshness import build_freshness_report
from .git_adapter import CommitRequest, GitAdapter
from .inventory import InventoryRepository
from .pending_changes import PendingChangeRepository, ProposedChangeRequest
from .relationships import build_relationships
from .validation import validate_repository
def _settings() -> Settings:
repo_root = os.environ.get("GNEXUS_BOOK_REPO_ROOT")
return Settings() if not repo_root else Settings(repo_root=Path(repo_root))
def _docs() -> DocsRepository:
return DocsRepository(_settings())
def _inventory() -> InventoryRepository:
return InventoryRepository(_settings())
def _pending() -> PendingChangeRepository:
return PendingChangeRepository(_settings())
def _git() -> GitAdapter:
return GitAdapter(_settings())
def _json(data: Any) -> str:
return json.dumps(data, ensure_ascii=False, indent=2)
def search_docs(query: str) -> list[dict[str, object]]:
"""Search Markdown documents by plain text query."""
return _docs().search(query)
def read_doc(path: str) -> dict[str, object]:
"""Read a Markdown document by repository-relative path."""
return _docs().read_doc(path)
def list_docs() -> list[dict[str, object]]:
"""List Markdown documents with titles and frontmatter."""
return _docs().list_docs()
def list_inventory(inventory_type: str | None = None) -> object:
"""List inventory types or read one parsed inventory type."""
repo = _inventory()
if inventory_type:
return repo.read_parsed(inventory_type)
return repo.list_types()
def get_inventory_item(inventory_type: str, item_id: str) -> dict[str, Any]:
"""Read a single inventory item by type and id."""
return _inventory().read_item(inventory_type, item_id)
def get_relationships() -> dict[str, object]:
"""Build current inventory relationship graph."""
return build_relationships(_inventory())
def validate_repository_tool() -> dict[str, object]:
"""Validate schemas, docs, inventory links, ids, and secret patterns."""
return validate_repository(_settings())
def check_freshness() -> dict[str, object]:
"""Return documentation freshness report."""
return build_freshness_report(_settings())
def git_status() -> dict[str, Any]:
"""Return local Git status for the documentation repository."""
return _git().status()
def git_diff(files: list[str] | None = None) -> dict[str, str]:
"""Return local Git diff, optionally limited to repository-relative files."""
return _git().diff(files)
def list_pending_changes() -> list[dict[str, Any]]:
"""List pending/applied/rejected change records."""
return _pending().list()
def propose_doc_change(path: str, content: str, summary: str, reason: str = "") -> dict[str, Any]:
"""Create a pending Markdown document change."""
request = ProposedChangeRequest(
kind="doc",
target=path,
summary=summary,
reason=reason,
payload={"content": content},
)
return _pending().create(request)
def propose_inventory_item_change(
inventory_type: str,
item_id: str,
patch: dict[str, Any],
summary: str,
reason: str = "",
mode: Literal["update", "create"] = "update",
) -> dict[str, Any]:
"""Create a pending inventory item patch or creation request."""
request = ProposedChangeRequest(
kind="inventory-item",
target=f"{inventory_type}/{item_id}",
summary=summary,
reason=reason,
payload={"mode": mode, "patch": patch},
)
return _pending().create(request)
def apply_pending_change(change_id: str) -> dict[str, Any]:
"""Apply one pending change after repository validation."""
return _pending().apply(change_id)
def commit_changes(summary: str, files: list[str], details: str = "") -> dict[str, Any]:
"""Validate repository and create a local Git commit for selected files."""
return _git().commit(CommitRequest(summary=summary, details=details, files=files))
def update_doc(path: str, content: str, summary: str, reason: str = "") -> dict[str, Any]:
"""Create, apply, validate, and commit a Markdown document change."""
change = propose_doc_change(path=path, content=content, summary=summary, reason=reason)
applied = apply_pending_change(str(change["id"]))
pending_path = f"90-maintenance/pending-changes/{change['id']}.json"
commit = commit_changes(summary=summary, files=[path, pending_path], details=reason)
return {"change": applied, "commit": commit}
mcp = FastMCP(
"gnexus-book",
instructions=(
"Gnexus Book MCP exposes infrastructure documentation, inventory, "
"validation, pending changes, and local Git commit tools. "
"Do not store raw secrets in documentation."
),
)
mcp.tool(name="search_docs")(search_docs)
mcp.tool(name="read_doc")(read_doc)
mcp.tool(name="list_docs")(list_docs)
mcp.tool(name="list_inventory")(list_inventory)
mcp.tool(name="get_inventory_item")(get_inventory_item)
mcp.tool(name="get_relationships")(get_relationships)
mcp.tool(name="validate_repository")(validate_repository_tool)
mcp.tool(name="check_freshness")(check_freshness)
mcp.tool(name="git_status")(git_status)
mcp.tool(name="git_diff")(git_diff)
mcp.tool(name="list_pending_changes")(list_pending_changes)
mcp.tool(name="propose_doc_change")(propose_doc_change)
mcp.tool(name="propose_inventory_item_change")(propose_inventory_item_change)
mcp.tool(name="apply_pending_change")(apply_pending_change)
mcp.tool(name="commit_changes")(commit_changes)
mcp.tool(name="update_doc")(update_doc)
@mcp.resource("gnexus-book://docs", mime_type="application/json")
def docs_resource() -> str:
return _json(list_docs())
@mcp.resource("gnexus-book://docs/{path}", mime_type="application/json")
def doc_resource(path: str) -> str:
return _json(read_doc(path))
@mcp.resource("gnexus-book://inventory", mime_type="application/json")
def inventory_resource() -> str:
return _json(list_inventory())
@mcp.resource("gnexus-book://inventory/{inventory_type}", mime_type="application/json")
def inventory_type_resource(inventory_type: str) -> str:
return _json(list_inventory(inventory_type))
@mcp.resource("gnexus-book://relationships", mime_type="application/json")
def relationships_resource() -> str:
return _json(get_relationships())
@mcp.resource("gnexus-book://freshness", mime_type="application/json")
def freshness_resource() -> str:
return _json(check_freshness())
@mcp.resource("gnexus-book://validation", mime_type="application/json")
def validation_resource() -> str:
return _json(validate_repository_tool())
@mcp.resource("gnexus-book://git/status", mime_type="application/json")
def git_status_resource() -> str:
return _json(git_status())
def main() -> None:
transport = os.environ.get("GNEXUS_BOOK_MCP_TRANSPORT", "stdio")
if transport not in {"stdio", "sse", "streamable-http"}:
raise SystemExit("GNEXUS_BOOK_MCP_TRANSPORT must be stdio, sse, or streamable-http")
mcp.run(transport=transport) # type: ignore[arg-type]
if __name__ == "__main__":
main()