From 45a161235fc8f2df75645d1bacd0f09d7e668a13 Mon Sep 17 00:00:00 2001 From: Tim Pearson Date: Sat, 23 May 2026 11:47:00 -0400 Subject: [PATCH] v4.0.0: slim tool defs and responses for ~30-55% smaller MCP payloads Token-efficiency overhaul. Verified against live BookStack: tools/list -32%, list endpoints -55-57%, get_recent_changes -51% (also drops N+1 fetches), get_book -31%. Response shape changes (breaking): - Drop redundant fields: direct_link, *_friendly date strings, content_info, contextual_info, change_summary, pagination_hint, location, shelf summary/tags_summary/book_count. - Fix buggy attachment page_url; keep download_url. - Compact JSON (no pretty-print whitespace). - get_recent_changes returns search-response data directly, no per-result book/page/chapter fetches. Tool-def changes: - Remove get_capabilities (redundant with tools/list). - Drop title field (clients key on name). - Trim verbose descriptions and 52 boilerplate param descriptions. - Dedupe advanced-search caveat onto search_content only. Add scripts/bench-context.mjs harness for re-measuring payload sizes against a live BookStack via JSON-RPC over stdio. --- README.md | 4 +- gemini-extension.json | 2 +- package.json | 2 +- scripts/bench-context.mjs | 111 +++++++++++ src/bookstack-client.ts | 178 ++++-------------- src/index.ts | 385 ++++++++++++++------------------------ 6 files changed, 289 insertions(+), 393 deletions(-) create mode 100644 scripts/bench-context.mjs diff --git a/README.md b/README.md index d37542a..25a6298 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ npx bookstack-mcp - Type-safe input validation with Zod (auto-coerces string/number params for broad client compatibility) - Embedded URLs and content previews in all responses - Markdown export fallback for HTML-authored pages, so AI clients always get usable content +- Token-efficient responses: compact JSON, no redundant fields, no N+1 fetches — ~30–55% smaller payloads than 3.x - Write operations disabled by default for safety - Works with Claude Desktop, Claude Code, LibreChat, and any MCP-compatible client - Stdio and Streamable HTTP transports @@ -214,13 +215,14 @@ Books and pages are also exposed as MCP resources, so clients that browse resour Both templates support `id` autocompletion: as you type, the server searches BookStack and returns matching IDs so you don't have to remember numeric IDs by hand. +> **4.0.0 breaking changes:** tool responses were trimmed for token efficiency. Removed fields: `direct_link`, `*_friendly` date strings, `content_info`, `contextual_info`, `change_summary`, `pagination_hint`, `location`, `summary`/`tags_summary`/`book_count` on shelves, and the buggy `page_url` on attachments. Use `url`, the ISO date fields, and `download_url` instead. The `get_capabilities` tool was removed — clients should use `tools/list` (built into MCP). Responses are now compact JSON (no pretty-printing) and `get_recent_changes` no longer issues per-result fetches. + ## Available Tools ### Read Operations (always available) | Tool | Description | |------|-------------| -| `get_capabilities` | Server capabilities and configuration | | `search_content` | Search across all content with filtering | | `search_pages` | Search pages with optional book filtering | | `get_books` / `get_book` | List or get details of books | diff --git a/gemini-extension.json b/gemini-extension.json index 2419891..a137333 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,6 +1,6 @@ { "name": "bookstack-mcp", - "version": "3.5.0", + "version": "4.0.0", "description": "BookStack wiki MCP server — search, read, create, and manage documentation", "icon": "logo.png", "mcpServers": { diff --git a/package.json b/package.json index cf9be05..b376467 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bookstack-mcp", - "version": "3.5.0", + "version": "4.0.0", "description": "MCP server for BookStack wiki — search, read, create, and manage documentation via AI assistants", "type": "module", "bin": { diff --git a/scripts/bench-context.mjs b/scripts/bench-context.mjs new file mode 100644 index 0000000..7fccd42 --- /dev/null +++ b/scripts/bench-context.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node +// Measure tools/list + a few read-only tool responses by speaking JSON-RPC +// over stdio to dist/index.js. Loads .env, prints sizes in bytes. +import { spawn } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, ".."); + +// Load .env +const env = { ...process.env }; +try { + for (const line of readFileSync(join(root, ".env"), "utf8").split("\n")) { + const m = line.match(/^\s*([A-Z_][A-Z0-9_]*)\s*=\s*(.*?)\s*$/); + if (m) env[m[1]] = m[2]; + } +} catch {} + +const child = spawn("node", [join(root, "dist/index.js")], { + env, + stdio: ["pipe", "pipe", "inherit"] +}); + +let buf = ""; +const pending = new Map(); +let nextId = 1; + +child.stdout.on("data", (chunk) => { + buf += chunk.toString("utf8"); + let idx; + while ((idx = buf.indexOf("\n")) >= 0) { + const line = buf.slice(0, idx).trim(); + buf = buf.slice(idx + 1); + if (!line) continue; + let msg; + try { msg = JSON.parse(line); } catch { continue; } + if (msg.id != null && pending.has(msg.id)) { + const { resolve } = pending.get(msg.id); + pending.delete(msg.id); + resolve(msg); + } + } +}); + +function send(method, params) { + const id = nextId++; + const req = { jsonrpc: "2.0", id, method, params }; + return new Promise((resolve, reject) => { + pending.set(id, { resolve, reject }); + child.stdin.write(JSON.stringify(req) + "\n"); + }); +} + +function bytes(obj) { + return Buffer.byteLength(JSON.stringify(obj), "utf8"); +} + +async function main() { + // 1. initialize + await send("initialize", { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "bench", version: "0.0.0" } + }); + child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n"); + + const results = {}; + + // 2. tools/list + const toolsList = await send("tools/list", {}); + results["tools/list"] = bytes(toolsList.result); + + // 3. Pick a small sample of read-only tool calls + const calls = [ + ["search_content", { query: "test", count: 5 }], + ["get_books", { count: 5 }], + ["get_pages", { count: 5 }], + ["get_recent_changes", { limit: 5, days: 365 }], + ["get_shelves", { count: 5 }], + ]; + + // Find any page id for get_page + const pagesRes = await send("tools/call", { name: "get_pages", arguments: { count: 1 } }); + try { + const payload = JSON.parse(pagesRes.result.content[0].text); + const pageId = payload.data?.[0]?.id; + if (pageId) calls.push(["get_page", { id: pageId, limit: 5000 }]); + } catch {} + + // Find any book id + const booksRes = await send("tools/call", { name: "get_books", arguments: { count: 1 } }); + try { + const payload = JSON.parse(booksRes.result.content[0].text); + const bookId = payload.data?.[0]?.id; + if (bookId) calls.push(["get_book", { id: bookId }]); + } catch {} + + for (const [name, args] of calls) { + const r = await send("tools/call", { name, arguments: args }); + // Measure the result content as the LLM would see it + const text = r.result?.content?.map(c => c.text ?? "").join("") ?? ""; + results[`tools/call:${name}`] = Buffer.byteLength(text, "utf8"); + } + + console.log(JSON.stringify(results, null, 2)); + child.kill(); +} + +main().catch((e) => { console.error(e); child.kill(); process.exit(1); }); diff --git a/src/bookstack-client.ts b/src/bookstack-client.ts index 2997359..dad5ada 100644 --- a/src/bookstack-client.ts +++ b/src/bookstack-client.ts @@ -222,19 +222,14 @@ export class BookStackClient { return `${this.baseUrl}/search?term=${encodedQuery}`; } - // Enhanced response helpers + // Enhanced response helpers. Each one adds only `url` on top of the raw + // BookStack record — narrated/duplicate fields (direct_link, *_friendly, + // content_info, summary, location, etc.) were removed to shrink the MCP + // payload; the LLM can derive them from the raw fields when needed. private enhanceBookResponse(book: Book): any { - const lastUpdated = this.formatDate(book.updated_at); - const created = this.formatDate(book.created_at); - return { ...book, - url: this.generateBookUrl(book), - direct_link: `[${book.name}](${this.generateBookUrl(book)})`, - last_updated_friendly: lastUpdated, - created_friendly: created, - summary: book.description ? `${book.description.substring(0, 100)}${book.description.length > 100 ? '...' : ''}` : 'No description available', - content_info: `Book created ${created}, last updated ${lastUpdated}` + url: this.generateBookUrl(book) }; } @@ -243,8 +238,6 @@ export class BookStackClient { offset?: number; limit?: number; }): Promise { - const lastUpdated = this.formatDate(page.updated_at); - const created = this.formatDate(page.created_at); const url = await this.generatePageUrl(page); // Pick a single content format to return to avoid 3x duplication (html + markdown + text) @@ -270,58 +263,27 @@ export class BookStackClient { return { ...pageMeta, url, - direct_link: `[${page.name}](${url})`, - last_updated_friendly: lastUpdated, - created_friendly: created, - content_info: `Page created ${created}, last updated ${lastUpdated}`, word_count: page.text ? page.text.split(' ').length : 0, - location: `Book ID ${page.book_id}${page.chapter_id ? `, Chapter ID ${page.chapter_id}` : ''}`, content_format: format, content_total_chars: totalChars, content_offset: offset, content_returned_chars: slice.length, content_truncated: truncated, content_next_offset: truncated ? nextOffset : null, - pagination_hint: truncated - ? `Only ${nextOffset}/${totalChars} characters returned. Call get_page again with offset=${nextOffset} to continue, or pass a larger limit.` - : undefined, content: slice }; } private async enhanceChapterResponse(chapter: Chapter): Promise { - const lastUpdated = this.formatDate(chapter.updated_at); - const created = this.formatDate(chapter.created_at); const url = await this.generateChapterUrl(chapter); - - return { - ...chapter, - url, - direct_link: `[${chapter.name}](${url})`, - last_updated_friendly: lastUpdated, - created_friendly: created, - summary: chapter.description ? `${chapter.description.substring(0, 100)}${chapter.description.length > 100 ? '...' : ''}` : 'No description available', - content_info: `Chapter created ${created}, last updated ${lastUpdated}`, - location: `In Book ID ${chapter.book_id}` - }; + return { ...chapter, url }; } private enhanceShelfResponse(shelf: Shelf): any { - const lastUpdated = this.formatDate(shelf.updated_at); - const created = this.formatDate(shelf.created_at); - const bookCount = shelf.books?.length || 0; - return { ...shelf, url: this.generateShelfUrl(shelf), - direct_link: `[${shelf.name}](${this.generateShelfUrl(shelf)})`, - last_updated_friendly: lastUpdated, - created_friendly: created, - summary: shelf.description ? `${shelf.description.substring(0, 100)}${shelf.description.length > 100 ? '...' : ''}` : 'No description available', - content_info: `Shelf with ${bookCount} book${bookCount !== 1 ? 's' : ''}, created ${created}, last updated ${lastUpdated}`, - book_count: bookCount, - books: shelf.books?.map(book => this.enhanceBookResponse(book)), - tags_summary: shelf.tags?.length ? `Tagged with: ${shelf.tags.map(t => `${t.name}${t.value ? `=${t.value}` : ''}`).join(', ')}` : 'No tags' + books: shelf.books?.map(book => this.enhanceBookResponse(book)) }; } @@ -329,21 +291,19 @@ export class BookStackClient { const enhancedResults = await Promise.all( results.map(async (result) => { const url = await this.generateContentUrl(result); - return { - ...result, - url, - direct_link: `[${result.name}](${url})`, - content_preview: result.preview_content?.content ? `${result.preview_content.content.substring(0, 150)}${result.preview_content.content.length > 150 ? '...' : ''}` : 'No preview available', - content_type: result.type.charAt(0).toUpperCase() + result.type.slice(1), - location_info: result.book_id ? `In book ID ${result.book_id}${result.chapter_id ? `, chapter ID ${result.chapter_id}` : ''}` : 'Location unknown' - }; + const preview = result.preview_content?.content; + const out: any = { ...result, url }; + if (preview) { + out.content_preview = preview.length > 150 ? `${preview.substring(0, 150)}...` : preview; + } + return out; }) ); return { search_query: originalQuery, search_url: this.generateSearchUrl(originalQuery), - summary: `Found ${results.length} results for "${originalQuery}"`, + total: results.length, results: enhancedResults }; } @@ -476,27 +436,17 @@ export class BookStackClient { } private async enhancePageListItem(page: Page): Promise { - const lastUpdated = this.formatDate(page.updated_at); - const created = this.formatDate(page.created_at); const url = await this.generatePageUrl(page); - const preview = page.text - ? `${page.text.substring(0, 200)}${page.text.length > 200 ? '...' : ''}` - : 'No content preview available'; // List responses from BookStack don't include html/markdown/text/raw_html, but strip // defensively and never embed full content here — use get_page for that. const { html: _h, markdown: _m, text: _t, raw_html: _r, ...pageMeta } = page as any; - return { - ...pageMeta, - url, - direct_link: `[${page.name}](${url})`, - last_updated_friendly: lastUpdated, - created_friendly: created, - content_preview: preview, - content_info: `Page created ${created}, last updated ${lastUpdated}`, - location: `Book ID ${page.book_id}${page.chapter_id ? `, Chapter ID ${page.chapter_id}` : ''}` - }; + const out: any = { ...pageMeta, url }; + if (page.text) { + out.content_preview = page.text.length > 200 ? `${page.text.substring(0, 200)}...` : page.text; + } + return out; } async getPage(id: number, options?: { @@ -754,81 +704,29 @@ export class BookStackClient { const response = await this.client.get('/search', { params }); const results = response.data.data || response.data; - - // Enhance results with additional context + + // Use the search response data as-is — no per-item BookStack fetch (was N+1). + // Callers can fetch full details via get_page/get_book/get_chapter if needed. const enhancedResults = await Promise.all( results.map(async (result: SearchResult) => { - let contextualInfo = ''; - let contentPreview = result.preview_content?.content || ''; - - try { - // Get additional context based on content type - if (result.type === 'page' && result.id) { - const fullPage = await this.client.get(`/pages/${result.id}`); - const pageData = fullPage.data; - contentPreview = pageData.text?.substring(0, 200) || contentPreview; - contextualInfo = `Updated in book: ${pageData.book?.name || 'Unknown Book'}`; - if (pageData.chapter) { - contextualInfo += `, chapter: ${pageData.chapter.name}`; - } - } else if (result.type === 'book' && result.id) { - const fullBook = await this.client.get(`/books/${result.id}`); - const bookData = fullBook.data; - contentPreview = bookData.description?.substring(0, 200) || 'No description available'; - contextualInfo = `Book with ${bookData.page_count || 0} pages`; - } else if (result.type === 'chapter' && result.id) { - const fullChapter = await this.client.get(`/chapters/${result.id}`); - const chapterData = fullChapter.data; - contentPreview = chapterData.description?.substring(0, 200) || 'No description available'; - contextualInfo = `Chapter in book: ${chapterData.book?.name || 'Unknown Book'}`; - } - } catch (error) { - // If we can't get additional context, use what we have - contextualInfo = `${result.type.charAt(0).toUpperCase() + result.type.slice(1)} content`; - } - const url = await this.generateContentUrl(result); - return { - ...result, - url, - direct_link: `[${result.name}](${url})`, - content_preview: contentPreview ? `${contentPreview}${contentPreview.length >= 200 ? '...' : ''}` : 'No preview available', - contextual_info: contextualInfo, - last_updated: this.formatDate(result.updated_at || result.created_at || ''), - change_summary: `${result.type === 'page' ? 'Page' : result.type === 'book' ? 'Book' : 'Chapter'} "${result.name}" was updated` - }; + const preview = result.preview_content?.content; + const out: any = { ...result, url }; + if (preview) { + out.content_preview = preview.length > 200 ? `${preview.substring(0, 200)}...` : preview; + } + return out; }) ); - + return { - search_query: `Recent changes in the last ${days} days (${type})`, date_threshold: dateFilter, - search_url: this.generateSearchUrl(searchQuery), - total_found: results.length, - summary: `Found ${results.length} items updated in the last ${days} days${type !== 'all' ? ` (${type}s only)` : ''}`, + type, + total: results.length, results: enhancedResults }; } - private formatDate(dateString: string): string { - if (!dateString) return 'Unknown date'; - - const date = new Date(dateString); - const now = new Date(); - const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60)); - - if (diffInHours < 1) return 'Less than an hour ago'; - if (diffInHours < 24) return `${diffInHours} hours ago`; - - const diffInDays = Math.floor(diffInHours / 24); - if (diffInDays < 7) return `${diffInDays} days ago`; - - const diffInWeeks = Math.floor(diffInDays / 7); - if (diffInWeeks < 4) return `${diffInWeeks} weeks ago`; - - return date.toLocaleDateString(); - } - // Shelves (Book Collections) Management async getShelves(options?: { offset?: number; @@ -914,8 +812,7 @@ export class BookStackClient { ...data, data: data.data.map((attachment: Attachment) => ({ ...attachment, - page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`, - direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})` + download_url: `${this.baseUrl}/attachments/${attachment.id}` })) }; } @@ -923,11 +820,8 @@ export class BookStackClient { async getAttachment(id: number): Promise { const response = await this.client.get(`/attachments/${id}`); const attachment = response.data; - return { ...attachment, - page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`, - direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})`, download_url: `${this.baseUrl}/attachments/${attachment.id}` }; } @@ -943,11 +837,9 @@ export class BookStackClient { } const response = await this.client.post('/attachments', data); const attachment = response.data; - return { ...attachment, - page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`, - direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})` + download_url: `${this.baseUrl}/attachments/${attachment.id}` }; } @@ -961,11 +853,9 @@ export class BookStackClient { } const response = await this.client.put(`/attachments/${id}`, data); const attachment = response.data; - return { ...attachment, - page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`, - direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})` + download_url: `${this.baseUrl}/attachments/${attachment.id}` }; } diff --git a/src/index.ts b/src/index.ts index 0827632..4e6246e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -65,7 +65,7 @@ function registerResources(server: McpServer, client: BookStackClient): void { resources: (books.data ?? []).map((b: any) => ({ uri: `bookstack://book/${b.id}`, name: b.name, - description: b.summary ?? b.description ?? undefined, + description: b.description ?? undefined, mimeType: "application/json" })) }; @@ -79,7 +79,6 @@ function registerResources(server: McpServer, client: BookStackClient): void { } }), { - title: "BookStack Book", description: "A BookStack book exposed as an MCP resource", mimeType: "application/json" }, @@ -93,7 +92,7 @@ function registerResources(server: McpServer, client: BookStackClient): void { contents: [{ uri: uri.href, mimeType: "application/json", - text: JSON.stringify(book, null, 2) + text: JSON.stringify(book) }] }; } @@ -109,7 +108,7 @@ function registerResources(server: McpServer, client: BookStackClient): void { resources: (pages.data ?? []).map((p: any) => ({ uri: `bookstack://page/${p.id}`, name: p.name, - description: p.content_preview ?? p.location ?? undefined, + description: p.content_preview ?? undefined, mimeType: "text/markdown" })) }; @@ -123,7 +122,6 @@ function registerResources(server: McpServer, client: BookStackClient): void { } }), { - title: "BookStack Page", description: "A BookStack page exposed as an MCP resource (markdown content)", mimeType: "text/markdown" }, @@ -148,10 +146,10 @@ function registerResources(server: McpServer, client: BookStackClient): void { chapter_id: page.chapter_id, url: page.url, word_count: page.word_count, - last_updated_friendly: page.last_updated_friendly, + updated_at: page.updated_at, content_truncated: page.content_truncated, content_total_chars: page.content_total_chars - }, null, 2) + }) }] }; } @@ -167,44 +165,18 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS const writeTool: typeof server.registerTool = ((name: string, cfg: any, handler: any) => server.registerTool(name, { ...cfg, annotations: { ...WRITE_ANNOTATIONS, ...(cfg.annotations ?? {}) } }, handler)) as any; - // Register read-only tools - readTool( - "get_capabilities", - { - title: "Get BookStack Capabilities", - description: "Get information about available BookStack MCP capabilities and current configuration", - inputSchema: {} - }, - async () => { - const capabilities = { - server_name: "BookStack MCP Server", - version: PKG_VERSION, - write_operations_enabled: config.enableWrite, - available_tools: config.enableWrite ? "All tools enabled" : "Read-only tools only", - security_note: config.enableWrite - ? "⚠️ Write operations are ENABLED - AI can create and modify BookStack content" - : "🛡️ Read-only mode - Safe for production use" - }; - return { - content: [{ type: "text", text: JSON.stringify(capabilities, null, 2) }] - }; - } - ); - + // Register read-only tools. + // Common params (offset/count/sort/filter/id) are self-describing and intentionally + // bare so tool definitions stay compact in the MCP tools/list payload. readTool( "search_content", { - title: "Search BookStack Content", - description: "Search across BookStack content with contextual previews and location info. Supports BookStack's advanced search syntax — see the query parameter for caveats.", + description: "Search BookStack content. Supports advanced syntax like {type:page} or {book_id:5}. {created_by:X}/{updated_by:X}/{owned_by:X} need a numeric user ID — use find_users to resolve names.", inputSchema: { - query: z.string().describe( - "Search query. Supports BookStack advanced search syntax like {type:page} or {book_id:5}. " + - "Caveat: {created_by:X}, {updated_by:X}, and {owned_by:X} require a numeric user ID — pass a name and BookStack silently ignores the filter, returning the unfiltered query. " + - "Use the find_users tool to resolve names to IDs." - ), - type: z.enum(["book", "page", "chapter", "bookshelf"]).optional().describe("Filter by content type"), - count: z.coerce.number().max(500).optional().describe("Number of results to return (max 500)"), - offset: z.coerce.number().optional().describe("Number of results to skip for pagination") + query: z.string(), + type: z.enum(["book", "page", "chapter", "bookshelf"]).optional(), + count: z.coerce.number().max(500).optional(), + offset: z.coerce.number().optional() } }, async (args) => { @@ -213,26 +185,19 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS count: args.count, offset: args.offset }); - return { - content: [{ type: "text", text: JSON.stringify(results, null, 2) }] - }; + return { content: [{ type: "text", text: JSON.stringify(results) }] }; } ); readTool( "search_pages", { - title: "Search Pages", - description: "Search specifically for pages with optional book filtering. Same advanced-search caveats as search_content — see the query parameter.", + description: "Search BookStack pages, optionally within a book. Same user-ID caveat as search_content.", inputSchema: { - query: z.string().describe( - "Search query for pages. Supports BookStack advanced search syntax. " + - "Caveat: {created_by:X}, {updated_by:X}, and {owned_by:X} require a numeric user ID — pass a name and BookStack silently ignores the filter. " + - "Use the find_users tool to resolve names to IDs." - ), - book_id: z.coerce.number().optional().describe("Filter results to pages within a specific book"), - count: z.coerce.number().max(500).optional().describe("Number of results to return"), - offset: z.coerce.number().optional().describe("Pagination offset") + query: z.string(), + book_id: z.coerce.number().optional(), + count: z.coerce.number().max(500).optional(), + offset: z.coerce.number().optional() } }, async (args) => { @@ -241,22 +206,19 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS count: args.count, offset: args.offset }); - return { - content: [{ type: "text", text: JSON.stringify(results, null, 2) }] - }; + return { content: [{ type: "text", text: JSON.stringify(results) }] }; } ); readTool( "get_books", { - title: "List Books", - description: "List available books with advanced filtering and sorting", + description: "List books with optional filter/sort.", inputSchema: { - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field (e.g., 'name', '-created_at', 'updated_at')"), - filter: z.record(z.any()).optional().describe("Filter criteria") + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional(), + filter: z.record(z.any()).optional() } }, async (args) => { @@ -267,7 +229,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS filter: args.filter }); return { - content: [{ type: "text", text: JSON.stringify(books, null, 2) }] + content: [{ type: "text", text: JSON.stringify(books) }] }; } ); @@ -275,16 +237,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_book", { - title: "Get Book Details", - description: "Get detailed information about a specific book", + description: "Get a book.", inputSchema: { - id: z.coerce.number().min(1).describe("Book ID") + id: z.coerce.number().min(1) } }, async (args) => { const book = await client.getBook(args.id); return { - content: [{ type: "text", text: JSON.stringify(book, null, 2) }] + content: [{ type: "text", text: JSON.stringify(book) }] }; } ); @@ -292,15 +253,14 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_pages", { - title: "List Pages", - description: "List pages with content previews, word counts, and contextual information", + description: "List pages with previews.", inputSchema: { - book_id: z.coerce.number().optional().describe("Filter by book ID"), - chapter_id: z.coerce.number().optional().describe("Filter by chapter ID"), - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field"), - filter: z.record(z.any()).optional().describe("Additional filter criteria") + book_id: z.coerce.number().optional(), + chapter_id: z.coerce.number().optional(), + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional(), + filter: z.record(z.any()).optional() } }, async (args) => { @@ -313,7 +273,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS filter: args.filter }); return { - content: [{ type: "text", text: JSON.stringify(pages, null, 2) }] + content: [{ type: "text", text: JSON.stringify(pages) }] }; } ); @@ -321,10 +281,9 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_page", { - title: "Get Page Content", - description: "Get content of a specific page. Returns one content format (default markdown) and supports character-range pagination so large pages don't blow the context window. Use offset/limit or the returned content_next_offset to page through long content.", + description: "Get a page. Returns one format (default markdown); use offset/limit + content_next_offset to paginate large pages.", inputSchema: { - id: z.coerce.number().min(1).describe("Page ID"), + id: z.coerce.number().min(1), format: z.enum(["markdown", "html", "text"]).optional().describe("Which content format to return. Defaults to markdown."), offset: z.coerce.number().min(0).optional().describe("Character offset into the content to start from (default 0)"), limit: z.coerce.number().min(1).max(200000).optional().describe("Max characters of content to return (default 50000)") @@ -337,7 +296,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS limit: args.limit }); return { - content: [{ type: "text", text: JSON.stringify(page, null, 2) }] + content: [{ type: "text", text: JSON.stringify(page) }] }; } ); @@ -345,18 +304,17 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_chapters", { - title: "List Chapters", - description: "List chapters, optionally filtered by book", + description: "List chapters; optional book_id.", inputSchema: { - book_id: z.coerce.number().optional().describe("Filter by book ID"), - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().default(50).describe("Number of results to return") + book_id: z.coerce.number().optional(), + offset: z.coerce.number().default(0), + count: z.coerce.number().default(50) } }, async (args) => { const chapters = await client.getChapters(args.book_id, args.offset, args.count); return { - content: [{ type: "text", text: JSON.stringify(chapters, null, 2) }] + content: [{ type: "text", text: JSON.stringify(chapters) }] }; } ); @@ -364,16 +322,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_chapter", { - title: "Get Chapter Details", - description: "Get details of a specific chapter", + description: "Get a chapter.", inputSchema: { - id: z.coerce.number().min(1).describe("Chapter ID") + id: z.coerce.number().min(1) } }, async (args) => { const chapter = await client.getChapter(args.id); return { - content: [{ type: "text", text: JSON.stringify(chapter, null, 2) }] + content: [{ type: "text", text: JSON.stringify(chapter) }] }; } ); @@ -381,34 +338,20 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "export_page", { - title: "Export Page", - description: "Export a page in various formats (PDF/ZIP provide direct BookStack download URLs)", + description: "Export a page (PDF/ZIP return download URL).", inputSchema: { - id: z.coerce.number().min(1).describe("Page ID"), - format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]).describe("Export format") + id: z.coerce.number().min(1), + format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]) } }, async (args) => { const content = await client.exportPage(args.id, args.format); - // Handle binary formats with direct URLs - if (typeof content === 'object' && content.download_url && content.direct_download) { - const format = args.format.toUpperCase(); - return { - content: [{ - type: "text", - text: `✅ **${format} Export Ready**\n\n` + - `📄 **Page:** ${content.page_name}\n` + - `📚 **Book:** ${content.book_name}\n` + - `📁 **File:** ${content.filename}\n\n` + - `🚀 **Direct Download Link:**\n${content.download_url}\n\n` + - `ℹ️ **Note:** ${content.note}` - }] - }; + if (typeof content === 'object' && content.download_url) { + return { content: [{ type: "text", text: JSON.stringify(content) }] }; } - // Handle text formats - const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2); + const text = typeof content === 'string' ? content : JSON.stringify(content); return { content: [{ type: "text", text }] }; @@ -418,31 +361,20 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "export_book", { - title: "Export Book", - description: "Export an entire book in various formats", + description: "Export a book.", inputSchema: { - id: z.coerce.number().min(1).describe("Book ID"), - format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]).describe("Export format") + id: z.coerce.number().min(1), + format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]) } }, async (args) => { const content = await client.exportBook(args.id, args.format); if (typeof content === 'object' && content.download_url) { - const format = args.format.toUpperCase(); - return { - content: [{ - type: "text", - text: `✅ **${format} Book Export Ready**\n\n` + - `📚 **Book:** ${content.book_name}\n` + - `📁 **File:** ${content.filename}\n\n` + - `🚀 **Direct Download Link:**\n${content.download_url}\n\n` + - `ℹ️ **Note:** ${content.note}` - }] - }; + return { content: [{ type: "text", text: JSON.stringify(content) }] }; } - const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2); + const text = typeof content === 'string' ? content : JSON.stringify(content); return { content: [{ type: "text", text }] }; @@ -452,32 +384,20 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "export_chapter", { - title: "Export Chapter", - description: "Export a chapter in various formats", + description: "Export a chapter.", inputSchema: { - id: z.coerce.number().min(1).describe("Chapter ID"), - format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]).describe("Export format") + id: z.coerce.number().min(1), + format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]) } }, async (args) => { const content = await client.exportChapter(args.id, args.format); if (typeof content === 'object' && content.download_url) { - const format = args.format.toUpperCase(); - return { - content: [{ - type: "text", - text: `✅ **${format} Chapter Export Ready**\n\n` + - `📖 **Chapter:** ${content.chapter_name}\n` + - `📚 **Book:** ${content.book_name}\n` + - `📁 **File:** ${content.filename}\n\n` + - `🚀 **Direct Download Link:**\n${content.download_url}\n\n` + - `ℹ️ **Note:** ${content.note}` - }] - }; + return { content: [{ type: "text", text: JSON.stringify(content) }] }; } - const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2); + const text = typeof content === 'string' ? content : JSON.stringify(content); return { content: [{ type: "text", text }] }; @@ -487,12 +407,11 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_recent_changes", { - title: "Get Recent Changes", - description: "Get recently updated content with contextual previews and change descriptions", + description: "List content updated in the last N days.", inputSchema: { - type: z.enum(["all", "page", "book", "chapter"]).default("all").describe("Filter by content type"), - limit: z.coerce.number().max(100).default(20).describe("Number of recent items to return"), - days: z.coerce.number().default(30).describe("Number of days back to look for changes") + type: z.enum(["all", "page", "book", "chapter"]).default("all"), + limit: z.coerce.number().max(100).default(20), + days: z.coerce.number().default(30) } }, async (args) => { @@ -502,7 +421,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS days: args.days }); return { - content: [{ type: "text", text: JSON.stringify(changes, null, 2) }] + content: [{ type: "text", text: JSON.stringify(changes) }] }; } ); @@ -510,13 +429,12 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_shelves", { - title: "List Shelves", - description: "List available book shelves (collections) with filtering and sorting", + description: "List shelves (book collections).", inputSchema: { - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field"), - filter: z.record(z.any()).optional().describe("Filter criteria") + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional(), + filter: z.record(z.any()).optional() } }, async (args) => { @@ -527,7 +445,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS filter: args.filter }); return { - content: [{ type: "text", text: JSON.stringify(shelves, null, 2) }] + content: [{ type: "text", text: JSON.stringify(shelves) }] }; } ); @@ -535,16 +453,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_shelf", { - title: "Get Shelf Details", - description: "Get details of a specific book shelf including all books", + description: "Get a shelf including its books.", inputSchema: { - id: z.coerce.number().min(1).describe("Shelf ID") + id: z.coerce.number().min(1) } }, async (args) => { const shelf = await client.getShelf(args.id); return { - content: [{ type: "text", text: JSON.stringify(shelf, null, 2) }] + content: [{ type: "text", text: JSON.stringify(shelf) }] }; } ); @@ -552,13 +469,12 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_attachments", { - title: "List Attachments", - description: "List attachments (files and links) with filtering and sorting", + description: "List attachments (files and links).", inputSchema: { - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field"), - filter: z.record(z.any()).optional().describe("Filter criteria") + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional(), + filter: z.record(z.any()).optional() } }, async (args) => { @@ -569,7 +485,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS filter: args.filter }); return { - content: [{ type: "text", text: JSON.stringify(attachments, null, 2) }] + content: [{ type: "text", text: JSON.stringify(attachments) }] }; } ); @@ -577,16 +493,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_attachment", { - title: "Get Attachment Details", - description: "Get details of a specific attachment including download links", + description: "Get an attachment with download_url.", inputSchema: { - id: z.coerce.number().min(1).describe("Attachment ID") + id: z.coerce.number().min(1) } }, async (args) => { const attachment = await client.getAttachment(args.id); return { - content: [{ type: "text", text: JSON.stringify(attachment, null, 2) }] + content: [{ type: "text", text: JSON.stringify(attachment) }] }; } ); @@ -594,13 +509,12 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_comments", { - title: "List Comments", - description: "List comments, optionally filtered to a single page. Requires BookStack v25.11+.", + description: "List comments (optional page_id). BookStack v25.11+.", inputSchema: { - page_id: z.coerce.number().optional().describe("Filter to comments on this page"), - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field") + page_id: z.coerce.number().optional(), + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional() } }, async (args) => { @@ -611,7 +525,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS sort: args.sort }); return { - content: [{ type: "text", text: JSON.stringify(comments, null, 2) }] + content: [{ type: "text", text: JSON.stringify(comments) }] }; } ); @@ -619,16 +533,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_comment", { - title: "Get Comment", - description: "Get a single comment by ID. Requires BookStack v25.11+.", + description: "Get a comment. BookStack v25.11+.", inputSchema: { - id: z.coerce.number().min(1).describe("Comment ID") + id: z.coerce.number().min(1) } }, async (args) => { const comment = await client.getComment(args.id); return { - content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] + content: [{ type: "text", text: JSON.stringify(comment) }] }; } ); @@ -636,15 +549,14 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "find_users", { - title: "Find Users", - description: "List BookStack users with optional filtering by name, email, or slug. Use this to resolve a person's name into the numeric user ID needed for {created_by:X}/{updated_by:X}/{owned_by:X} search filters and for the filter parameter on get_pages, get_books, etc. Requires admin permissions on the API token.", + description: "List users; filter by name/email/slug (partial match). Resolves names to numeric user IDs for {created_by:X}/{updated_by:X}/{owned_by:X} search filters. Requires admin token.", inputSchema: { - name: z.string().optional().describe("Filter by user display name (partial match)"), - email: z.string().optional().describe("Filter by email address (partial match)"), - slug: z.string().optional().describe("Filter by URL slug (e.g. \"jane-doe\")"), - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field (e.g. 'name', '-created_at')") + name: z.string().optional(), + email: z.string().optional(), + slug: z.string().optional(), + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional() } }, async (args) => { @@ -659,7 +571,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS filter: Object.keys(filter).length ? filter : undefined }); return { - content: [{ type: "text", text: JSON.stringify(users, null, 2) }] + content: [{ type: "text", text: JSON.stringify(users) }] }; } ); @@ -667,12 +579,11 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS readTool( "get_recycle_bin", { - title: "List Recycle Bin", - description: "List items in the recycle bin (deleted books, chapters, pages, shelves). Requires admin permissions on the API token.", + description: "List recycle-bin items. Admin token required.", inputSchema: { - offset: z.coerce.number().default(0).describe("Pagination offset"), - count: z.coerce.number().max(500).default(50).describe("Number of results to return"), - sort: z.string().optional().describe("Sort field") + offset: z.coerce.number().default(0), + count: z.coerce.number().max(500).default(50), + sort: z.string().optional() } }, async (args) => { @@ -682,7 +593,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS sort: args.sort }); return { - content: [{ type: "text", text: JSON.stringify(items, null, 2) }] + content: [{ type: "text", text: JSON.stringify(items) }] }; } ); @@ -692,7 +603,6 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "create_book", { - title: "Create Book", description: "Create a new book in BookStack", inputSchema: { name: z.string().describe("Book name"), @@ -710,7 +620,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS tags: args.tags as any }); return { - content: [{ type: "text", text: JSON.stringify(book, null, 2) }] + content: [{ type: "text", text: JSON.stringify(book) }] }; } ); @@ -718,7 +628,6 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "create_chapter", { - title: "Create Chapter", description: "Create a new chapter within a book", inputSchema: { book_id: z.coerce.number().min(1).describe("Book ID where the chapter will be created"), @@ -738,7 +647,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS tags: args.tags as any }); return { - content: [{ type: "text", text: JSON.stringify(chapter, null, 2) }] + content: [{ type: "text", text: JSON.stringify(chapter) }] }; } ); @@ -746,7 +655,6 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "create_page", { - title: "Create Page", description: "Create a new page in BookStack", inputSchema: { name: z.string().describe("Page name"), @@ -765,7 +673,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS markdown: args.markdown }); return { - content: [{ type: "text", text: JSON.stringify(page, null, 2) }] + content: [{ type: "text", text: JSON.stringify(page) }] }; } ); @@ -773,10 +681,9 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "update_page", { - title: "Update Page", description: "Update an existing page. Pass book_id (and optionally chapter_id) to move the page to a different location.", inputSchema: { - id: z.coerce.number().min(1).describe("Page ID"), + id: z.coerce.number().min(1), name: z.string().optional().describe("Optional: New page name"), html: z.string().optional().describe("Optional: New HTML content"), markdown: z.string().optional().describe("Optional: New Markdown content"), @@ -793,7 +700,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS chapter_id: args.chapter_id }); return { - content: [{ type: "text", text: JSON.stringify(page, null, 2) }] + content: [{ type: "text", text: JSON.stringify(page) }] }; } ); @@ -801,7 +708,6 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "create_shelf", { - title: "Create Shelf", description: "Create a new book shelf (collection)", inputSchema: { name: z.string().describe("Shelf name"), @@ -821,7 +727,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS tags: args.tags as any }); return { - content: [{ type: "text", text: JSON.stringify(shelf, null, 2) }] + content: [{ type: "text", text: JSON.stringify(shelf) }] }; } ); @@ -829,10 +735,9 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "update_shelf", { - title: "Update Shelf", description: "Update an existing book shelf", inputSchema: { - id: z.coerce.number().min(1).describe("Shelf ID"), + id: z.coerce.number().min(1), name: z.string().optional().describe("New shelf name"), description: z.string().optional().describe("New shelf description"), books: z.array(z.coerce.number()).optional().describe("Array of book IDs"), @@ -850,7 +755,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS tags: args.tags as any }); return { - content: [{ type: "text", text: JSON.stringify(shelf, null, 2) }] + content: [{ type: "text", text: JSON.stringify(shelf) }] }; } ); @@ -858,16 +763,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "delete_shelf", { - title: "Delete Shelf", description: "Delete a book shelf (collection)", inputSchema: { - id: z.coerce.number().min(1).describe("Shelf ID") + id: z.coerce.number().min(1) } }, async (args) => { const result = await client.deleteShelf(args.id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -875,7 +779,6 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "create_attachment", { - title: "Create Attachment", description: "Create a new link attachment to a page", inputSchema: { name: z.string().describe("Attachment name"), @@ -890,7 +793,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS link: args.link }); return { - content: [{ type: "text", text: JSON.stringify(attachment, null, 2) }] + content: [{ type: "text", text: JSON.stringify(attachment) }] }; } ); @@ -898,10 +801,9 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "update_attachment", { - title: "Update Attachment", description: "Update an existing attachment", inputSchema: { - id: z.coerce.number().min(1).describe("Attachment ID"), + id: z.coerce.number().min(1), name: z.string().optional().describe("New attachment name"), link: z.string().optional().describe("New URL for link attachment"), uploaded_to: z.coerce.number().optional().describe("Move attachment to different page") @@ -914,7 +816,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS uploaded_to: args.uploaded_to }); return { - content: [{ type: "text", text: JSON.stringify(attachment, null, 2) }] + content: [{ type: "text", text: JSON.stringify(attachment) }] }; } ); @@ -922,16 +824,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "delete_attachment", { - title: "Delete Attachment", description: "Delete an attachment", inputSchema: { - id: z.coerce.number().min(1).describe("Attachment ID") + id: z.coerce.number().min(1) } }, async (args) => { const result = await client.deleteAttachment(args.id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -939,16 +840,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "delete_book", { - title: "Delete Book", description: "Delete a book. Goes to the recycle bin and can be restored from there.", inputSchema: { - id: z.coerce.number().min(1).describe("Book ID") + id: z.coerce.number().min(1) } }, async (args) => { const result = await client.deleteBook(args.id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -956,16 +856,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "delete_chapter", { - title: "Delete Chapter", description: "Delete a chapter. Goes to the recycle bin and can be restored from there.", inputSchema: { - id: z.coerce.number().min(1).describe("Chapter ID") + id: z.coerce.number().min(1) } }, async (args) => { const result = await client.deleteChapter(args.id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -973,16 +872,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "delete_page", { - title: "Delete Page", description: "Delete a page. Goes to the recycle bin and can be restored from there.", inputSchema: { - id: z.coerce.number().min(1).describe("Page ID") + id: z.coerce.number().min(1) } }, async (args) => { const result = await client.deletePage(args.id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -990,7 +888,6 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "create_comment", { - title: "Create Comment", description: "Add a comment to a page. Pass parent_id to reply to an existing comment. Requires BookStack v25.11+.", inputSchema: { page_id: z.coerce.number().min(1).describe("Page ID to comment on"), @@ -1007,7 +904,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS content_ref: args.content_ref }); return { - content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] + content: [{ type: "text", text: JSON.stringify(comment) }] }; } ); @@ -1015,10 +912,9 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "update_comment", { - title: "Update Comment", description: "Edit a comment's body or archive/unarchive it. Requires BookStack v25.11+.", inputSchema: { - id: z.coerce.number().min(1).describe("Comment ID"), + id: z.coerce.number().min(1), html: z.string().optional().describe("Optional: new HTML body"), archived: z.boolean().optional().describe("Optional: archive (true) or unarchive (false) the comment") } @@ -1029,7 +925,7 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS archived: args.archived }); return { - content: [{ type: "text", text: JSON.stringify(comment, null, 2) }] + content: [{ type: "text", text: JSON.stringify(comment) }] }; } ); @@ -1037,16 +933,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "delete_comment", { - title: "Delete Comment", description: "Delete a comment. Requires BookStack v25.11+.", inputSchema: { - id: z.coerce.number().min(1).describe("Comment ID") + id: z.coerce.number().min(1) } }, async (args) => { const result = await client.deleteComment(args.id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -1054,16 +949,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "restore_deleted", { - title: "Restore from Recycle Bin", description: "Restore a deleted item from the recycle bin. Use the deletion ID from get_recycle_bin (not the original entity ID). Requires admin permissions.", inputSchema: { - deletion_id: z.coerce.number().min(1).describe("Deletion ID (from get_recycle_bin)") + deletion_id: z.coerce.number().min(1) } }, async (args) => { const result = await client.restoreFromRecycleBin(args.deletion_id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } ); @@ -1071,16 +965,15 @@ function registerTools(server: McpServer, client: BookStackClient, config: BookS writeTool( "permanently_delete", { - title: "Permanently Delete from Recycle Bin", description: "PERMANENTLY destroys a deleted item — this cannot be undone. Use the deletion ID from get_recycle_bin. Requires admin permissions.", inputSchema: { - deletion_id: z.coerce.number().min(1).describe("Deletion ID (from get_recycle_bin)") + deletion_id: z.coerce.number().min(1) } }, async (args) => { const result = await client.destroyFromRecycleBin(args.deletion_id); return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }] + content: [{ type: "text", text: JSON.stringify(result) }] }; } );