diff --git a/server/app/mcp_server.py b/server/app/mcp_server.py index b9262b5..ca20f45 100644 --- a/server/app/mcp_server.py +++ b/server/app/mcp_server.py @@ -3,9 +3,10 @@ import json import os from pathlib import Path -from typing import Any, Literal +from typing import Annotated, Any, Literal from mcp.server.fastmcp import FastMCP +from pydantic import Field from .config import Settings from .docs_repository import DocsRepository @@ -48,6 +49,10 @@ 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. +For propose_inventory_item_change, pass concrete top-level arguments, not a wrapper: +inventory_type="hosts", item_id="bserv", mode="create", patch={"ip_address": "192.168.1.130", "user": "bserv"}, summary="Document bserv host". +The patch object is the inventory item fields to create or update. + 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; @@ -93,6 +98,12 @@ 4. Use commit_changes with a concise factual summary. 5. Use update_doc only for simple full-document updates. +Inventory write call shape: +- propose_inventory_item_change(inventory_type, item_id, patch, summary, reason?, mode?) +- Do not wrap arguments in params. +- For create mode, patch is the new inventory item fields. +- For update mode, patch is the fields to merge into the existing item. + Do document: - hostnames, usernames, IPs, domains, ports, routes, access models, credential references, versions, repository activity. @@ -194,8 +205,13 @@ return _pending().list() -def propose_doc_change(path: str, content: str, summary: str, reason: str = "") -> dict[str, Any]: - """Create a pending Markdown document change when durable non-secret knowledge should be added or corrected.""" +def propose_doc_change( + path: Annotated[str, Field(description="Repository-relative Markdown path to create or replace, for example '10-systems/applications/gitbucket.md'.")], + content: Annotated[str, Field(description="Full Markdown document content, including frontmatter when the target document uses it.")], + summary: Annotated[str, Field(description="Short factual change summary used for the pending change and later commit message.")], + reason: Annotated[str, Field(description="Why this change is needed; mention the source of the fact when useful.")] = "", +) -> dict[str, Any]: + """Create a pending Markdown document change. Pass path, content, summary, and optional reason as top-level arguments.""" request = ProposedChangeRequest( kind="doc", target=path, @@ -207,14 +223,14 @@ def propose_inventory_item_change( - inventory_type: str, - item_id: str, - patch: dict[str, Any], - summary: str, - reason: str = "", - mode: Literal["update", "create"] = "update", + inventory_type: Annotated[str, Field(description="Inventory collection name, for example 'hosts', 'services', 'domains', 'networks', 'hardware', or 'virtual-machines'.")], + item_id: Annotated[str, Field(description="Canonical inventory item id. For a host, use the stable host id such as 'bserv', not the IP address.")], + patch: Annotated[dict[str, Any], Field(description="Inventory fields to create or update. For mode='create', this object is the new item body. Do not wrap it in 'inventory_item' or 'params'.")], + summary: Annotated[str, Field(description="Short factual summary of the inventory change.")], + reason: Annotated[str, Field(description="Why this inventory fact should be persisted; mention the source when useful.")] = "", + mode: Annotated[Literal["update", "create"], Field(description="'create' for a new inventory item; 'update' to patch an existing item.")] = "update", ) -> dict[str, Any]: - """Create a pending structured inventory patch or new item for durable facts about infrastructure.""" + """Create a pending inventory change. Use top-level arguments: inventory_type, item_id, patch, summary, reason, mode.""" request = ProposedChangeRequest( kind="inventory-item", target=f"{inventory_type}/{item_id}", diff --git a/server/tests/test_mcp_server.py b/server/tests/test_mcp_server.py index a8d8fcf..888b7aa 100644 --- a/server/tests/test_mcp_server.py +++ b/server/tests/test_mcp_server.py @@ -25,6 +25,17 @@ assert "commit_changes" in names +def test_inventory_write_tool_schema_is_self_describing() -> None: + tools = asyncio.run(mcp.list_tools()) + tool = next(t for t in tools if t.name == "propose_inventory_item_change") + + assert "top-level arguments" in tool.description + props = tool.inputSchema["properties"] + assert "Do not wrap" in props["patch"]["description"] + assert "Canonical inventory item id" in props["item_id"]["description"] + assert set(tool.inputSchema["required"]) == {"inventory_type", "item_id", "patch", "summary"} + + def test_mcp_instructions_tell_agents_to_use_knowledge_base_proactively() -> None: assert mcp.instructions is not None assert "canonical knowledge base" in mcp.instructions