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
MCP_INSTRUCTIONS = """
Gnexus Book is the canonical knowledge base for the owner's digital, server,
network, project, smart-home, and local infrastructure.
This MCP server is not a generic file browser. It is the preferred operational
interface for AI agents that need infrastructure context or need to maintain the
knowledge base. Use it proactively when a user request mentions, depends on, or
may be affected by documented infrastructure.
Use Gnexus Book before answering or acting when the task involves:
- hosts, VPS, VMs, servers, routers, networks, domains, ports, traffic routes, or VPNs;
- services such as GitBucket, nginx proxies, smart-home, auth, storage, media, or project deployments;
- repository/project ownership, activity, deployment mapping, or source-of-truth questions;
- credentials/access methods, but never request or store raw secrets in documentation;
- checking whether information is stale, incomplete, already documented, or safe to change;
- updating documentation after discovering new facts.
Typical read workflow:
1. search_docs(query) for natural-language lookup.
2. read_doc(path) for canonical narrative context.
3. list_inventory(type) or get_inventory_item(type, id) for structured facts.
4. get_relationships() when dependencies, upstreams, domains, hosts, or unresolved references matter.
5. check_freshness() and validate_repository() when making maintenance decisions.
Typical write workflow:
1. Prefer propose_doc_change or propose_inventory_item_change for non-trivial changes.
2. Use apply_pending_change to validate and apply.
3. Use commit_changes with a short factual summary.
4. For simple full-document replacements, update_doc may create, apply, validate, and commit in one call.
Rules:
- Do not store raw passwords, tokens, private keys, recovery codes, session cookies, or secret config values.
- IP addresses, hostnames, usernames, domains, routes, ports, and credential references may be documented when useful.
- Treat Git history as the rollback mechanism; still keep changes focused and commit summaries clear.
- If documentation conflicts with live observations, say so and update the knowledge base when appropriate.
- If the user asks about infrastructure and you have this MCP server available, do not wait for the user to say "look in the knowledge base"; consult it yourself.
""".strip()
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=MCP_INSTRUCTIONS,
)
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()