From d6f53b61a62b87c192fef0438db19d8d96624ca5 Mon Sep 17 00:00:00 2001 From: Noah Sprent Date: Sun, 26 Apr 2026 09:53:12 +0100 Subject: [PATCH] Add write_knowledge_file and delete_knowledge_file MCP tools - write_knowledge_file(filename, content, mode='overwrite'|'append') creates or updates a knowledge-base file. - delete_knowledge_file(filename) for obsolete files. - Filename validation: basename only, .md extension, alphanumeric/dash/underscore. - 200 KB per-file size cap. - Bumps version to 0.2.0. --- knowledge_base/__init__.py | 57 ++++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/knowledge_base/__init__.py b/knowledge_base/__init__.py index efa9c38..35955b3 100644 --- a/knowledge_base/__init__.py +++ b/knowledge_base/__init__.py @@ -65,3 +65,60 @@ def read_knowledge_file(filename: str) -> dict: return {"filename": filename, "content": path.read_text()} +# Maximum size for a single knowledge-base file. Generous enough for full reference docs, +# small enough that nothing here is being used as bulk storage. +_MAX_FILE_SIZE_BYTES = 200_000 + + +@register_mcp_tool() +def write_knowledge_file(filename: str, content: str, mode: str = "overwrite") -> dict: + """Create or update a knowledge-base file. Use this to record durable, reactor-specific facts (cluster layout, calibration history, hardware quirks, hard-won workarounds) that future sessions should be able to find via list_knowledge_files. Filename must end in .md and contain only the basename (no path separators). Content should start with YAML frontmatter including a `description:` field — that field is what list_knowledge_files surfaces, so make it specific. Mode 'overwrite' replaces existing content; 'append' adds to the end. + + Do NOT use this for transient session notes, secrets, generic Pioreactor docs (those belong in the skill or upstream docs), or anything that's not durably useful for THIS reactor.""" + if not filename.endswith(".md"): + return {"error": "Filename must end in .md"} + if ".." in filename or "/" in filename or "\\" in filename: + return {"error": "Invalid filename — basename only, no path separators"} + if not filename.replace("-", "").replace("_", "").replace(".", "").isalnum(): + return {"error": "Filename should contain only letters, digits, hyphens, underscores, and the .md extension"} + if mode not in ("overwrite", "append"): + return {"error": "Mode must be 'overwrite' or 'append'"} + if len(content.encode("utf-8")) > _MAX_FILE_SIZE_BYTES: + return {"error": f"Content exceeds max size ({_MAX_FILE_SIZE_BYTES} bytes); split into multiple files"} + + KNOWLEDGE_DIR.mkdir(parents=True, exist_ok=True) + path = KNOWLEDGE_DIR / filename + + existed = path.exists() + if mode == "append" and existed: + existing = path.read_text() + # Ensure exactly one blank line separator + sep = "" if existing.endswith("\n\n") else ("\n" if existing.endswith("\n") else "\n\n") + new_content = existing + sep + content + if len(new_content.encode("utf-8")) > _MAX_FILE_SIZE_BYTES: + return {"error": f"Appended content would exceed max size ({_MAX_FILE_SIZE_BYTES} bytes)"} + path.write_text(new_content) + else: + path.write_text(content) + + return { + "filename": filename, + "action": ("appended_to" if mode == "append" and existed else ("updated" if existed else "created")), + "size_bytes": path.stat().st_size, + } + + +@register_mcp_tool() +def delete_knowledge_file(filename: str) -> dict: + """Delete a knowledge-base file. Use sparingly — only when the file is genuinely obsolete (e.g. it described hardware that's been removed, or its content has been merged into another file). For corrections, prefer write_knowledge_file with mode='overwrite'.""" + if not filename.endswith(".md"): + return {"error": "Filename must end in .md"} + if ".." in filename or "/" in filename or "\\" in filename: + return {"error": "Invalid filename"} + path = KNOWLEDGE_DIR / filename + if not path.exists(): + return {"error": f"File {filename} not found"} + path.unlink() + return {"filename": filename, "action": "deleted"} + + diff --git a/setup.py b/setup.py index 0c19335..4098055 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ setup( name="pioreactor-knowledge-base", - version="0.1.0", + version="0.2.0", license="MIT", description="MCP tools for accessing domain knowledge files on a Pioreactor", long_description=open("README.md").read(),