"""Smoke test: run the MCP server against a fake BookStack API.""" import asyncio import os import threading import time import uvicorn from starlette.applications import Starlette from starlette.responses import JSONResponse, PlainTextResponse from starlette.routing import Route # ---------------- fake BookStack ---------------- EXPECTED_AUTH = "Token tok-id:tok-secret" def check(request): assert request.headers.get("authorization") == EXPECTED_AUTH, "bad auth header" async def search(request): check(request) q = request.query_params.get("query") return JSONResponse({ "data": [{ "id": 42, "name": "Firewall Rules", "type": "page", "book_id": 3, "chapter_id": 0, "url": "https://wiki.test/books/net/page/firewall-rules", "preview_html": {"name": "Firewall Rules", "content": "Inbound 443 allowed & logged"}, "tags": [{"name": "env", "value": "prod"}], "updated_at": "2026-08-01T10:00:00Z", }], "total": 1, "query": q, }) async def page_detail(request): check(request) return JSONResponse({ "id": 42, "name": "Firewall Rules", "book_id": 3, "chapter_id": 0, "slug": "firewall-rules", "book_slug": "net", "tags": [{"name": "env", "value": "prod"}], "updated_at": "2026-08-01T10:00:00Z", "html": "

hi

", }) async def page_export(request): check(request) return PlainTextResponse("# Firewall Rules\n\nInbound 443 allowed.\n") async def books(request): check(request) return JSONResponse({"data": [{"id": 3, "name": "Networking", "description": "net docs", "updated_at": "2026-08-01T10:00:00Z"}], "total": 1}) async def book_detail(request): check(request) return JSONResponse({ "id": 3, "name": "Networking", "description": "net docs", "tags": [], "updated_at": "2026-08-01T10:00:00Z", "contents": [ {"type": "chapter", "id": 7, "name": "Edge", "pages": [{"id": 42, "name": "Firewall Rules"}]}, {"type": "page", "id": 43, "name": "Loose Page"}, ], }) async def create_page(request): check(request) body = await request.json() assert body["markdown"], "missing markdown" return JSONResponse({"id": 99, "name": body["name"], "slug": "new-page"}) async def forbidden(request): return JSONResponse({"error": "nope"}, status_code=403) fake = Starlette(routes=[ Route("/api/search", search), Route("/api/pages/42", page_detail), Route("/api/pages/42/export/markdown", page_export), Route("/api/pages", books, methods=["GET"]), Route("/api/pages", create_page, methods=["POST"]), Route("/api/books", books, methods=["GET"]), Route("/api/books/3", book_detail), Route("/api/shelves", forbidden), ]) def serve(app, port): uvicorn.run(app, host="127.0.0.1", port=port, log_level="error") async def main(): threading.Thread(target=serve, args=(fake, 9911), daemon=True).start() os.environ.update({ "BOOKSTACK_URL": "http://127.0.0.1:9911", "BOOKSTACK_TOKEN_ID": "tok-id", "BOOKSTACK_TOKEN_SECRET": "tok-secret", "BOOKSTACK_READ_ONLY": "false", "MCP_AUTH_MODE": "token", "MCP_STATIC_TOKENS": "secret-abc,secret-def", "MCP_PORT": "9912", }) from bookstack_mcp.__main__ import build_app app, settings = build_app() threading.Thread(target=serve, args=(app, 9912), daemon=True).start() time.sleep(2.5) from fastmcp import Client from fastmcp.client.auth import BearerAuth url = "http://127.0.0.1:9912/mcp" # 1. unauthenticated must fail try: async with Client(url) as c: await c.list_tools() print("FAIL: unauthenticated access was allowed") except Exception as exc: print(f"PASS auth required -> {type(exc).__name__}") # 2. authenticated async with Client(url, auth=BearerAuth("secret-abc")) as c: tools = sorted(t.name for t in await c.list_tools()) print("PASS tools:", tools) r = await c.call_tool("search_content", {"query": "{type:page} firewall"}) hit = r.data["results"][0] assert hit["name"] == "Firewall Rules", hit assert hit["excerpt"] == "Inbound 443 allowed & logged", hit["excerpt"] assert hit["tags"] == ["env=prod"], hit["tags"] print("PASS search_content ->", hit["name"], "|", hit["excerpt"], "|", hit["tags"]) r = await c.call_tool("get_page", {"page_id": 42}) assert r.data["content"].startswith("# Firewall Rules"), r.data print("PASS get_page ->", repr(r.data["content"][:30]), "url:", r.data["url"]) r = await c.call_tool("get_book", {"book_id": 3}) assert r.data["contents"][0]["pages"][0]["id"] == 42 print("PASS get_book ->", r.data["contents"]) r = await c.call_tool("create_page", { "name": "New", "markdown": "# hi", "book_id": 3, "tags": ["env=dev", "draft"]}) assert r.data["id"] == 99 print("PASS create_page ->", r.data) # error surfacing: shelves endpoint returns 403 try: await c.call_tool("list_shelves", {}) print("FAIL: 403 was not surfaced") except Exception as exc: print("PASS 403 surfaced ->", str(exc)[:80]) # validation try: await c.call_tool("create_page", {"name": "x", "markdown": "y"}) print("FAIL: missing parent not caught") except Exception as exc: print("PASS missing parent ->", str(exc)[:60]) # 3. wrong token rejected try: async with Client(url, auth=BearerAuth("wrong")) as c: await c.list_tools() print("FAIL: bad token accepted") except Exception as exc: print(f"PASS bad token rejected -> {type(exc).__name__}") asyncio.run(main())