"""BookStack wiki tools, loaded from the bookstack-mcp server over stdio. Admin-only: the tools are only compiled into the admin agent, and each call re-checks ``is_admin`` in the run config as a second line of defense. """ import logging import os from langchain_core.tools import BaseTool, StructuredTool from utils.mcp_tools import admin_only_message, load_stdio_tools, select_tools logger = logging.getLogger(__name__) # Read + create/update only. The server exposes 43 tools with writes enabled; # exposing all of them would blow the local model's 32k context and allow # deletes, shelf/attachment management, and recycle-bin purges. BOOKSTACK_TOOL_ALLOWLIST = ( "search_content", "get_books", "get_book", "get_chapters", "get_pages", "get_page", "get_recent_changes", "create_book", "create_chapter", "create_page", "update_page", "create_comment", ) LABEL = "BookStack" ADMIN_ONLY_MESSAGE = admin_only_message(LABEL) def select_bookstack_tools(mcp_tools: list[BaseTool]) -> list[StructuredTool]: """Keep allowlisted tools (in allowlist order) and wrap them admin-only.""" return select_tools(mcp_tools, BOOKSTACK_TOOL_ALLOWLIST, "bookstack", LABEL) async def load_bookstack_tools() -> list[StructuredTool]: """Start bookstack-mcp and return the wrapped tools, or [] if unavailable.""" url = os.getenv("BOOKSTACK_BASE_URL") token_id = os.getenv("BOOKSTACK_TOKEN_ID") token_secret = os.getenv("BOOKSTACK_TOKEN_SECRET") if not url or not token_id or not token_secret: logger.info( "BookStack MCP not configured " "(BOOKSTACK_BASE_URL/BOOKSTACK_TOKEN_ID/BOOKSTACK_TOKEN_SECRET unset)" ) return [] return await load_stdio_tools( server="bookstack", command=os.getenv("BOOKSTACK_MCP_COMMAND", "bookstack-mcp"), env={ "BOOKSTACK_BASE_URL": url, "BOOKSTACK_TOKEN_ID": token_id, "BOOKSTACK_TOKEN_SECRET": token_secret, # Server-side write gate; the allowlist above keeps deletes out. "BOOKSTACK_ENABLE_WRITE": "true", }, allowlist=BOOKSTACK_TOOL_ALLOWLIST, prefix="bookstack", label=LABEL, )