server.py•7.74 kB
#!/home/borjigin/dev/bookstack-mcp/venv/bin/python
"""
Minimal BookStack MCP Server for Cursor
Provides essential tools for managing BookStack documentation.
"""
import os
import asyncio
import httpx
from typing import Optional, Literal
from fastmcp import FastMCP
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
BS_URL = os.getenv("BS_URL", "http://192.168.1.193:6875")
BS_TOKEN_ID = os.getenv("BS_TOKEN_ID")
BS_TOKEN_SECRET = os.getenv("BS_TOKEN_SECRET")
# Initialize FastMCP server
mcp = FastMCP("bookstack-mcp")
class BookStackClient:
"""Simple BookStack API client"""
def __init__(self, base_url: str, token_id: str, token_secret: str):
self.base_url = base_url.rstrip("/")
self.headers = {
"Authorization": f"Token {token_id}:{token_secret}",
"Content-Type": "application/json"
}
async def request(self, method: str, endpoint: str, **kwargs):
"""Make an HTTP request to BookStack API"""
url = f"{self.base_url}/api/{endpoint.lstrip('/')}"
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.request(method, url, headers=self.headers, **kwargs)
response.raise_for_status()
return response.json()
def get_client() -> BookStackClient:
"""Get configured BookStack client"""
if not BS_TOKEN_ID or not BS_TOKEN_SECRET:
raise ValueError("BS_TOKEN_ID and BS_TOKEN_SECRET must be set in .env file")
return BookStackClient(BS_URL, BS_TOKEN_ID, BS_TOKEN_SECRET)
@mcp.tool()
async def bookstack_list_content(
entity_type: Literal["books", "bookshelves", "chapters", "pages"],
count: int = 20,
offset: int = 0,
filter_name: Optional[str] = None
) -> str:
"""
List BookStack content entities (books, bookshelves, chapters, or pages).
Args:
entity_type: Type of entity to list
count: Number of items to return (default 20)
offset: Pagination offset (default 0)
filter_name: Optional filter by name (substring match)
Returns:
JSON string with list of entities
"""
client = get_client()
params = {"count": count, "offset": offset}
if filter_name:
params["filter[name]"] = filter_name
result = await client.request("GET", entity_type, params=params)
return str(result)
@mcp.tool()
async def bookstack_search(
query: str,
page: int = 1,
count: int = 20
) -> str:
"""
Search across all BookStack content.
Args:
query: Search query string
page: Page number for pagination (default 1)
count: Number of results per page (default 20)
Returns:
JSON string with search results
"""
client = get_client()
params = {"query": query, "page": page, "count": count}
result = await client.request("GET", "search", params=params)
return str(result)
@mcp.tool()
async def bookstack_get_page(page_id: int) -> str:
"""
Get detailed information about a specific page including its content.
Args:
page_id: The ID of the page to retrieve
Returns:
JSON string with page details including HTML and Markdown content
"""
client = get_client()
result = await client.request("GET", f"pages/{page_id}")
return str(result)
@mcp.tool()
async def bookstack_create_page(
book_id: int,
name: str,
html: Optional[str] = None,
markdown: Optional[str] = None,
chapter_id: Optional[int] = None,
tags: Optional[str] = None
) -> str:
"""
Create a new page in BookStack.
Args:
book_id: ID of the book to create the page in
name: Name/title of the page
html: HTML content (use either html or markdown, not both)
markdown: Markdown content (use either html or markdown, not both)
chapter_id: Optional chapter ID to place the page in
tags: Optional comma-separated tags (e.g., "tag1=value1,tag2=value2")
Returns:
JSON string with created page details
"""
client = get_client()
data = {
"book_id": book_id,
"name": name
}
if html:
data["html"] = html
elif markdown:
data["markdown"] = markdown
if chapter_id:
data["chapter_id"] = chapter_id
if tags:
data["tags"] = [{"name": tag.split("=")[0], "value": tag.split("=")[1] if "=" in tag else ""}
for tag in tags.split(",")]
result = await client.request("POST", "pages", json=data)
return str(result)
@mcp.tool()
async def bookstack_update_page(
page_id: int,
name: Optional[str] = None,
html: Optional[str] = None,
markdown: Optional[str] = None,
tags: Optional[str] = None
) -> str:
"""
Update an existing page in BookStack.
Args:
page_id: ID of the page to update
name: New name/title (optional)
html: New HTML content (optional)
markdown: New Markdown content (optional)
tags: Optional comma-separated tags (e.g., "tag1=value1,tag2=value2")
Returns:
JSON string with updated page details
"""
client = get_client()
data = {}
if name:
data["name"] = name
if html:
data["html"] = html
if markdown:
data["markdown"] = markdown
if tags:
data["tags"] = [{"name": tag.split("=")[0], "value": tag.split("=")[1] if "=" in tag else ""}
for tag in tags.split(",")]
result = await client.request("PUT", f"pages/{page_id}", json=data)
return str(result)
@mcp.tool()
async def bookstack_delete_page(page_id: int) -> str:
"""
Delete a page from BookStack.
Args:
page_id: ID of the page to delete
Returns:
Success message
"""
client = get_client()
await client.request("DELETE", f"pages/{page_id}")
return f"Page {page_id} deleted successfully"
@mcp.tool()
async def bookstack_create_book(
name: str,
description: Optional[str] = None,
tags: Optional[str] = None
) -> str:
"""
Create a new book in BookStack.
Args:
name: Name of the book
description: Optional description
tags: Optional comma-separated tags
Returns:
JSON string with created book details
"""
client = get_client()
data = {"name": name}
if description:
data["description"] = description
if tags:
data["tags"] = [{"name": tag.split("=")[0], "value": tag.split("=")[1] if "=" in tag else ""}
for tag in tags.split(",")]
result = await client.request("POST", "books", json=data)
return str(result)
@mcp.tool()
async def bookstack_create_chapter(
book_id: int,
name: str,
description: Optional[str] = None,
tags: Optional[str] = None
) -> str:
"""
Create a new chapter in a book.
Args:
book_id: ID of the book to create the chapter in
name: Name of the chapter
description: Optional description
tags: Optional comma-separated tags
Returns:
JSON string with created chapter details
"""
client = get_client()
data = {
"book_id": book_id,
"name": name
}
if description:
data["description"] = description
if tags:
data["tags"] = [{"name": tag.split("=")[0], "value": tag.split("=")[1] if "=" in tag else ""}
for tag in tags.split(",")]
result = await client.request("POST", "chapters", json=data)
return str(result)
if __name__ == "__main__":
# Run the MCP server
mcp.run()