diff --git a/90-maintenance/agent-mcp-usage.md b/90-maintenance/agent-mcp-usage.md index 0e3ae33..1abf942 100644 --- a/90-maintenance/agent-mcp-usage.md +++ b/90-maintenance/agent-mcp-usage.md @@ -40,6 +40,8 @@ - where a project is hosted, deployed, or documented; - what an agent should update after discovering new infrastructure facts. +At the beginning of an infrastructure-related task, agents should call `get_agent_usage_guide()` if they are not already familiar with this workflow. + ## When Not To Use It Do not use MCP for unrelated general knowledge. @@ -78,6 +80,19 @@ Documentation writes should be factual, focused, and committed. +The default stance is: if the agent learns a durable, non-secret infrastructure fact that will be useful later, the agent should update Gnexus Book during the same task. + +Durable facts include: + +- a new or changed host, VM, VPS, router, device, or physical server; +- a new or changed IP address, hostname, MAC address, domain, port, route, upstream, VPN path, or network segment; +- a new or changed service, project, repository, deployment path, owner, version, status, or dependency; +- a live observation that contradicts existing documentation; +- a user-provided clarification that changes the meaning of existing docs; +- evidence that a project is active, dormant, abandoned, deployed, or no longer deployed. + +Do not update the canonical docs for purely temporary observations unless they are useful as discovery notes. Use `90-maintenance/discovery-observations/` for scans, uncertain findings, or facts that should not yet become canonical inventory. + For non-trivial changes: 1. Use `propose_doc_change` or `propose_inventory_item_change`. @@ -94,6 +109,45 @@ - no raw secrets; - `last_reviewed` updates only when the agent actually verified the fact. +If the agent decides not to update Gnexus Book after learning a relevant fact, it should state why. Acceptable reasons include: + +- the fact is sensitive and would expose a secret; +- the fact is too temporary or uncertain; +- the user explicitly asked not to document it; +- the change requires a larger documentation pass and should be queued. + +## Tool Selection + +Use this decision table: + +| Situation | Tool | +| --- | --- | +| Agent is unsure how to use this MCP server | `get_agent_usage_guide()` | +| Broad lookup by phrase or object name | `search_docs(query)` | +| Read canonical narrative docs | `read_doc(path)` | +| See available structured inventory types | `list_inventory()` | +| Read all records of one inventory type | `list_inventory(inventory_type)` | +| Read one known record | `get_inventory_item(inventory_type, item_id)` | +| Understand dependencies/routes/upstreams | `get_relationships()` | +| Check staleness | `check_freshness()` | +| Check repository safety before/after changes | `validate_repository()` | +| Create/update a Markdown page | `propose_doc_change()` then `apply_pending_change()` | +| Create/update structured YAML inventory | `propose_inventory_item_change()` then `apply_pending_change()` | +| Review local changes | `git_status()` and `git_diff()` | +| Commit validated changes | `commit_changes()` | +| Simple full-document replacement | `update_doc()` | + +## End-Of-Task Checklist + +Before finishing an infrastructure-related task, the agent should check: + +- Did I learn a durable infrastructure fact? +- Did I discover a mismatch between live state and documentation? +- Did I add or update documentation/inventory? +- If not, did I explain why not? +- Did I run validation after changes? +- Did I create a local Git commit when changes were applied? + ## Interpreting Repository Activity For GitBucket repositories, last commit date matters. diff --git a/server/README.md b/server/README.md index 9899e25..2c18f5c 100644 --- a/server/README.md +++ b/server/README.md @@ -34,6 +34,7 @@ Core tools: +- `get_agent_usage_guide()` - `search_docs(query)` - `read_doc(path)` - `list_docs()` @@ -52,6 +53,7 @@ Agent usage policy: +- Call `get_agent_usage_guide()` when unsure how to use the knowledge tools. - Use MCP before answering questions about hosts, VPS, VMs, networks, domains, routes, services, deployments, GitBucket repositories, or smart-home infrastructure. - Use `search_docs` first for broad lookup, then `read_doc`, inventory tools, and `get_relationships` for authoritative context. - Use `check_freshness` when stale information matters. @@ -63,6 +65,7 @@ Useful resources: - `gnexus-book://docs` +- `gnexus-book://agent-guide` - `gnexus-book://inventory` - `gnexus-book://relationships` - `gnexus-book://freshness` diff --git a/server/app/mcp_server.py b/server/app/mcp_server.py index 3327768..7ae586b 100644 --- a/server/app/mcp_server.py +++ b/server/app/mcp_server.py @@ -42,10 +42,20 @@ 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. +1. If you learn a durable infrastructure fact that is missing, stale, or wrong, update Gnexus Book in the same task whenever practical. +2. Prefer propose_doc_change or propose_inventory_item_change for non-trivial changes. +3. Use apply_pending_change to validate and apply. +4. Use commit_changes with a short factual summary. +5. For simple full-document replacements, update_doc may create, apply, validate, and commit in one call. + +Knowledge maintenance triggers: +- new host, VM, domain, endpoint, route, service, repository, device, credential reference, or dependency; +- changed IP address, port, upstream, status, operating system, version, owner, deployment path, or last-reviewed state; +- live observation contradicts documentation; +- user provides infrastructure facts in chat; +- agent discovers facts while connected to hosts, routers, APIs, GitBucket, or local services. + +If a discovered fact is useful beyond the current chat and is not secret, it belongs in Gnexus Book. If you decide not to update it, say why. Rules: - Do not store raw passwords, tokens, private keys, recovery codes, session cookies, or secret config values. @@ -56,6 +66,46 @@ """.strip() +AGENT_USAGE_GUIDE = """ +Gnexus Book MCP quick guide for agents: + +Mission: +- Treat this MCP server as the owner's infrastructure memory. +- Use it before answering infrastructure-specific questions. +- Keep it current when durable facts are discovered. + +Use it proactively for: +- hosts, VMs, VPS, routers, LANs, VPNs, domains, ports, routes, proxies; +- deployed services, smart-home, GitBucket repositories, project activity, source/deployment mapping; +- freshness, missing documentation, stale records, conflicting observations. + +Read path: +1. search_docs(query) +2. read_doc(path) +3. list_inventory(type) or get_inventory_item(type, id) +4. get_relationships() for dependencies and routes +5. check_freshness() if age matters + +Write path: +1. If a fact is durable, useful later, and non-secret, update docs/inventory. +2. Use propose_doc_change or propose_inventory_item_change for reviewable changes. +3. Use apply_pending_change, then validate_repository. +4. Use commit_changes with a concise factual summary. +5. Use update_doc only for simple full-document updates. + +Do document: +- hostnames, usernames, IPs, domains, ports, routes, access models, credential references, versions, repository activity. + +Do not document: +- raw passwords, tokens, private keys, recovery codes, session cookies, secret config values. + +End-of-task maintenance check: +- Did I learn a new durable infrastructure fact? +- Did I find stale or conflicting documentation? +- Did I update Gnexus Book or explicitly explain why not? +""".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)) @@ -81,23 +131,28 @@ return json.dumps(data, ensure_ascii=False, indent=2) +def get_agent_usage_guide() -> dict[str, str]: + """Read the mandatory quick guide that tells agents when and how to use Gnexus Book MCP.""" + return {"guide": AGENT_USAGE_GUIDE} + + def search_docs(query: str) -> list[dict[str, object]]: - """Search Markdown documents by plain text query.""" + """Search canonical Markdown docs. Use this first for broad infrastructure lookup.""" return _docs().search(query) def read_doc(path: str) -> dict[str, object]: - """Read a Markdown document by repository-relative path.""" + """Read one canonical Markdown document by repository-relative path after search/list discovery.""" return _docs().read_doc(path) def list_docs() -> list[dict[str, object]]: - """List Markdown documents with titles and frontmatter.""" + """List all canonical Markdown docs 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.""" + """List inventory types or read parsed structured inventory such as hosts, services, domains, endpoints, projects, networks, hardware, or virtual-machines.""" repo = _inventory() if inventory_type: return repo.read_parsed(inventory_type) @@ -105,42 +160,42 @@ def get_inventory_item(inventory_type: str, item_id: str) -> dict[str, Any]: - """Read a single inventory item by type and id.""" + """Read one structured inventory record by type and id when you know the canonical id.""" return _inventory().read_item(inventory_type, item_id) def get_relationships() -> dict[str, object]: - """Build current inventory relationship graph.""" + """Build the inventory relationship graph for dependencies, domains, upstreams, hosts, routes, and unresolved references.""" return build_relationships(_inventory()) def validate_repository_tool() -> dict[str, object]: - """Validate schemas, docs, inventory links, ids, and secret patterns.""" + """Validate schemas, docs, inventory links, duplicate ids, and raw secret patterns before trusting or committing changes.""" return validate_repository(_settings()) def check_freshness() -> dict[str, object]: - """Return documentation freshness report.""" + """Return freshness/staleness report for deciding what documentation needs review.""" return build_freshness_report(_settings()) def git_status() -> dict[str, Any]: - """Return local Git status for the documentation repository.""" + """Return local Git status before editing or committing documentation changes.""" 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 local Git diff, optionally limited to repository-relative files, for reviewing documentation changes.""" return _git().diff(files) def list_pending_changes() -> list[dict[str, Any]]: - """List pending/applied/rejected change records.""" + """List pending/applied/rejected knowledge-change records created by agents.""" return _pending().list() def propose_doc_change(path: str, content: str, summary: str, reason: str = "") -> dict[str, Any]: - """Create a pending Markdown document change.""" + """Create a pending Markdown document change when durable non-secret knowledge should be added or corrected.""" request = ProposedChangeRequest( kind="doc", target=path, @@ -159,7 +214,7 @@ reason: str = "", mode: Literal["update", "create"] = "update", ) -> dict[str, Any]: - """Create a pending inventory item patch or creation request.""" + """Create a pending structured inventory patch or new item for durable facts about infrastructure.""" request = ProposedChangeRequest( kind="inventory-item", target=f"{inventory_type}/{item_id}", @@ -171,17 +226,17 @@ def apply_pending_change(change_id: str) -> dict[str, Any]: - """Apply one pending change after repository validation.""" + """Apply one pending knowledge change and roll it back automatically if validation fails.""" 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.""" + """Validate the repository and create a local Git commit for selected documentation 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.""" + """For simple full-document updates: create pending change, apply, validate, and commit in one call.""" 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" @@ -196,6 +251,7 @@ mcp.tool(name="search_docs")(search_docs) +mcp.tool(name="get_agent_usage_guide")(get_agent_usage_guide) mcp.tool(name="read_doc")(read_doc) mcp.tool(name="list_docs")(list_docs) mcp.tool(name="list_inventory")(list_inventory) @@ -218,6 +274,11 @@ return _json(list_docs()) +@mcp.resource("gnexus-book://agent-guide", mime_type="text/markdown") +def agent_guide_resource() -> str: + return AGENT_USAGE_GUIDE + + @mcp.resource("gnexus-book://docs/{path}", mime_type="application/json") def doc_resource(path: str) -> str: return _json(read_doc(path)) diff --git a/server/tests/test_mcp_server.py b/server/tests/test_mcp_server.py index c7dfd9f..a8d8fcf 100644 --- a/server/tests/test_mcp_server.py +++ b/server/tests/test_mcp_server.py @@ -3,6 +3,7 @@ import asyncio from app.mcp_server import ( + get_agent_usage_guide, get_inventory_item, list_inventory, mcp, @@ -17,6 +18,7 @@ names = {tool.name for tool in tools} assert "search_docs" in names + assert "get_agent_usage_guide" in names assert "read_doc" in names assert "list_inventory" in names assert "update_doc" in names @@ -29,6 +31,16 @@ assert "Use it proactively" in mcp.instructions assert "do not wait for the user" in mcp.instructions assert "Do not store raw passwords" in mcp.instructions + assert "If a discovered fact is useful beyond the current chat" in mcp.instructions + + +def test_mcp_agent_usage_guide_explains_maintenance_workflow() -> None: + guide = get_agent_usage_guide()["guide"] + + assert "infrastructure memory" in guide + assert "Keep it current" in guide + assert "End-of-task maintenance check" in guide + assert "Do not document" in guide def test_mcp_tool_functions_read_repository() -> None: