bookstack-mcp 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/bookstack-client.js +552 -0
- package/dist/index.js +507 -0
- package/package.json +53 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 BookStack MCP Server
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# BookStack MCP Server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that gives AI assistants full access to your [BookStack](https://www.bookstackapp.com) documentation — search, read, create, and manage content.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 17 read-only tools + 8 write tools for complete BookStack API coverage
|
|
8
|
+
- Type-safe input validation with Zod (auto-coerces string/number params for broad client compatibility)
|
|
9
|
+
- Embedded URLs and content previews in all responses
|
|
10
|
+
- Write operations disabled by default for safety
|
|
11
|
+
- Works with Claude Desktop, LibreChat, and any MCP-compatible client
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### Install from npm
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npx bookstack-mcp
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Or clone and build
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
git clone https://github.com/ttpears/bookstack-mcp.git
|
|
25
|
+
cd bookstack-mcp
|
|
26
|
+
npm install && npm run build
|
|
27
|
+
npm start
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Environment Variables
|
|
31
|
+
|
|
32
|
+
```env
|
|
33
|
+
BOOKSTACK_BASE_URL=https://your-bookstack.com # Required
|
|
34
|
+
BOOKSTACK_TOKEN_ID=your-token-id # Required
|
|
35
|
+
BOOKSTACK_TOKEN_SECRET=your-token-secret # Required
|
|
36
|
+
BOOKSTACK_ENABLE_WRITE=false # Optional, default false
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Client Configuration
|
|
40
|
+
|
|
41
|
+
### Claude Desktop
|
|
42
|
+
|
|
43
|
+
Add to your Claude Desktop config:
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"mcpServers": {
|
|
48
|
+
"bookstack": {
|
|
49
|
+
"command": "npx",
|
|
50
|
+
"args": ["-y", "bookstack-mcp"],
|
|
51
|
+
"env": {
|
|
52
|
+
"BOOKSTACK_BASE_URL": "https://your-bookstack.com",
|
|
53
|
+
"BOOKSTACK_TOKEN_ID": "your-token-id",
|
|
54
|
+
"BOOKSTACK_TOKEN_SECRET": "your-token-secret"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### LibreChat
|
|
62
|
+
|
|
63
|
+
Add to your `librechat.yaml`:
|
|
64
|
+
|
|
65
|
+
```yaml
|
|
66
|
+
mcpServers:
|
|
67
|
+
bookstack:
|
|
68
|
+
command: npx
|
|
69
|
+
args:
|
|
70
|
+
- -y
|
|
71
|
+
- bookstack-mcp
|
|
72
|
+
env:
|
|
73
|
+
BOOKSTACK_BASE_URL: "https://your-bookstack.com"
|
|
74
|
+
BOOKSTACK_TOKEN_ID: "your-token-id"
|
|
75
|
+
BOOKSTACK_TOKEN_SECRET: "your-token-secret"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Restart LibreChat after config changes.
|
|
79
|
+
|
|
80
|
+
## Available Tools
|
|
81
|
+
|
|
82
|
+
### Read Operations (always available)
|
|
83
|
+
|
|
84
|
+
| Tool | Description |
|
|
85
|
+
|------|-------------|
|
|
86
|
+
| `get_capabilities` | Server capabilities and configuration |
|
|
87
|
+
| `search_content` | Search across all content with filtering |
|
|
88
|
+
| `search_pages` | Search pages with optional book filtering |
|
|
89
|
+
| `get_books` / `get_book` | List or get details of books |
|
|
90
|
+
| `get_pages` / `get_page` | List or get full page content |
|
|
91
|
+
| `get_chapters` / `get_chapter` | List or get chapter details |
|
|
92
|
+
| `get_shelves` / `get_shelf` | List or get shelf details |
|
|
93
|
+
| `get_attachments` / `get_attachment` | List or get attachment details |
|
|
94
|
+
| `export_page` | Export page as HTML, PDF, Markdown, plaintext, or ZIP |
|
|
95
|
+
| `export_book` | Export entire book |
|
|
96
|
+
| `export_chapter` | Export chapter |
|
|
97
|
+
| `get_recent_changes` | Recently updated content |
|
|
98
|
+
|
|
99
|
+
### Write Operations (requires `BOOKSTACK_ENABLE_WRITE=true`)
|
|
100
|
+
|
|
101
|
+
| Tool | Description |
|
|
102
|
+
|------|-------------|
|
|
103
|
+
| `create_page` / `update_page` | Create or update pages |
|
|
104
|
+
| `create_shelf` / `update_shelf` / `delete_shelf` | Manage shelves |
|
|
105
|
+
| `create_attachment` / `update_attachment` / `delete_attachment` | Manage attachments |
|
|
106
|
+
|
|
107
|
+
## BookStack API Setup
|
|
108
|
+
|
|
109
|
+
1. Log into BookStack as an admin
|
|
110
|
+
2. Go to **Settings > Users > Edit your user**
|
|
111
|
+
3. Ensure the user has **Access System API** permission
|
|
112
|
+
4. In the **API Tokens** section, create a new token
|
|
113
|
+
5. Copy the Token ID and Token Secret
|
|
114
|
+
|
|
115
|
+
## Security
|
|
116
|
+
|
|
117
|
+
- Write operations are **disabled by default**
|
|
118
|
+
- Use HTTPS for production instances
|
|
119
|
+
- Store API tokens securely (never commit to git)
|
|
120
|
+
- Consider a dedicated BookStack user with limited permissions
|
|
121
|
+
|
|
122
|
+
## Development
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npm run dev # Hot reload with tsx
|
|
126
|
+
npm run type-check # Type checking only
|
|
127
|
+
npm run build # Production build
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
export class BookStackClient {
|
|
3
|
+
client;
|
|
4
|
+
enableWrite;
|
|
5
|
+
baseUrl;
|
|
6
|
+
bookSlugCache = new Map();
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.enableWrite = config.enableWrite || false;
|
|
9
|
+
this.baseUrl = config.baseUrl;
|
|
10
|
+
this.client = axios.create({
|
|
11
|
+
baseURL: `${config.baseUrl}/api`,
|
|
12
|
+
headers: {
|
|
13
|
+
'Authorization': `Token ${config.tokenId}:${config.tokenSecret}`,
|
|
14
|
+
'Content-Type': 'application/json'
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
async getBookSlug(bookId) {
|
|
19
|
+
// Check cache first
|
|
20
|
+
if (this.bookSlugCache.has(bookId)) {
|
|
21
|
+
return this.bookSlugCache.get(bookId);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const response = await this.client.get(`/books/${bookId}`);
|
|
25
|
+
const slug = response.data.slug || String(bookId);
|
|
26
|
+
this.bookSlugCache.set(bookId, slug);
|
|
27
|
+
return slug;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
// Fallback to ID if book fetch fails
|
|
31
|
+
return String(bookId);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// URL generation utilities
|
|
35
|
+
generateBookUrl(book) {
|
|
36
|
+
return `${this.baseUrl}/books/${book.slug || book.id}`;
|
|
37
|
+
}
|
|
38
|
+
async generatePageUrl(page) {
|
|
39
|
+
const bookSlug = await this.getBookSlug(page.book_id);
|
|
40
|
+
return `${this.baseUrl}/books/${bookSlug}/page/${page.slug || page.id}`;
|
|
41
|
+
}
|
|
42
|
+
async generateChapterUrl(chapter) {
|
|
43
|
+
const bookSlug = await this.getBookSlug(chapter.book_id);
|
|
44
|
+
return `${this.baseUrl}/books/${bookSlug}/chapter/${chapter.slug || chapter.id}`;
|
|
45
|
+
}
|
|
46
|
+
generateShelfUrl(shelf) {
|
|
47
|
+
return `${this.baseUrl}/shelves/${shelf.slug || shelf.id}`;
|
|
48
|
+
}
|
|
49
|
+
generateSearchUrl(query) {
|
|
50
|
+
const encodedQuery = encodeURIComponent(query);
|
|
51
|
+
return `${this.baseUrl}/search?term=${encodedQuery}`;
|
|
52
|
+
}
|
|
53
|
+
// Enhanced response helpers
|
|
54
|
+
enhanceBookResponse(book) {
|
|
55
|
+
const lastUpdated = this.formatDate(book.updated_at);
|
|
56
|
+
const created = this.formatDate(book.created_at);
|
|
57
|
+
return {
|
|
58
|
+
...book,
|
|
59
|
+
url: this.generateBookUrl(book),
|
|
60
|
+
direct_link: `[${book.name}](${this.generateBookUrl(book)})`,
|
|
61
|
+
last_updated_friendly: lastUpdated,
|
|
62
|
+
created_friendly: created,
|
|
63
|
+
summary: book.description ? `${book.description.substring(0, 100)}${book.description.length > 100 ? '...' : ''}` : 'No description available',
|
|
64
|
+
content_info: `Book created ${created}, last updated ${lastUpdated}`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async enhancePageResponse(page) {
|
|
68
|
+
const lastUpdated = this.formatDate(page.updated_at);
|
|
69
|
+
const created = this.formatDate(page.created_at);
|
|
70
|
+
const contentPreview = page.text ? `${page.text.substring(0, 200)}${page.text.length > 200 ? '...' : ''}` : 'No content preview available';
|
|
71
|
+
const url = await this.generatePageUrl(page);
|
|
72
|
+
return {
|
|
73
|
+
...page,
|
|
74
|
+
url,
|
|
75
|
+
direct_link: `[${page.name}](${url})`,
|
|
76
|
+
last_updated_friendly: lastUpdated,
|
|
77
|
+
created_friendly: created,
|
|
78
|
+
content_preview: contentPreview,
|
|
79
|
+
content_info: `Page created ${created}, last updated ${lastUpdated}`,
|
|
80
|
+
word_count: page.text ? page.text.split(' ').length : 0,
|
|
81
|
+
location: `Book ID ${page.book_id}${page.chapter_id ? `, Chapter ID ${page.chapter_id}` : ''}`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async enhanceChapterResponse(chapter) {
|
|
85
|
+
const lastUpdated = this.formatDate(chapter.updated_at);
|
|
86
|
+
const created = this.formatDate(chapter.created_at);
|
|
87
|
+
const url = await this.generateChapterUrl(chapter);
|
|
88
|
+
return {
|
|
89
|
+
...chapter,
|
|
90
|
+
url,
|
|
91
|
+
direct_link: `[${chapter.name}](${url})`,
|
|
92
|
+
last_updated_friendly: lastUpdated,
|
|
93
|
+
created_friendly: created,
|
|
94
|
+
summary: chapter.description ? `${chapter.description.substring(0, 100)}${chapter.description.length > 100 ? '...' : ''}` : 'No description available',
|
|
95
|
+
content_info: `Chapter created ${created}, last updated ${lastUpdated}`,
|
|
96
|
+
location: `In Book ID ${chapter.book_id}`
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
enhanceShelfResponse(shelf) {
|
|
100
|
+
const lastUpdated = this.formatDate(shelf.updated_at);
|
|
101
|
+
const created = this.formatDate(shelf.created_at);
|
|
102
|
+
const bookCount = shelf.books?.length || 0;
|
|
103
|
+
return {
|
|
104
|
+
...shelf,
|
|
105
|
+
url: this.generateShelfUrl(shelf),
|
|
106
|
+
direct_link: `[${shelf.name}](${this.generateShelfUrl(shelf)})`,
|
|
107
|
+
last_updated_friendly: lastUpdated,
|
|
108
|
+
created_friendly: created,
|
|
109
|
+
summary: shelf.description ? `${shelf.description.substring(0, 100)}${shelf.description.length > 100 ? '...' : ''}` : 'No description available',
|
|
110
|
+
content_info: `Shelf with ${bookCount} book${bookCount !== 1 ? 's' : ''}, created ${created}, last updated ${lastUpdated}`,
|
|
111
|
+
book_count: bookCount,
|
|
112
|
+
books: shelf.books?.map(book => this.enhanceBookResponse(book)),
|
|
113
|
+
tags_summary: shelf.tags?.length ? `Tagged with: ${shelf.tags.map(t => `${t.name}${t.value ? `=${t.value}` : ''}`).join(', ')}` : 'No tags'
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async enhanceSearchResults(results, originalQuery) {
|
|
117
|
+
const enhancedResults = await Promise.all(results.map(async (result) => {
|
|
118
|
+
const url = await this.generateContentUrl(result);
|
|
119
|
+
return {
|
|
120
|
+
...result,
|
|
121
|
+
url,
|
|
122
|
+
direct_link: `[${result.name}](${url})`,
|
|
123
|
+
content_preview: result.preview_content?.content ? `${result.preview_content.content.substring(0, 150)}${result.preview_content.content.length > 150 ? '...' : ''}` : 'No preview available',
|
|
124
|
+
content_type: result.type.charAt(0).toUpperCase() + result.type.slice(1),
|
|
125
|
+
location_info: result.book_id ? `In book ID ${result.book_id}${result.chapter_id ? `, chapter ID ${result.chapter_id}` : ''}` : 'Location unknown'
|
|
126
|
+
};
|
|
127
|
+
}));
|
|
128
|
+
return {
|
|
129
|
+
search_query: originalQuery,
|
|
130
|
+
search_url: this.generateSearchUrl(originalQuery),
|
|
131
|
+
summary: `Found ${results.length} results for "${originalQuery}"`,
|
|
132
|
+
results: enhancedResults
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
async generateContentUrl(result) {
|
|
136
|
+
switch (result.type) {
|
|
137
|
+
case 'page':
|
|
138
|
+
if (result.book_id) {
|
|
139
|
+
const bookSlug = await this.getBookSlug(result.book_id);
|
|
140
|
+
return `${this.baseUrl}/books/${bookSlug}/page/${result.slug || result.id}`;
|
|
141
|
+
}
|
|
142
|
+
return `${this.baseUrl}/link/${result.id}`;
|
|
143
|
+
case 'chapter':
|
|
144
|
+
if (result.book_id) {
|
|
145
|
+
const bookSlug = await this.getBookSlug(result.book_id);
|
|
146
|
+
return `${this.baseUrl}/books/${bookSlug}/chapter/${result.slug || result.id}`;
|
|
147
|
+
}
|
|
148
|
+
return `${this.baseUrl}/link/${result.id}`;
|
|
149
|
+
case 'book':
|
|
150
|
+
return `${this.baseUrl}/books/${result.slug || result.id}`;
|
|
151
|
+
case 'bookshelf':
|
|
152
|
+
case 'shelf':
|
|
153
|
+
return `${this.baseUrl}/shelves/${result.slug || result.id}`;
|
|
154
|
+
default:
|
|
155
|
+
return `${this.baseUrl}/link/${result.id}`;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async searchContent(query, options) {
|
|
159
|
+
let searchQuery = query;
|
|
160
|
+
// Use advanced search syntax for type filtering
|
|
161
|
+
if (options?.type) {
|
|
162
|
+
searchQuery = `{type:${options.type}} ${query}`.trim();
|
|
163
|
+
}
|
|
164
|
+
const params = { query: searchQuery };
|
|
165
|
+
if (options?.count)
|
|
166
|
+
params.count = Math.min(options.count, 500); // BookStack max
|
|
167
|
+
if (options?.offset)
|
|
168
|
+
params.offset = options.offset;
|
|
169
|
+
const response = await this.client.get('/search', { params });
|
|
170
|
+
const results = response.data.data || response.data;
|
|
171
|
+
return await this.enhanceSearchResults(results, query);
|
|
172
|
+
}
|
|
173
|
+
async searchPages(query, options) {
|
|
174
|
+
let searchQuery = `{type:page} ${query}`.trim();
|
|
175
|
+
// Add book filtering if specified
|
|
176
|
+
if (options?.bookId) {
|
|
177
|
+
searchQuery = `{book_id:${options.bookId}} ${searchQuery}`;
|
|
178
|
+
}
|
|
179
|
+
const params = { query: searchQuery };
|
|
180
|
+
if (options?.count)
|
|
181
|
+
params.count = Math.min(options.count, 500);
|
|
182
|
+
if (options?.offset)
|
|
183
|
+
params.offset = options.offset;
|
|
184
|
+
const response = await this.client.get('/search', { params });
|
|
185
|
+
const results = response.data.data || response.data;
|
|
186
|
+
return await this.enhanceSearchResults(results, query);
|
|
187
|
+
}
|
|
188
|
+
async getBooks(options) {
|
|
189
|
+
const params = {
|
|
190
|
+
offset: options?.offset || 0,
|
|
191
|
+
count: Math.min(options?.count || 50, 500)
|
|
192
|
+
};
|
|
193
|
+
if (options?.sort)
|
|
194
|
+
params.sort = options.sort;
|
|
195
|
+
if (options?.filter)
|
|
196
|
+
params.filter = JSON.stringify(options.filter);
|
|
197
|
+
const response = await this.client.get('/books', { params });
|
|
198
|
+
const data = response.data;
|
|
199
|
+
return {
|
|
200
|
+
...data,
|
|
201
|
+
data: data.data.map((book) => this.enhanceBookResponse(book))
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
async getBook(id) {
|
|
205
|
+
const response = await this.client.get(`/books/${id}`);
|
|
206
|
+
return this.enhanceBookResponse(response.data);
|
|
207
|
+
}
|
|
208
|
+
async getPages(options) {
|
|
209
|
+
const params = {
|
|
210
|
+
offset: options?.offset || 0,
|
|
211
|
+
count: Math.min(options?.count || 50, 500)
|
|
212
|
+
};
|
|
213
|
+
// Build filter object
|
|
214
|
+
const filter = { ...options?.filter };
|
|
215
|
+
if (options?.bookId)
|
|
216
|
+
filter.book_id = options.bookId;
|
|
217
|
+
if (options?.chapterId)
|
|
218
|
+
filter.chapter_id = options.chapterId;
|
|
219
|
+
if (Object.keys(filter).length > 0) {
|
|
220
|
+
params.filter = JSON.stringify(filter);
|
|
221
|
+
}
|
|
222
|
+
if (options?.sort)
|
|
223
|
+
params.sort = options.sort;
|
|
224
|
+
const response = await this.client.get('/pages', { params });
|
|
225
|
+
const data = response.data;
|
|
226
|
+
return {
|
|
227
|
+
...data,
|
|
228
|
+
data: await Promise.all(data.data.map((page) => this.enhancePageResponse(page)))
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
async getPage(id) {
|
|
232
|
+
const response = await this.client.get(`/pages/${id}`);
|
|
233
|
+
return await this.enhancePageResponse(response.data);
|
|
234
|
+
}
|
|
235
|
+
async getChapters(bookId, offset = 0, count = 50) {
|
|
236
|
+
const params = { offset, count };
|
|
237
|
+
if (bookId)
|
|
238
|
+
params.filter = JSON.stringify({ book_id: bookId });
|
|
239
|
+
const response = await this.client.get('/chapters', { params });
|
|
240
|
+
const data = response.data;
|
|
241
|
+
return {
|
|
242
|
+
...data,
|
|
243
|
+
data: await Promise.all(data.data.map((chapter) => this.enhanceChapterResponse(chapter)))
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async getChapter(id) {
|
|
247
|
+
const response = await this.client.get(`/chapters/${id}`);
|
|
248
|
+
return await this.enhanceChapterResponse(response.data);
|
|
249
|
+
}
|
|
250
|
+
async createPage(data) {
|
|
251
|
+
if (!this.enableWrite) {
|
|
252
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
253
|
+
}
|
|
254
|
+
const response = await this.client.post('/pages', data);
|
|
255
|
+
return await this.enhancePageResponse(response.data);
|
|
256
|
+
}
|
|
257
|
+
async updatePage(id, data) {
|
|
258
|
+
if (!this.enableWrite) {
|
|
259
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
260
|
+
}
|
|
261
|
+
const response = await this.client.put(`/pages/${id}`, data);
|
|
262
|
+
return await this.enhancePageResponse(response.data);
|
|
263
|
+
}
|
|
264
|
+
async exportPage(id, format) {
|
|
265
|
+
try {
|
|
266
|
+
// For binary formats (PDF, ZIP), return BookStack web URL using slugs
|
|
267
|
+
if (format === 'pdf' || format === 'zip') {
|
|
268
|
+
// First fetch the page data to get slugs
|
|
269
|
+
const page = await this.getPage(id);
|
|
270
|
+
const book = await this.getBook(page.book_id);
|
|
271
|
+
// Construct the correct web URL with slugs
|
|
272
|
+
const directUrl = `${this.baseUrl}/books/${book.slug}/page/${page.slug}/export/${format}`;
|
|
273
|
+
const filename = `${page.slug}.${format}`;
|
|
274
|
+
const contentType = format === 'pdf' ? 'application/pdf' : 'application/zip';
|
|
275
|
+
return {
|
|
276
|
+
format: format,
|
|
277
|
+
filename: filename,
|
|
278
|
+
download_url: directUrl,
|
|
279
|
+
content_type: contentType,
|
|
280
|
+
export_success: true,
|
|
281
|
+
page_id: id,
|
|
282
|
+
page_name: page.name,
|
|
283
|
+
book_name: book.name,
|
|
284
|
+
direct_download: true,
|
|
285
|
+
note: "This is a direct link to BookStack's web export. You may need to be logged in to BookStack to access it."
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// For text formats, fetch the content via API
|
|
290
|
+
console.error(`Exporting page ${id} as ${format}...`);
|
|
291
|
+
const response = await this.client.get(`/pages/${id}/export/${format}`);
|
|
292
|
+
console.error(`Export response status: ${response.status}`);
|
|
293
|
+
// For text formats, validate and return as string
|
|
294
|
+
if (!response.data) {
|
|
295
|
+
throw new Error(`Empty ${format} content returned from BookStack API`);
|
|
296
|
+
}
|
|
297
|
+
console.error(`Text export length: ${response.data.length} characters`);
|
|
298
|
+
return response.data;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
console.error(`Export error for page ${id}:`, error);
|
|
303
|
+
throw new Error(`Failed to export page ${id} as ${format}: ${error instanceof Error ? error.message : String(error)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async exportBook(id, format) {
|
|
307
|
+
// For binary formats (PDF, ZIP), return BookStack web URL using slug
|
|
308
|
+
if (format === 'pdf' || format === 'zip') {
|
|
309
|
+
// First fetch the book data to get slug
|
|
310
|
+
const book = await this.getBook(id);
|
|
311
|
+
// Construct the correct web URL with slug
|
|
312
|
+
const directUrl = `${this.baseUrl}/books/${book.slug}/export/${format}`;
|
|
313
|
+
const filename = `${book.slug}.${format}`;
|
|
314
|
+
const contentType = format === 'pdf' ? 'application/pdf' : 'application/zip';
|
|
315
|
+
return {
|
|
316
|
+
format: format,
|
|
317
|
+
filename: filename,
|
|
318
|
+
download_url: directUrl,
|
|
319
|
+
content_type: contentType,
|
|
320
|
+
export_success: true,
|
|
321
|
+
book_id: id,
|
|
322
|
+
book_name: book.name,
|
|
323
|
+
direct_download: true,
|
|
324
|
+
note: "This is a direct link to BookStack's web export. You may need to be logged in to BookStack to access it."
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
// For text formats, fetch the content via API
|
|
328
|
+
const response = await this.client.get(`/books/${id}/export/${format}`);
|
|
329
|
+
return response.data;
|
|
330
|
+
}
|
|
331
|
+
async exportChapter(id, format) {
|
|
332
|
+
// For binary formats (PDF, ZIP), return BookStack web URL using slugs
|
|
333
|
+
if (format === 'pdf' || format === 'zip') {
|
|
334
|
+
// First fetch the chapter data to get slugs
|
|
335
|
+
const chapter = await this.getChapter(id);
|
|
336
|
+
const book = await this.getBook(chapter.book_id);
|
|
337
|
+
// Construct the correct web URL with both book and chapter slugs
|
|
338
|
+
const directUrl = `${this.baseUrl}/books/${book.slug}/chapter/${chapter.slug}/export/${format}`;
|
|
339
|
+
const filename = `${chapter.slug}.${format}`;
|
|
340
|
+
const contentType = format === 'pdf' ? 'application/pdf' : 'application/zip';
|
|
341
|
+
return {
|
|
342
|
+
format: format,
|
|
343
|
+
filename: filename,
|
|
344
|
+
download_url: directUrl,
|
|
345
|
+
content_type: contentType,
|
|
346
|
+
export_success: true,
|
|
347
|
+
chapter_id: id,
|
|
348
|
+
chapter_name: chapter.name,
|
|
349
|
+
book_name: book.name,
|
|
350
|
+
direct_download: true,
|
|
351
|
+
note: "This is a direct link to BookStack's web export. You may need to be logged in to BookStack to access it."
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
// For text formats, fetch the content via API
|
|
355
|
+
const response = await this.client.get(`/chapters/${id}/export/${format}`);
|
|
356
|
+
return response.data;
|
|
357
|
+
}
|
|
358
|
+
async getRecentChanges(options) {
|
|
359
|
+
const limit = Math.min(options?.limit || 20, 100);
|
|
360
|
+
const days = options?.days || 30;
|
|
361
|
+
const type = options?.type || 'all';
|
|
362
|
+
// Calculate date threshold
|
|
363
|
+
const dateThreshold = new Date();
|
|
364
|
+
dateThreshold.setDate(dateThreshold.getDate() - days);
|
|
365
|
+
const dateFilter = dateThreshold.toISOString().split('T')[0]; // YYYY-MM-DD format
|
|
366
|
+
// Build search query for recent changes
|
|
367
|
+
let searchQuery = `{updated_at:>=${dateFilter}}`;
|
|
368
|
+
if (type !== 'all') {
|
|
369
|
+
searchQuery = `{type:${type}} ${searchQuery}`;
|
|
370
|
+
}
|
|
371
|
+
const params = {
|
|
372
|
+
query: searchQuery,
|
|
373
|
+
count: limit,
|
|
374
|
+
sort: 'updated_at' // Sort by most recently updated
|
|
375
|
+
};
|
|
376
|
+
const response = await this.client.get('/search', { params });
|
|
377
|
+
const results = response.data.data || response.data;
|
|
378
|
+
// Enhance results with additional context
|
|
379
|
+
const enhancedResults = await Promise.all(results.map(async (result) => {
|
|
380
|
+
let contextualInfo = '';
|
|
381
|
+
let contentPreview = result.preview_content?.content || '';
|
|
382
|
+
try {
|
|
383
|
+
// Get additional context based on content type
|
|
384
|
+
if (result.type === 'page' && result.id) {
|
|
385
|
+
const fullPage = await this.client.get(`/pages/${result.id}`);
|
|
386
|
+
const pageData = fullPage.data;
|
|
387
|
+
contentPreview = pageData.text?.substring(0, 200) || contentPreview;
|
|
388
|
+
contextualInfo = `Updated in book: ${pageData.book?.name || 'Unknown Book'}`;
|
|
389
|
+
if (pageData.chapter) {
|
|
390
|
+
contextualInfo += `, chapter: ${pageData.chapter.name}`;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
else if (result.type === 'book' && result.id) {
|
|
394
|
+
const fullBook = await this.client.get(`/books/${result.id}`);
|
|
395
|
+
const bookData = fullBook.data;
|
|
396
|
+
contentPreview = bookData.description?.substring(0, 200) || 'No description available';
|
|
397
|
+
contextualInfo = `Book with ${bookData.page_count || 0} pages`;
|
|
398
|
+
}
|
|
399
|
+
else if (result.type === 'chapter' && result.id) {
|
|
400
|
+
const fullChapter = await this.client.get(`/chapters/${result.id}`);
|
|
401
|
+
const chapterData = fullChapter.data;
|
|
402
|
+
contentPreview = chapterData.description?.substring(0, 200) || 'No description available';
|
|
403
|
+
contextualInfo = `Chapter in book: ${chapterData.book?.name || 'Unknown Book'}`;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
// If we can't get additional context, use what we have
|
|
408
|
+
contextualInfo = `${result.type.charAt(0).toUpperCase() + result.type.slice(1)} content`;
|
|
409
|
+
}
|
|
410
|
+
const url = await this.generateContentUrl(result);
|
|
411
|
+
return {
|
|
412
|
+
...result,
|
|
413
|
+
url,
|
|
414
|
+
direct_link: `[${result.name}](${url})`,
|
|
415
|
+
content_preview: contentPreview ? `${contentPreview}${contentPreview.length >= 200 ? '...' : ''}` : 'No preview available',
|
|
416
|
+
contextual_info: contextualInfo,
|
|
417
|
+
last_updated: this.formatDate(result.updated_at || result.created_at || ''),
|
|
418
|
+
change_summary: `${result.type === 'page' ? 'Page' : result.type === 'book' ? 'Book' : 'Chapter'} "${result.name}" was updated`
|
|
419
|
+
};
|
|
420
|
+
}));
|
|
421
|
+
return {
|
|
422
|
+
search_query: `Recent changes in the last ${days} days (${type})`,
|
|
423
|
+
date_threshold: dateFilter,
|
|
424
|
+
search_url: this.generateSearchUrl(searchQuery),
|
|
425
|
+
total_found: results.length,
|
|
426
|
+
summary: `Found ${results.length} items updated in the last ${days} days${type !== 'all' ? ` (${type}s only)` : ''}`,
|
|
427
|
+
results: enhancedResults
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
formatDate(dateString) {
|
|
431
|
+
if (!dateString)
|
|
432
|
+
return 'Unknown date';
|
|
433
|
+
const date = new Date(dateString);
|
|
434
|
+
const now = new Date();
|
|
435
|
+
const diffInHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60));
|
|
436
|
+
if (diffInHours < 1)
|
|
437
|
+
return 'Less than an hour ago';
|
|
438
|
+
if (diffInHours < 24)
|
|
439
|
+
return `${diffInHours} hours ago`;
|
|
440
|
+
const diffInDays = Math.floor(diffInHours / 24);
|
|
441
|
+
if (diffInDays < 7)
|
|
442
|
+
return `${diffInDays} days ago`;
|
|
443
|
+
const diffInWeeks = Math.floor(diffInDays / 7);
|
|
444
|
+
if (diffInWeeks < 4)
|
|
445
|
+
return `${diffInWeeks} weeks ago`;
|
|
446
|
+
return date.toLocaleDateString();
|
|
447
|
+
}
|
|
448
|
+
// Shelves (Book Collections) Management
|
|
449
|
+
async getShelves(options) {
|
|
450
|
+
const params = {
|
|
451
|
+
offset: options?.offset || 0,
|
|
452
|
+
count: Math.min(options?.count || 50, 500)
|
|
453
|
+
};
|
|
454
|
+
if (options?.sort)
|
|
455
|
+
params.sort = options.sort;
|
|
456
|
+
if (options?.filter)
|
|
457
|
+
params.filter = JSON.stringify(options.filter);
|
|
458
|
+
const response = await this.client.get('/shelves', { params });
|
|
459
|
+
const data = response.data;
|
|
460
|
+
return {
|
|
461
|
+
...data,
|
|
462
|
+
data: data.data.map((shelf) => this.enhanceShelfResponse(shelf))
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
async getShelf(id) {
|
|
466
|
+
const response = await this.client.get(`/shelves/${id}`);
|
|
467
|
+
return this.enhanceShelfResponse(response.data);
|
|
468
|
+
}
|
|
469
|
+
async createShelf(data) {
|
|
470
|
+
if (!this.enableWrite) {
|
|
471
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
472
|
+
}
|
|
473
|
+
const response = await this.client.post('/shelves', data);
|
|
474
|
+
return this.enhanceShelfResponse(response.data);
|
|
475
|
+
}
|
|
476
|
+
async updateShelf(id, data) {
|
|
477
|
+
if (!this.enableWrite) {
|
|
478
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
479
|
+
}
|
|
480
|
+
const response = await this.client.put(`/shelves/${id}`, data);
|
|
481
|
+
return this.enhanceShelfResponse(response.data);
|
|
482
|
+
}
|
|
483
|
+
async deleteShelf(id) {
|
|
484
|
+
if (!this.enableWrite) {
|
|
485
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
486
|
+
}
|
|
487
|
+
const response = await this.client.delete(`/shelves/${id}`);
|
|
488
|
+
return response.data;
|
|
489
|
+
}
|
|
490
|
+
// Attachments Management
|
|
491
|
+
async getAttachments(options) {
|
|
492
|
+
const params = {
|
|
493
|
+
offset: options?.offset || 0,
|
|
494
|
+
count: Math.min(options?.count || 50, 500)
|
|
495
|
+
};
|
|
496
|
+
if (options?.sort)
|
|
497
|
+
params.sort = options.sort;
|
|
498
|
+
if (options?.filter)
|
|
499
|
+
params.filter = JSON.stringify(options.filter);
|
|
500
|
+
const response = await this.client.get('/attachments', { params });
|
|
501
|
+
const data = response.data;
|
|
502
|
+
return {
|
|
503
|
+
...data,
|
|
504
|
+
data: data.data.map((attachment) => ({
|
|
505
|
+
...attachment,
|
|
506
|
+
page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`,
|
|
507
|
+
direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})`
|
|
508
|
+
}))
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
async getAttachment(id) {
|
|
512
|
+
const response = await this.client.get(`/attachments/${id}`);
|
|
513
|
+
const attachment = response.data;
|
|
514
|
+
return {
|
|
515
|
+
...attachment,
|
|
516
|
+
page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`,
|
|
517
|
+
direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})`,
|
|
518
|
+
download_url: `${this.baseUrl}/attachments/${attachment.id}`
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
async createAttachment(data) {
|
|
522
|
+
if (!this.enableWrite) {
|
|
523
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
524
|
+
}
|
|
525
|
+
const response = await this.client.post('/attachments', data);
|
|
526
|
+
const attachment = response.data;
|
|
527
|
+
return {
|
|
528
|
+
...attachment,
|
|
529
|
+
page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`,
|
|
530
|
+
direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})`
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
async updateAttachment(id, data) {
|
|
534
|
+
if (!this.enableWrite) {
|
|
535
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
536
|
+
}
|
|
537
|
+
const response = await this.client.put(`/attachments/${id}`, data);
|
|
538
|
+
const attachment = response.data;
|
|
539
|
+
return {
|
|
540
|
+
...attachment,
|
|
541
|
+
page_url: `${this.baseUrl}/books/${Math.floor(attachment.uploaded_to / 1000)}/page/${attachment.uploaded_to}`,
|
|
542
|
+
direct_link: `[${attachment.name}](${this.baseUrl}/attachments/${attachment.id})`
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
async deleteAttachment(id) {
|
|
546
|
+
if (!this.enableWrite) {
|
|
547
|
+
throw new Error('Write operations are disabled. Set BOOKSTACK_ENABLE_WRITE=true to enable.');
|
|
548
|
+
}
|
|
549
|
+
const response = await this.client.delete(`/attachments/${id}`);
|
|
550
|
+
return response.data;
|
|
551
|
+
}
|
|
552
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { BookStackClient } from "./bookstack-client.js";
|
|
6
|
+
function getRequiredEnvVar(name) {
|
|
7
|
+
const value = process.env[name];
|
|
8
|
+
if (!value) {
|
|
9
|
+
console.error(`Error: ${name} environment variable is required`);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
async function main() {
|
|
15
|
+
// Load configuration from environment
|
|
16
|
+
const config = {
|
|
17
|
+
baseUrl: getRequiredEnvVar('BOOKSTACK_BASE_URL'),
|
|
18
|
+
tokenId: getRequiredEnvVar('BOOKSTACK_TOKEN_ID'),
|
|
19
|
+
tokenSecret: getRequiredEnvVar('BOOKSTACK_TOKEN_SECRET'),
|
|
20
|
+
enableWrite: process.env.BOOKSTACK_ENABLE_WRITE?.toLowerCase() === 'true'
|
|
21
|
+
};
|
|
22
|
+
console.error('Initializing BookStack MCP Server...');
|
|
23
|
+
console.error(`BookStack URL: ${config.baseUrl}`);
|
|
24
|
+
console.error(`Write operations: ${config.enableWrite ? 'ENABLED' : 'DISABLED'}`);
|
|
25
|
+
const client = new BookStackClient(config);
|
|
26
|
+
const server = new McpServer({
|
|
27
|
+
name: "bookstack-mcp",
|
|
28
|
+
version: "2.1.0"
|
|
29
|
+
});
|
|
30
|
+
// Register read-only tools
|
|
31
|
+
server.registerTool("get_capabilities", {
|
|
32
|
+
title: "Get BookStack Capabilities",
|
|
33
|
+
description: "Get information about available BookStack MCP capabilities and current configuration",
|
|
34
|
+
inputSchema: {}
|
|
35
|
+
}, async () => {
|
|
36
|
+
const capabilities = {
|
|
37
|
+
server_name: "BookStack MCP Server",
|
|
38
|
+
version: "2.1.0",
|
|
39
|
+
write_operations_enabled: config.enableWrite,
|
|
40
|
+
available_tools: config.enableWrite ? "All tools enabled" : "Read-only tools only",
|
|
41
|
+
security_note: config.enableWrite
|
|
42
|
+
? "⚠️ Write operations are ENABLED - AI can create and modify BookStack content"
|
|
43
|
+
: "🛡️ Read-only mode - Safe for production use"
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
content: [{ type: "text", text: JSON.stringify(capabilities, null, 2) }]
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
server.registerTool("search_content", {
|
|
50
|
+
title: "Search BookStack Content",
|
|
51
|
+
description: "Search across BookStack content with contextual previews and location info",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
query: z.string().describe("Search query. Use BookStack advanced search syntax like {type:page} or {book_id:5}"),
|
|
54
|
+
type: z.enum(["book", "page", "chapter", "bookshelf"]).optional().describe("Filter by content type"),
|
|
55
|
+
count: z.coerce.number().max(500).optional().describe("Number of results to return (max 500)"),
|
|
56
|
+
offset: z.coerce.number().optional().describe("Number of results to skip for pagination")
|
|
57
|
+
}
|
|
58
|
+
}, async (args) => {
|
|
59
|
+
const results = await client.searchContent(args.query, {
|
|
60
|
+
type: args.type,
|
|
61
|
+
count: args.count,
|
|
62
|
+
offset: args.offset
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
server.registerTool("search_pages", {
|
|
69
|
+
title: "Search Pages",
|
|
70
|
+
description: "Search specifically for pages with optional book filtering",
|
|
71
|
+
inputSchema: {
|
|
72
|
+
query: z.string().describe("Search query for pages"),
|
|
73
|
+
book_id: z.coerce.number().optional().describe("Filter results to pages within a specific book"),
|
|
74
|
+
count: z.coerce.number().max(500).optional().describe("Number of results to return"),
|
|
75
|
+
offset: z.coerce.number().optional().describe("Pagination offset")
|
|
76
|
+
}
|
|
77
|
+
}, async (args) => {
|
|
78
|
+
const results = await client.searchPages(args.query, {
|
|
79
|
+
bookId: args.book_id,
|
|
80
|
+
count: args.count,
|
|
81
|
+
offset: args.offset
|
|
82
|
+
});
|
|
83
|
+
return {
|
|
84
|
+
content: [{ type: "text", text: JSON.stringify(results, null, 2) }]
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
server.registerTool("get_books", {
|
|
88
|
+
title: "List Books",
|
|
89
|
+
description: "List available books with advanced filtering and sorting",
|
|
90
|
+
inputSchema: {
|
|
91
|
+
offset: z.coerce.number().default(0).describe("Pagination offset"),
|
|
92
|
+
count: z.coerce.number().max(500).default(50).describe("Number of results to return"),
|
|
93
|
+
sort: z.string().optional().describe("Sort field (e.g., 'name', '-created_at', 'updated_at')"),
|
|
94
|
+
filter: z.record(z.any()).optional().describe("Filter criteria")
|
|
95
|
+
}
|
|
96
|
+
}, async (args) => {
|
|
97
|
+
const books = await client.getBooks({
|
|
98
|
+
offset: args.offset,
|
|
99
|
+
count: args.count,
|
|
100
|
+
sort: args.sort,
|
|
101
|
+
filter: args.filter
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
content: [{ type: "text", text: JSON.stringify(books, null, 2) }]
|
|
105
|
+
};
|
|
106
|
+
});
|
|
107
|
+
server.registerTool("get_book", {
|
|
108
|
+
title: "Get Book Details",
|
|
109
|
+
description: "Get detailed information about a specific book",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
id: z.coerce.number().min(1).describe("Book ID")
|
|
112
|
+
}
|
|
113
|
+
}, async (args) => {
|
|
114
|
+
const book = await client.getBook(args.id);
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: "text", text: JSON.stringify(book, null, 2) }]
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
server.registerTool("get_pages", {
|
|
120
|
+
title: "List Pages",
|
|
121
|
+
description: "List pages with content previews, word counts, and contextual information",
|
|
122
|
+
inputSchema: {
|
|
123
|
+
book_id: z.coerce.number().optional().describe("Filter by book ID"),
|
|
124
|
+
chapter_id: z.coerce.number().optional().describe("Filter by chapter ID"),
|
|
125
|
+
offset: z.coerce.number().default(0).describe("Pagination offset"),
|
|
126
|
+
count: z.coerce.number().max(500).default(50).describe("Number of results to return"),
|
|
127
|
+
sort: z.string().optional().describe("Sort field"),
|
|
128
|
+
filter: z.record(z.any()).optional().describe("Additional filter criteria")
|
|
129
|
+
}
|
|
130
|
+
}, async (args) => {
|
|
131
|
+
const pages = await client.getPages({
|
|
132
|
+
bookId: args.book_id,
|
|
133
|
+
chapterId: args.chapter_id,
|
|
134
|
+
offset: args.offset,
|
|
135
|
+
count: args.count,
|
|
136
|
+
sort: args.sort,
|
|
137
|
+
filter: args.filter
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
content: [{ type: "text", text: JSON.stringify(pages, null, 2) }]
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
server.registerTool("get_page", {
|
|
144
|
+
title: "Get Page Content",
|
|
145
|
+
description: "Get full content of a specific page",
|
|
146
|
+
inputSchema: {
|
|
147
|
+
id: z.coerce.number().min(1).describe("Page ID")
|
|
148
|
+
}
|
|
149
|
+
}, async (args) => {
|
|
150
|
+
const page = await client.getPage(args.id);
|
|
151
|
+
return {
|
|
152
|
+
content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
server.registerTool("get_chapters", {
|
|
156
|
+
title: "List Chapters",
|
|
157
|
+
description: "List chapters, optionally filtered by book",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
book_id: z.coerce.number().optional().describe("Filter by book ID"),
|
|
160
|
+
offset: z.coerce.number().default(0).describe("Pagination offset"),
|
|
161
|
+
count: z.coerce.number().default(50).describe("Number of results to return")
|
|
162
|
+
}
|
|
163
|
+
}, async (args) => {
|
|
164
|
+
const chapters = await client.getChapters(args.book_id, args.offset, args.count);
|
|
165
|
+
return {
|
|
166
|
+
content: [{ type: "text", text: JSON.stringify(chapters, null, 2) }]
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
server.registerTool("get_chapter", {
|
|
170
|
+
title: "Get Chapter Details",
|
|
171
|
+
description: "Get details of a specific chapter",
|
|
172
|
+
inputSchema: {
|
|
173
|
+
id: z.coerce.number().min(1).describe("Chapter ID")
|
|
174
|
+
}
|
|
175
|
+
}, async (args) => {
|
|
176
|
+
const chapter = await client.getChapter(args.id);
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: JSON.stringify(chapter, null, 2) }]
|
|
179
|
+
};
|
|
180
|
+
});
|
|
181
|
+
server.registerTool("export_page", {
|
|
182
|
+
title: "Export Page",
|
|
183
|
+
description: "Export a page in various formats (PDF/ZIP provide direct BookStack download URLs)",
|
|
184
|
+
inputSchema: {
|
|
185
|
+
id: z.coerce.number().min(1).describe("Page ID"),
|
|
186
|
+
format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]).describe("Export format")
|
|
187
|
+
}
|
|
188
|
+
}, async (args) => {
|
|
189
|
+
const content = await client.exportPage(args.id, args.format);
|
|
190
|
+
// Handle binary formats with direct URLs
|
|
191
|
+
if (typeof content === 'object' && content.download_url && content.direct_download) {
|
|
192
|
+
const format = args.format.toUpperCase();
|
|
193
|
+
return {
|
|
194
|
+
content: [{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: `✅ **${format} Export Ready**\n\n` +
|
|
197
|
+
`📄 **Page:** ${content.page_name}\n` +
|
|
198
|
+
`📚 **Book:** ${content.book_name}\n` +
|
|
199
|
+
`📁 **File:** ${content.filename}\n\n` +
|
|
200
|
+
`🚀 **Direct Download Link:**\n${content.download_url}\n\n` +
|
|
201
|
+
`ℹ️ **Note:** ${content.note}`
|
|
202
|
+
}]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// Handle text formats
|
|
206
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
207
|
+
return {
|
|
208
|
+
content: [{ type: "text", text }]
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
server.registerTool("export_book", {
|
|
212
|
+
title: "Export Book",
|
|
213
|
+
description: "Export an entire book in various formats",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
id: z.coerce.number().min(1).describe("Book ID"),
|
|
216
|
+
format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]).describe("Export format")
|
|
217
|
+
}
|
|
218
|
+
}, async (args) => {
|
|
219
|
+
const content = await client.exportBook(args.id, args.format);
|
|
220
|
+
if (typeof content === 'object' && content.download_url) {
|
|
221
|
+
const format = args.format.toUpperCase();
|
|
222
|
+
return {
|
|
223
|
+
content: [{
|
|
224
|
+
type: "text",
|
|
225
|
+
text: `✅ **${format} Book Export Ready**\n\n` +
|
|
226
|
+
`📚 **Book:** ${content.book_name}\n` +
|
|
227
|
+
`📁 **File:** ${content.filename}\n\n` +
|
|
228
|
+
`🚀 **Direct Download Link:**\n${content.download_url}\n\n` +
|
|
229
|
+
`ℹ️ **Note:** ${content.note}`
|
|
230
|
+
}]
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
234
|
+
return {
|
|
235
|
+
content: [{ type: "text", text }]
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
server.registerTool("export_chapter", {
|
|
239
|
+
title: "Export Chapter",
|
|
240
|
+
description: "Export a chapter in various formats",
|
|
241
|
+
inputSchema: {
|
|
242
|
+
id: z.coerce.number().min(1).describe("Chapter ID"),
|
|
243
|
+
format: z.enum(["html", "pdf", "markdown", "plaintext", "zip"]).describe("Export format")
|
|
244
|
+
}
|
|
245
|
+
}, async (args) => {
|
|
246
|
+
const content = await client.exportChapter(args.id, args.format);
|
|
247
|
+
if (typeof content === 'object' && content.download_url) {
|
|
248
|
+
const format = args.format.toUpperCase();
|
|
249
|
+
return {
|
|
250
|
+
content: [{
|
|
251
|
+
type: "text",
|
|
252
|
+
text: `✅ **${format} Chapter Export Ready**\n\n` +
|
|
253
|
+
`📖 **Chapter:** ${content.chapter_name}\n` +
|
|
254
|
+
`📚 **Book:** ${content.book_name}\n` +
|
|
255
|
+
`📁 **File:** ${content.filename}\n\n` +
|
|
256
|
+
`🚀 **Direct Download Link:**\n${content.download_url}\n\n` +
|
|
257
|
+
`ℹ️ **Note:** ${content.note}`
|
|
258
|
+
}]
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
const text = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
|
|
262
|
+
return {
|
|
263
|
+
content: [{ type: "text", text }]
|
|
264
|
+
};
|
|
265
|
+
});
|
|
266
|
+
server.registerTool("get_recent_changes", {
|
|
267
|
+
title: "Get Recent Changes",
|
|
268
|
+
description: "Get recently updated content with contextual previews and change descriptions",
|
|
269
|
+
inputSchema: {
|
|
270
|
+
type: z.enum(["all", "page", "book", "chapter"]).default("all").describe("Filter by content type"),
|
|
271
|
+
limit: z.coerce.number().max(100).default(20).describe("Number of recent items to return"),
|
|
272
|
+
days: z.coerce.number().default(30).describe("Number of days back to look for changes")
|
|
273
|
+
}
|
|
274
|
+
}, async (args) => {
|
|
275
|
+
const changes = await client.getRecentChanges({
|
|
276
|
+
type: args.type,
|
|
277
|
+
limit: args.limit,
|
|
278
|
+
days: args.days
|
|
279
|
+
});
|
|
280
|
+
return {
|
|
281
|
+
content: [{ type: "text", text: JSON.stringify(changes, null, 2) }]
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
server.registerTool("get_shelves", {
|
|
285
|
+
title: "List Shelves",
|
|
286
|
+
description: "List available book shelves (collections) with filtering and sorting",
|
|
287
|
+
inputSchema: {
|
|
288
|
+
offset: z.coerce.number().default(0).describe("Pagination offset"),
|
|
289
|
+
count: z.coerce.number().max(500).default(50).describe("Number of results to return"),
|
|
290
|
+
sort: z.string().optional().describe("Sort field"),
|
|
291
|
+
filter: z.record(z.any()).optional().describe("Filter criteria")
|
|
292
|
+
}
|
|
293
|
+
}, async (args) => {
|
|
294
|
+
const shelves = await client.getShelves({
|
|
295
|
+
offset: args.offset,
|
|
296
|
+
count: args.count,
|
|
297
|
+
sort: args.sort,
|
|
298
|
+
filter: args.filter
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: "text", text: JSON.stringify(shelves, null, 2) }]
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
server.registerTool("get_shelf", {
|
|
305
|
+
title: "Get Shelf Details",
|
|
306
|
+
description: "Get details of a specific book shelf including all books",
|
|
307
|
+
inputSchema: {
|
|
308
|
+
id: z.coerce.number().min(1).describe("Shelf ID")
|
|
309
|
+
}
|
|
310
|
+
}, async (args) => {
|
|
311
|
+
const shelf = await client.getShelf(args.id);
|
|
312
|
+
return {
|
|
313
|
+
content: [{ type: "text", text: JSON.stringify(shelf, null, 2) }]
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
server.registerTool("get_attachments", {
|
|
317
|
+
title: "List Attachments",
|
|
318
|
+
description: "List attachments (files and links) with filtering and sorting",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
offset: z.coerce.number().default(0).describe("Pagination offset"),
|
|
321
|
+
count: z.coerce.number().max(500).default(50).describe("Number of results to return"),
|
|
322
|
+
sort: z.string().optional().describe("Sort field"),
|
|
323
|
+
filter: z.record(z.any()).optional().describe("Filter criteria")
|
|
324
|
+
}
|
|
325
|
+
}, async (args) => {
|
|
326
|
+
const attachments = await client.getAttachments({
|
|
327
|
+
offset: args.offset,
|
|
328
|
+
count: args.count,
|
|
329
|
+
sort: args.sort,
|
|
330
|
+
filter: args.filter
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text", text: JSON.stringify(attachments, null, 2) }]
|
|
334
|
+
};
|
|
335
|
+
});
|
|
336
|
+
server.registerTool("get_attachment", {
|
|
337
|
+
title: "Get Attachment Details",
|
|
338
|
+
description: "Get details of a specific attachment including download links",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
id: z.coerce.number().min(1).describe("Attachment ID")
|
|
341
|
+
}
|
|
342
|
+
}, async (args) => {
|
|
343
|
+
const attachment = await client.getAttachment(args.id);
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: "text", text: JSON.stringify(attachment, null, 2) }]
|
|
346
|
+
};
|
|
347
|
+
});
|
|
348
|
+
// Register write tools if enabled
|
|
349
|
+
if (config.enableWrite) {
|
|
350
|
+
server.registerTool("create_page", {
|
|
351
|
+
title: "Create Page",
|
|
352
|
+
description: "Create a new page in BookStack",
|
|
353
|
+
inputSchema: {
|
|
354
|
+
name: z.string().describe("Page name"),
|
|
355
|
+
book_id: z.coerce.number().min(1).describe("Book ID where the page will be created"),
|
|
356
|
+
chapter_id: z.coerce.number().optional().describe("Optional: Chapter ID if page should be in a chapter"),
|
|
357
|
+
html: z.string().optional().describe("Optional: HTML content"),
|
|
358
|
+
markdown: z.string().optional().describe("Optional: Markdown content")
|
|
359
|
+
}
|
|
360
|
+
}, async (args) => {
|
|
361
|
+
const page = await client.createPage({
|
|
362
|
+
name: args.name,
|
|
363
|
+
book_id: args.book_id,
|
|
364
|
+
chapter_id: args.chapter_id,
|
|
365
|
+
html: args.html,
|
|
366
|
+
markdown: args.markdown
|
|
367
|
+
});
|
|
368
|
+
return {
|
|
369
|
+
content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
|
|
370
|
+
};
|
|
371
|
+
});
|
|
372
|
+
server.registerTool("update_page", {
|
|
373
|
+
title: "Update Page",
|
|
374
|
+
description: "Update an existing page",
|
|
375
|
+
inputSchema: {
|
|
376
|
+
id: z.coerce.number().min(1).describe("Page ID"),
|
|
377
|
+
name: z.string().optional().describe("Optional: New page name"),
|
|
378
|
+
html: z.string().optional().describe("Optional: New HTML content"),
|
|
379
|
+
markdown: z.string().optional().describe("Optional: New Markdown content")
|
|
380
|
+
}
|
|
381
|
+
}, async (args) => {
|
|
382
|
+
const page = await client.updatePage(args.id, {
|
|
383
|
+
name: args.name,
|
|
384
|
+
html: args.html,
|
|
385
|
+
markdown: args.markdown
|
|
386
|
+
});
|
|
387
|
+
return {
|
|
388
|
+
content: [{ type: "text", text: JSON.stringify(page, null, 2) }]
|
|
389
|
+
};
|
|
390
|
+
});
|
|
391
|
+
server.registerTool("create_shelf", {
|
|
392
|
+
title: "Create Shelf",
|
|
393
|
+
description: "Create a new book shelf (collection)",
|
|
394
|
+
inputSchema: {
|
|
395
|
+
name: z.string().describe("Shelf name"),
|
|
396
|
+
description: z.string().optional().describe("Shelf description"),
|
|
397
|
+
books: z.array(z.coerce.number()).optional().describe("Array of book IDs to add to the shelf"),
|
|
398
|
+
tags: z.array(z.object({
|
|
399
|
+
name: z.string(),
|
|
400
|
+
value: z.string()
|
|
401
|
+
}).strict()).optional().describe("Tags for the shelf")
|
|
402
|
+
}
|
|
403
|
+
}, async (args) => {
|
|
404
|
+
const shelf = await client.createShelf({
|
|
405
|
+
name: args.name,
|
|
406
|
+
description: args.description,
|
|
407
|
+
books: args.books,
|
|
408
|
+
tags: args.tags
|
|
409
|
+
});
|
|
410
|
+
return {
|
|
411
|
+
content: [{ type: "text", text: JSON.stringify(shelf, null, 2) }]
|
|
412
|
+
};
|
|
413
|
+
});
|
|
414
|
+
server.registerTool("update_shelf", {
|
|
415
|
+
title: "Update Shelf",
|
|
416
|
+
description: "Update an existing book shelf",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
id: z.coerce.number().min(1).describe("Shelf ID"),
|
|
419
|
+
name: z.string().optional().describe("New shelf name"),
|
|
420
|
+
description: z.string().optional().describe("New shelf description"),
|
|
421
|
+
books: z.array(z.coerce.number()).optional().describe("Array of book IDs"),
|
|
422
|
+
tags: z.array(z.object({
|
|
423
|
+
name: z.string(),
|
|
424
|
+
value: z.string()
|
|
425
|
+
}).strict()).optional().describe("Tags for the shelf")
|
|
426
|
+
}
|
|
427
|
+
}, async (args) => {
|
|
428
|
+
const shelf = await client.updateShelf(args.id, {
|
|
429
|
+
name: args.name,
|
|
430
|
+
description: args.description,
|
|
431
|
+
books: args.books,
|
|
432
|
+
tags: args.tags
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: JSON.stringify(shelf, null, 2) }]
|
|
436
|
+
};
|
|
437
|
+
});
|
|
438
|
+
server.registerTool("delete_shelf", {
|
|
439
|
+
title: "Delete Shelf",
|
|
440
|
+
description: "Delete a book shelf (collection)",
|
|
441
|
+
inputSchema: {
|
|
442
|
+
id: z.coerce.number().min(1).describe("Shelf ID")
|
|
443
|
+
}
|
|
444
|
+
}, async (args) => {
|
|
445
|
+
const result = await client.deleteShelf(args.id);
|
|
446
|
+
return {
|
|
447
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
server.registerTool("create_attachment", {
|
|
451
|
+
title: "Create Attachment",
|
|
452
|
+
description: "Create a new link attachment to a page",
|
|
453
|
+
inputSchema: {
|
|
454
|
+
name: z.string().describe("Attachment name"),
|
|
455
|
+
uploaded_to: z.coerce.number().min(1).describe("Page ID where attachment will be attached"),
|
|
456
|
+
link: z.string().describe("URL for link attachment")
|
|
457
|
+
}
|
|
458
|
+
}, async (args) => {
|
|
459
|
+
const attachment = await client.createAttachment({
|
|
460
|
+
name: args.name,
|
|
461
|
+
uploaded_to: args.uploaded_to,
|
|
462
|
+
link: args.link
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
content: [{ type: "text", text: JSON.stringify(attachment, null, 2) }]
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
server.registerTool("update_attachment", {
|
|
469
|
+
title: "Update Attachment",
|
|
470
|
+
description: "Update an existing attachment",
|
|
471
|
+
inputSchema: {
|
|
472
|
+
id: z.coerce.number().min(1).describe("Attachment ID"),
|
|
473
|
+
name: z.string().optional().describe("New attachment name"),
|
|
474
|
+
link: z.string().optional().describe("New URL for link attachment"),
|
|
475
|
+
uploaded_to: z.coerce.number().optional().describe("Move attachment to different page")
|
|
476
|
+
}
|
|
477
|
+
}, async (args) => {
|
|
478
|
+
const attachment = await client.updateAttachment(args.id, {
|
|
479
|
+
name: args.name,
|
|
480
|
+
link: args.link,
|
|
481
|
+
uploaded_to: args.uploaded_to
|
|
482
|
+
});
|
|
483
|
+
return {
|
|
484
|
+
content: [{ type: "text", text: JSON.stringify(attachment, null, 2) }]
|
|
485
|
+
};
|
|
486
|
+
});
|
|
487
|
+
server.registerTool("delete_attachment", {
|
|
488
|
+
title: "Delete Attachment",
|
|
489
|
+
description: "Delete an attachment",
|
|
490
|
+
inputSchema: {
|
|
491
|
+
id: z.coerce.number().min(1).describe("Attachment ID")
|
|
492
|
+
}
|
|
493
|
+
}, async (args) => {
|
|
494
|
+
const result = await client.deleteAttachment(args.id);
|
|
495
|
+
return {
|
|
496
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
497
|
+
};
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
// Connect via stdio transport
|
|
501
|
+
const transport = new StdioServerTransport();
|
|
502
|
+
await server.connect(transport);
|
|
503
|
+
console.error("BookStack MCP server running on stdio");
|
|
504
|
+
}
|
|
505
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
506
|
+
main().catch(console.error);
|
|
507
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bookstack-mcp",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "MCP server for BookStack wiki — search, read, create, and manage documentation via AI assistants",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"bookstack-mcp": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc && shx chmod +x dist/*.js",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"type-check": "tsc --noEmit",
|
|
17
|
+
"prepare": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"bookstack",
|
|
22
|
+
"model-context-protocol",
|
|
23
|
+
"ai",
|
|
24
|
+
"wiki",
|
|
25
|
+
"documentation",
|
|
26
|
+
"claude",
|
|
27
|
+
"librechat"
|
|
28
|
+
],
|
|
29
|
+
"author": "ttpears",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/ttpears/bookstack-mcp.git"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/ttpears/bookstack-mcp#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/ttpears/bookstack-mcp/issues"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
44
|
+
"axios": "^1.6.0",
|
|
45
|
+
"zod": "^3.25.76"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "^20.10.0",
|
|
49
|
+
"shx": "^0.4.0",
|
|
50
|
+
"tsx": "^4.6.0",
|
|
51
|
+
"typescript": "^5.3.0"
|
|
52
|
+
}
|
|
53
|
+
}
|