docmost-mcp 1.0.1
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 +78 -0
- package/build/index.js +409 -0
- package/build/lib/auth-utils.js +43 -0
- package/build/lib/collaboration.js +111 -0
- package/build/lib/filters.js +70 -0
- package/build/lib/markdown-converter.js +166 -0
- package/build/lib/tiptap-extensions.js +18 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Moritz Krause
|
|
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,78 @@
|
|
|
1
|
+
# Docmost MCP Server
|
|
2
|
+
|
|
3
|
+
A Model Context Protocol (MCP) server for [Docmost](https://docmost.com/), enabling AI agents to search, create, modify, and organize documentation pages and spaces.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### Core Management
|
|
8
|
+
|
|
9
|
+
- **`create_page`**: Smart creation tool. Creates content (via import) AND handles hierarchy (nesting under a parent) in one go.
|
|
10
|
+
- **`update_page`**: Update a page's content and/or title. Updates are performed via real-time collaboration (WebSocket).
|
|
11
|
+
- **`delete_page` / `delete_pages`**: Delete single or multiple pages at once.
|
|
12
|
+
- **`move_page`**: Organize pages hierarchically by moving them to a new parent or root.
|
|
13
|
+
|
|
14
|
+
### Exploration & Retrieval
|
|
15
|
+
|
|
16
|
+
- **`search`**: Full-text search across spaces with optional space filtering (`query`, `spaceId`).
|
|
17
|
+
- **`get_workspace`**: Get information about the current Docmost workspace.
|
|
18
|
+
- **`list_spaces`**: View all spaces within the current workspace.
|
|
19
|
+
- **`list_groups`**: View all groups within the current workspace.
|
|
20
|
+
- **`list_pages`**: List pages within a space (ordered by `updatedAt` descending).
|
|
21
|
+
- **`get_page`**: Retrieve full content and metadata of a specific page.
|
|
22
|
+
|
|
23
|
+
### Technical Details
|
|
24
|
+
|
|
25
|
+
- **Automatic Markdown Conversion**: Page content is automatically converted from Docmost's internal ProseMirror/TipTap JSON format to clean Markdown for easy agent consumption. Supports all Docmost extensions including callouts, task lists, math blocks, embeds, and more.
|
|
26
|
+
- **Smart Import API**: Uses Docmost's import API to ensure clean Markdown-to-ProseMirror conversion when creating pages.
|
|
27
|
+
- **Child Preservation**: The `update_page` tool creates a new page ID but effectively simulates an in-place update by reparenting existing child pages to the new version.
|
|
28
|
+
- **Pagination Support**: Automatically handles pagination for large datasets (spaces, pages, groups).
|
|
29
|
+
- **Filtered Responses**: API responses are filtered to include only relevant information, optimizing data transfer for agents.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install
|
|
35
|
+
npm run build
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
This server requires the following environment variables to be set:
|
|
41
|
+
|
|
42
|
+
- `DOCMOST_API_URL`: The full URL to your Docmost API (e.g., `https://docs.example.com/api`).
|
|
43
|
+
- `DOCMOST_EMAIL`: The email address for authentication.
|
|
44
|
+
- `DOCMOST_PASSWORD`: The password for authentication.
|
|
45
|
+
|
|
46
|
+
## usage with Claude Desktop / generic MCP Client
|
|
47
|
+
|
|
48
|
+
Add the following to your MCP configuration (e.g. `claude_desktop_config.json`):
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"mcpServers": {
|
|
53
|
+
"docmost-local": {
|
|
54
|
+
"command": "node",
|
|
55
|
+
"args": ["./build/index.js"],
|
|
56
|
+
"env": {
|
|
57
|
+
"DOCMOST_API_URL": "http://localhost:3000/api",
|
|
58
|
+
"DOCMOST_EMAIL": "test@docmost.com",
|
|
59
|
+
"DOCMOST_PASSWORD": "test"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Development
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Watch mode
|
|
70
|
+
npm run watch
|
|
71
|
+
|
|
72
|
+
# Build
|
|
73
|
+
npm run build
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
MIT
|
package/build/index.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
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 FormData from "form-data";
|
|
5
|
+
import axios from "axios";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { filterWorkspace, filterSpace, filterGroup, filterPage, filterSearchResult, } from "./lib/filters.js";
|
|
8
|
+
import { convertProseMirrorToMarkdown } from "./lib/markdown-converter.js";
|
|
9
|
+
import { readFileSync } from "fs";
|
|
10
|
+
import { fileURLToPath } from "url";
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { updatePageContentRealtime } from "./lib/collaboration.js";
|
|
13
|
+
import { getCollabToken, performLogin } from "./lib/auth-utils.js";
|
|
14
|
+
// Read version from package.json
|
|
15
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
16
|
+
const __dirname = dirname(__filename);
|
|
17
|
+
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
|
|
18
|
+
const VERSION = packageJson.version;
|
|
19
|
+
const API_URL = process.env.DOCMOST_API_URL;
|
|
20
|
+
const EMAIL = process.env.DOCMOST_EMAIL;
|
|
21
|
+
const PASSWORD = process.env.DOCMOST_PASSWORD;
|
|
22
|
+
if (!API_URL || !EMAIL || !PASSWORD) {
|
|
23
|
+
console.error("Error: DOCMOST_API_URL, DOCMOST_EMAIL, and DOCMOST_PASSWORD environment variables are required.");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
class DocmostClient {
|
|
27
|
+
// ... [Client Implementation stays exactly the same] ...
|
|
28
|
+
client;
|
|
29
|
+
token = null;
|
|
30
|
+
constructor(baseURL) {
|
|
31
|
+
this.client = axios.create({
|
|
32
|
+
baseURL,
|
|
33
|
+
headers: {
|
|
34
|
+
"Content-Type": "application/json",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async login() {
|
|
39
|
+
if (!EMAIL || !PASSWORD) {
|
|
40
|
+
throw new Error("Missing Credentials (DOCMOST_EMAIL, DOCMOST_PASSWORD)");
|
|
41
|
+
}
|
|
42
|
+
// baseURL is already set in this.client
|
|
43
|
+
const baseURL = this.client.defaults.baseURL || "";
|
|
44
|
+
// Use shared auth utility
|
|
45
|
+
this.token = await performLogin(baseURL, EMAIL, PASSWORD);
|
|
46
|
+
this.client.defaults.headers.common["Authorization"] =
|
|
47
|
+
`Bearer ${this.token}`;
|
|
48
|
+
}
|
|
49
|
+
async ensureAuthenticated() {
|
|
50
|
+
if (!this.token) {
|
|
51
|
+
await this.login();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Generic pagination handler for Docmost API endpoints
|
|
56
|
+
* @param endpoint - The API endpoint path (e.g., "/spaces", "/pages/recent")
|
|
57
|
+
* @param basePayload - Base payload object to send with each request
|
|
58
|
+
* @param limit - Items per page (min: 1, max: 100, default: 100)
|
|
59
|
+
* @returns All items collected from all pages
|
|
60
|
+
*/
|
|
61
|
+
async paginateAll(endpoint, basePayload = {}, limit = 100) {
|
|
62
|
+
await this.ensureAuthenticated();
|
|
63
|
+
// Clamp limit between 1 and 100
|
|
64
|
+
const clampedLimit = Math.max(1, Math.min(100, limit));
|
|
65
|
+
let page = 1;
|
|
66
|
+
let allItems = [];
|
|
67
|
+
let hasNextPage = true;
|
|
68
|
+
while (hasNextPage) {
|
|
69
|
+
const response = await this.client.post(endpoint, {
|
|
70
|
+
...basePayload,
|
|
71
|
+
limit: clampedLimit,
|
|
72
|
+
page,
|
|
73
|
+
});
|
|
74
|
+
const data = response.data;
|
|
75
|
+
// Handle both direct data.items and data.data.items structures
|
|
76
|
+
const items = data.data?.items || data.items || [];
|
|
77
|
+
const meta = data.data?.meta || data.meta;
|
|
78
|
+
allItems = allItems.concat(items);
|
|
79
|
+
hasNextPage = meta?.hasNextPage || false;
|
|
80
|
+
page++;
|
|
81
|
+
}
|
|
82
|
+
return allItems;
|
|
83
|
+
}
|
|
84
|
+
async getWorkspace() {
|
|
85
|
+
await this.ensureAuthenticated();
|
|
86
|
+
const response = await this.client.post("/workspace/info", {});
|
|
87
|
+
return {
|
|
88
|
+
data: filterWorkspace(response.data.data),
|
|
89
|
+
success: response.data.success,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async getSpaces() {
|
|
93
|
+
const spaces = await this.paginateAll("/spaces", {});
|
|
94
|
+
return spaces.map((space) => filterSpace(space));
|
|
95
|
+
}
|
|
96
|
+
async getGroups() {
|
|
97
|
+
const groups = await this.paginateAll("/groups", {});
|
|
98
|
+
return groups.map((group) => filterGroup(group));
|
|
99
|
+
}
|
|
100
|
+
async listPages(spaceId) {
|
|
101
|
+
const payload = spaceId ? { spaceId } : {};
|
|
102
|
+
const pages = await this.paginateAll("/pages/recent", payload);
|
|
103
|
+
return pages.map((page) => filterPage(page));
|
|
104
|
+
}
|
|
105
|
+
async listSidebarPages(spaceId, pageId) {
|
|
106
|
+
await this.ensureAuthenticated();
|
|
107
|
+
const response = await this.client.post("/pages/sidebar-pages", {
|
|
108
|
+
spaceId,
|
|
109
|
+
pageId,
|
|
110
|
+
page: 1,
|
|
111
|
+
});
|
|
112
|
+
return response.data?.data?.items || [];
|
|
113
|
+
}
|
|
114
|
+
async getPage(pageId) {
|
|
115
|
+
await this.ensureAuthenticated();
|
|
116
|
+
const response = await this.client.post("/pages/info", { pageId });
|
|
117
|
+
const resultData = response.data.data; // Assuming data is nested under 'data'
|
|
118
|
+
let content = resultData.content
|
|
119
|
+
? convertProseMirrorToMarkdown(resultData.content)
|
|
120
|
+
: ""; // Default to empty string
|
|
121
|
+
// Always fetch subpages to provide context to the agent
|
|
122
|
+
let subpages = [];
|
|
123
|
+
try {
|
|
124
|
+
subpages = await this.listSidebarPages(resultData.spaceId, pageId);
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
console.warn("Failed to fetch subpages:", e);
|
|
128
|
+
}
|
|
129
|
+
// Resolve subpages if the placeholder exists
|
|
130
|
+
if (content && content.includes("{{SUBPAGES}}")) {
|
|
131
|
+
if (subpages && subpages.length > 0) {
|
|
132
|
+
const list = subpages
|
|
133
|
+
.map((p) => `- [${p.title}](page:${p.id})`)
|
|
134
|
+
.join("\n");
|
|
135
|
+
content = content.replace("{{SUBPAGES}}", `### Subpages\n${list}`);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
content = content.replace("{{SUBPAGES}}", "");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
data: filterPage(resultData, content, subpages),
|
|
143
|
+
success: response.data.success,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Create a new page with title and content.
|
|
148
|
+
*
|
|
149
|
+
* Note: As long as Docmost doesn't provide a /pages/create endpoint that allows
|
|
150
|
+
* setting content directly, we must use the /pages/import workaround to create
|
|
151
|
+
* pages with initial content. This method:
|
|
152
|
+
* 1. Creates the page via /pages/import (which supports content)
|
|
153
|
+
* 2. Moves it to the correct parent if specified
|
|
154
|
+
*/
|
|
155
|
+
async createPage(title, content, spaceId, parentPageId) {
|
|
156
|
+
await this.ensureAuthenticated();
|
|
157
|
+
if (parentPageId) {
|
|
158
|
+
try {
|
|
159
|
+
await this.getPage(parentPageId);
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
throw new Error(`Parent page with ID ${parentPageId} not found.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// 1. Create content via Import (using multipart/form-data)
|
|
166
|
+
const form = new FormData();
|
|
167
|
+
form.append("spaceId", spaceId);
|
|
168
|
+
const fileContent = Buffer.from(content, "utf-8");
|
|
169
|
+
form.append("file", fileContent, {
|
|
170
|
+
filename: `${title || "import"}.md`,
|
|
171
|
+
contentType: "text/markdown",
|
|
172
|
+
});
|
|
173
|
+
const headers = {
|
|
174
|
+
...form.getHeaders(),
|
|
175
|
+
Authorization: `Bearer ${this.token}`,
|
|
176
|
+
};
|
|
177
|
+
// Use raw axios call for FormData handling
|
|
178
|
+
const response = await axios.post(`${API_URL}/pages/import`, form, {
|
|
179
|
+
headers,
|
|
180
|
+
});
|
|
181
|
+
const newPageId = response.data.data.id;
|
|
182
|
+
// 2. Move to parent if needed
|
|
183
|
+
if (parentPageId) {
|
|
184
|
+
await this.movePage(newPageId, parentPageId);
|
|
185
|
+
}
|
|
186
|
+
// Return the final page object
|
|
187
|
+
return this.getPage(newPageId);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Update a page's content and optionally its title.
|
|
191
|
+
* Leverages WebSocket collaboration to update content without changing Page ID.
|
|
192
|
+
*/
|
|
193
|
+
async updatePage(pageId, content, title) {
|
|
194
|
+
await this.ensureAuthenticated();
|
|
195
|
+
// 1. Update Title via REST API if provided
|
|
196
|
+
if (title) {
|
|
197
|
+
await this.client.post("/pages/update", { pageId, title });
|
|
198
|
+
}
|
|
199
|
+
// 2. Update Content via WebSocket
|
|
200
|
+
let collabToken = "";
|
|
201
|
+
try {
|
|
202
|
+
const baseURL = this.client.defaults.baseURL || "";
|
|
203
|
+
collabToken = await getCollabToken(baseURL, this.token);
|
|
204
|
+
await updatePageContentRealtime(pageId, content, collabToken, baseURL);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
console.error("Failed to update page content via realtime collaboration:", error);
|
|
208
|
+
const tokenPreview = collabToken
|
|
209
|
+
? collabToken.substring(0, 15) + "..."
|
|
210
|
+
: "null";
|
|
211
|
+
throw new Error(`Failed to update page content: ${error.message} (Token: ${tokenPreview})`);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
modified: true,
|
|
216
|
+
message: "Page updated successfully.",
|
|
217
|
+
pageId: pageId,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
async search(query, spaceId) {
|
|
221
|
+
await this.ensureAuthenticated();
|
|
222
|
+
const response = await this.client.post("/search", {
|
|
223
|
+
query,
|
|
224
|
+
spaceId,
|
|
225
|
+
});
|
|
226
|
+
// Filter search results (data is directly an array)
|
|
227
|
+
const items = response.data?.data || [];
|
|
228
|
+
const filteredItems = items.map((item) => filterSearchResult(item));
|
|
229
|
+
return {
|
|
230
|
+
items: filteredItems,
|
|
231
|
+
success: response.data?.success || false,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async movePage(pageId, parentPageId, position) {
|
|
235
|
+
await this.ensureAuthenticated();
|
|
236
|
+
// Docmost requires position >= 5 chars.
|
|
237
|
+
const validPosition = position || "a00000";
|
|
238
|
+
return this.client
|
|
239
|
+
.post("/pages/move", {
|
|
240
|
+
pageId,
|
|
241
|
+
parentPageId,
|
|
242
|
+
position: validPosition,
|
|
243
|
+
})
|
|
244
|
+
.then((res) => res.data);
|
|
245
|
+
}
|
|
246
|
+
async deletePage(pageId) {
|
|
247
|
+
await this.ensureAuthenticated();
|
|
248
|
+
return this.client
|
|
249
|
+
.post("/pages/delete", { pageId })
|
|
250
|
+
.then((res) => res.data);
|
|
251
|
+
}
|
|
252
|
+
async deletePages(pageIds) {
|
|
253
|
+
await this.ensureAuthenticated();
|
|
254
|
+
const promises = pageIds.map((id) => this.client
|
|
255
|
+
.post("/pages/delete", { pageId: id })
|
|
256
|
+
.then(() => ({ id, success: true }))
|
|
257
|
+
.catch((err) => ({ id, success: false, error: err.message })));
|
|
258
|
+
return Promise.all(promises);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const docmostClient = new DocmostClient(API_URL);
|
|
262
|
+
// --- Modern McpServer Implementation ---
|
|
263
|
+
const server = new McpServer({
|
|
264
|
+
name: "docmost-mcp",
|
|
265
|
+
version: VERSION,
|
|
266
|
+
});
|
|
267
|
+
// Helper to format JSON responses
|
|
268
|
+
const jsonContent = (data) => ({
|
|
269
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
270
|
+
});
|
|
271
|
+
// Tool: list_workspaces
|
|
272
|
+
server.registerTool("get_workspace", {
|
|
273
|
+
description: "Get the current Docmost workspace",
|
|
274
|
+
}, async () => {
|
|
275
|
+
const workspace = await docmostClient.getWorkspace();
|
|
276
|
+
return jsonContent(workspace);
|
|
277
|
+
});
|
|
278
|
+
// Tool: list_spaces
|
|
279
|
+
server.registerTool("list_spaces", {
|
|
280
|
+
description: "List all available spaces in Docmost",
|
|
281
|
+
}, async () => {
|
|
282
|
+
const spaces = await docmostClient.getSpaces();
|
|
283
|
+
return jsonContent(spaces);
|
|
284
|
+
});
|
|
285
|
+
// Tool: list_groups
|
|
286
|
+
server.registerTool("list_groups", {
|
|
287
|
+
description: "List all available groups in Docmost",
|
|
288
|
+
}, async () => {
|
|
289
|
+
const groups = await docmostClient.getGroups();
|
|
290
|
+
return jsonContent(groups);
|
|
291
|
+
});
|
|
292
|
+
// Tool: list_pages
|
|
293
|
+
server.registerTool("list_pages", {
|
|
294
|
+
description: "List pages in a space ordered by updatedAt (descending).",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
spaceId: z.string().optional(),
|
|
297
|
+
},
|
|
298
|
+
}, async ({ spaceId }) => {
|
|
299
|
+
const result = await docmostClient.listPages(spaceId);
|
|
300
|
+
return jsonContent(result);
|
|
301
|
+
});
|
|
302
|
+
// Tool: get_page
|
|
303
|
+
server.registerTool("get_page", {
|
|
304
|
+
description: "Get details and content of a specific page by ID",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
pageId: z.string(),
|
|
307
|
+
},
|
|
308
|
+
}, async ({ pageId }) => {
|
|
309
|
+
const page = await docmostClient.getPage(pageId);
|
|
310
|
+
return jsonContent(page);
|
|
311
|
+
});
|
|
312
|
+
// Tool: create_page (Smart)
|
|
313
|
+
server.registerTool("create_page", {
|
|
314
|
+
description: "Create a new page with content (automatically moves it to the correct hierarchy).",
|
|
315
|
+
inputSchema: {
|
|
316
|
+
title: z.string().describe("Title of the page"),
|
|
317
|
+
content: z.string().describe("Markdown content"),
|
|
318
|
+
spaceId: z.string(),
|
|
319
|
+
parentPageId: z
|
|
320
|
+
.string()
|
|
321
|
+
.optional()
|
|
322
|
+
.describe("Optional parent page ID to nest under"),
|
|
323
|
+
},
|
|
324
|
+
}, async ({ title, content, spaceId, parentPageId }) => {
|
|
325
|
+
const result = await docmostClient.createPage(title, content, spaceId, parentPageId);
|
|
326
|
+
return jsonContent(result);
|
|
327
|
+
});
|
|
328
|
+
// Tool: update_page (Safe)
|
|
329
|
+
server.registerTool("update_page", {
|
|
330
|
+
description: "Update a page's content and/or title via realtime collaboration (preserves Page ID and history).",
|
|
331
|
+
inputSchema: {
|
|
332
|
+
pageId: z.string().describe("ID of the page to update"),
|
|
333
|
+
content: z.string().describe("New Markdown content"),
|
|
334
|
+
title: z.string().optional().describe("Optional new title"),
|
|
335
|
+
},
|
|
336
|
+
}, async ({ pageId, content, title }) => {
|
|
337
|
+
const result = await docmostClient.updatePage(pageId, content, title);
|
|
338
|
+
return jsonContent(result);
|
|
339
|
+
});
|
|
340
|
+
// Tool: move_page
|
|
341
|
+
server.registerTool("move_page", {
|
|
342
|
+
description: "Move a page to a new parent (nesting) or root. Essential for organizing pages created via 'import_page'.",
|
|
343
|
+
inputSchema: {
|
|
344
|
+
pageId: z.string(),
|
|
345
|
+
parentPageId: z
|
|
346
|
+
.string()
|
|
347
|
+
.nullable()
|
|
348
|
+
.optional()
|
|
349
|
+
.describe("Target parent page ID. Pass 'null' or empty string to move to root."),
|
|
350
|
+
position: z
|
|
351
|
+
.string()
|
|
352
|
+
.optional()
|
|
353
|
+
.describe("Optional position string (5-12 chars). Defaults to 'a00000' (end) if omitted."),
|
|
354
|
+
},
|
|
355
|
+
}, async ({ pageId, parentPageId, position }) => {
|
|
356
|
+
// Ensure parentPageId is null if string "null" or empty is passed, or undefined
|
|
357
|
+
// Note: Zod handles type checking, but we double check for empty strings just in case
|
|
358
|
+
const finalParentId = parentPageId === "" || parentPageId === "null" ? null : parentPageId;
|
|
359
|
+
await docmostClient.movePage(pageId, finalParentId || null, position);
|
|
360
|
+
return {
|
|
361
|
+
content: [
|
|
362
|
+
{
|
|
363
|
+
type: "text",
|
|
364
|
+
text: `Successfully moved page ${pageId} to parent ${finalParentId || "root"}`,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
};
|
|
368
|
+
});
|
|
369
|
+
// Tool: delete_page
|
|
370
|
+
server.registerTool("delete_page", {
|
|
371
|
+
description: "Delete a single page by ID.",
|
|
372
|
+
inputSchema: {
|
|
373
|
+
pageId: z.string(),
|
|
374
|
+
},
|
|
375
|
+
}, async ({ pageId }) => {
|
|
376
|
+
await docmostClient.deletePage(pageId);
|
|
377
|
+
return {
|
|
378
|
+
content: [{ type: "text", text: `Successfully deleted page ${pageId}` }],
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
// Tool: delete_pages
|
|
382
|
+
server.registerTool("delete_pages", {
|
|
383
|
+
description: "Delete multiple pages at once. Useful for cleanup.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
pageIds: z.array(z.string()),
|
|
386
|
+
},
|
|
387
|
+
}, async ({ pageIds }) => {
|
|
388
|
+
const results = await docmostClient.deletePages(pageIds);
|
|
389
|
+
return jsonContent(results);
|
|
390
|
+
});
|
|
391
|
+
// Tool: search
|
|
392
|
+
server.registerTool("search", {
|
|
393
|
+
description: "Search for pages and content.",
|
|
394
|
+
inputSchema: {
|
|
395
|
+
query: z.string().describe("Search query"),
|
|
396
|
+
spaceId: z.string().optional().describe("Optional space ID to filter by"),
|
|
397
|
+
},
|
|
398
|
+
}, async ({ query, spaceId }) => {
|
|
399
|
+
const result = await docmostClient.search(query, spaceId);
|
|
400
|
+
return jsonContent(result);
|
|
401
|
+
});
|
|
402
|
+
async function run() {
|
|
403
|
+
const transport = new StdioServerTransport();
|
|
404
|
+
await server.connect(transport);
|
|
405
|
+
}
|
|
406
|
+
run().catch((error) => {
|
|
407
|
+
console.error("Fatal error running server:", error);
|
|
408
|
+
process.exit(1);
|
|
409
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
export async function getCollabToken(baseUrl, apiToken) {
|
|
3
|
+
try {
|
|
4
|
+
const response = await axios.post(`${baseUrl}/auth/collab-token`, {}, {
|
|
5
|
+
headers: {
|
|
6
|
+
Authorization: `Bearer ${apiToken}`,
|
|
7
|
+
"Content-Type": "application/json",
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
// console.error('Collab Token Response:', response.data);
|
|
11
|
+
// Response is wrapped in { data: { token: ... } }
|
|
12
|
+
return response.data.data?.token || response.data.token;
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (axios.isAxiosError(error)) {
|
|
16
|
+
throw new Error(`Failed to get collab token: ${error.response?.status} ${error.response?.statusText} - ${JSON.stringify(error.response?.data)}`);
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function performLogin(baseUrl, email, password) {
|
|
22
|
+
try {
|
|
23
|
+
const response = await axios.post(`${baseUrl}/auth/login`, {
|
|
24
|
+
email,
|
|
25
|
+
password,
|
|
26
|
+
});
|
|
27
|
+
// Extract token from Set-Cookie header
|
|
28
|
+
const cookies = response.headers["set-cookie"];
|
|
29
|
+
if (!cookies) {
|
|
30
|
+
throw new Error("No Set-Cookie header found in login response");
|
|
31
|
+
}
|
|
32
|
+
const authCookie = cookies.find((c) => c.startsWith("authToken="));
|
|
33
|
+
if (!authCookie) {
|
|
34
|
+
throw new Error("No authToken cookie found in login response");
|
|
35
|
+
}
|
|
36
|
+
const token = authCookie.split(";")[0].split("=")[1];
|
|
37
|
+
return token;
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error("Login failed:", axios.isAxiosError(error) ? error.response?.data : error.message);
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { HocuspocusProvider } from "@hocuspocus/provider";
|
|
2
|
+
import { TiptapTransformer } from "@hocuspocus/transformer";
|
|
3
|
+
import * as Y from "yjs";
|
|
4
|
+
import WebSocket from "ws";
|
|
5
|
+
import { marked } from "marked";
|
|
6
|
+
import { generateJSON } from "@tiptap/html";
|
|
7
|
+
import { JSDOM } from "jsdom";
|
|
8
|
+
import { tiptapExtensions } from "./tiptap-extensions.js";
|
|
9
|
+
// Setup DOM environment for Tiptap HTML parsing in Node.js
|
|
10
|
+
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>");
|
|
11
|
+
global.window = dom.window;
|
|
12
|
+
global.document = dom.window.document;
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
global.Element = dom.window.Element;
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
global.WebSocket = WebSocket;
|
|
17
|
+
// Navigator is read-only in newer Node versions and already exists
|
|
18
|
+
// global.navigator = dom.window.navigator;
|
|
19
|
+
export async function updatePageContentRealtime(pageId, markdownContent, collabToken, baseUrl) {
|
|
20
|
+
console.error(`Starting realtime update for page ${pageId}`);
|
|
21
|
+
console.error(`Token prefix: ${collabToken ? collabToken.substring(0, 5) : "NONE"}...`);
|
|
22
|
+
// 1. Convert Markdown to HTML
|
|
23
|
+
const html = await marked.parse(markdownContent);
|
|
24
|
+
// 2. Convert HTML to ProseMirror JSON
|
|
25
|
+
const tiptapJson = generateJSON(html, tiptapExtensions);
|
|
26
|
+
// 3. Setup Hocuspocus Provider
|
|
27
|
+
const ydoc = new Y.Doc();
|
|
28
|
+
// Construct WebSocket URL
|
|
29
|
+
// Replace protocol
|
|
30
|
+
let wsUrl = baseUrl.replace(/^http/, "ws");
|
|
31
|
+
try {
|
|
32
|
+
const urlObj = new URL(wsUrl);
|
|
33
|
+
// Remove /api suffix if present, as the websocket is mounted on root /collab
|
|
34
|
+
if (urlObj.pathname.endsWith("/api") || urlObj.pathname.endsWith("/api/")) {
|
|
35
|
+
urlObj.pathname = urlObj.pathname.replace(/\/api\/?$/, "");
|
|
36
|
+
}
|
|
37
|
+
// Set correct path to /collab
|
|
38
|
+
urlObj.pathname = urlObj.pathname.replace(/\/$/, "") + "/collab";
|
|
39
|
+
wsUrl = urlObj.toString();
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
// Fallback if URL parsing fails
|
|
43
|
+
if (!wsUrl.endsWith("/collab")) {
|
|
44
|
+
wsUrl = wsUrl.replace(/\/$/, "") + "/collab";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
console.error(`Connecting to WebSocket: ${wsUrl}`);
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
// Safety timeout
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
if (provider)
|
|
52
|
+
provider.destroy();
|
|
53
|
+
reject(new Error("Connection timeout to collaboration server"));
|
|
54
|
+
}, 25000);
|
|
55
|
+
const provider = new HocuspocusProvider({
|
|
56
|
+
url: wsUrl,
|
|
57
|
+
name: `page.${pageId}`,
|
|
58
|
+
document: ydoc,
|
|
59
|
+
token: collabToken,
|
|
60
|
+
// @ts-ignore - Required for Node.js environment
|
|
61
|
+
WebSocketPolyfill: WebSocket,
|
|
62
|
+
onConnect: () => console.error("WS Connect"),
|
|
63
|
+
onDisconnect: (data) => console.error("WS Disconnect"),
|
|
64
|
+
onClose: (data) => console.error("WS Close"),
|
|
65
|
+
onSynced: () => {
|
|
66
|
+
console.error("Connected and synced!");
|
|
67
|
+
try {
|
|
68
|
+
// Prepare the new content in a separate doc
|
|
69
|
+
const tempDoc = TiptapTransformer.toYdoc(tiptapJson, "default", tiptapExtensions);
|
|
70
|
+
// Apply update
|
|
71
|
+
ydoc.transact(() => {
|
|
72
|
+
const fragment = ydoc.getXmlFragment("default");
|
|
73
|
+
// 1. Clear existing content
|
|
74
|
+
if (fragment.length > 0) {
|
|
75
|
+
fragment.delete(0, fragment.length);
|
|
76
|
+
}
|
|
77
|
+
// 2. Apply new content from tempDoc
|
|
78
|
+
// Note: applyUpdate adds content. Since we cleared, it should effectively replace.
|
|
79
|
+
// However, applyUpdate merges structures based on IDs. tempDoc has new IDs.
|
|
80
|
+
const update = Y.encodeStateAsUpdate(tempDoc);
|
|
81
|
+
Y.applyUpdate(ydoc, update);
|
|
82
|
+
});
|
|
83
|
+
console.error("Content replaced. Returning success to user immediately (Background persistence)...");
|
|
84
|
+
// Clear safety timeout as we are successful
|
|
85
|
+
clearTimeout(timer);
|
|
86
|
+
// Resolve immediately so the user doesn't have to wait
|
|
87
|
+
resolve();
|
|
88
|
+
// Keep connection open in background for save/sync (Docmost has 10s debounce)
|
|
89
|
+
// The node process will keep running this timeout even after the tool returns
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
try {
|
|
92
|
+
console.error(`Closing background connection for page ${pageId}`);
|
|
93
|
+
provider.destroy();
|
|
94
|
+
}
|
|
95
|
+
catch (err) { }
|
|
96
|
+
}, 15000);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
provider.destroy();
|
|
101
|
+
reject(e);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
onAuthenticationFailed: () => {
|
|
105
|
+
clearTimeout(timer);
|
|
106
|
+
provider.destroy();
|
|
107
|
+
reject(new Error("Authentication failed for collaboration connection"));
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filter functions to extract only relevant information from API responses
|
|
3
|
+
* for better agent consumption
|
|
4
|
+
*/
|
|
5
|
+
export function filterWorkspace(data) {
|
|
6
|
+
return {
|
|
7
|
+
id: data.id,
|
|
8
|
+
name: data.name,
|
|
9
|
+
description: data.description,
|
|
10
|
+
defaultSpaceId: data.defaultSpaceId,
|
|
11
|
+
createdAt: data.createdAt,
|
|
12
|
+
updatedAt: data.updatedAt,
|
|
13
|
+
deletedAt: data.deletedAt,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export function filterSpace(space) {
|
|
17
|
+
return {
|
|
18
|
+
id: space.id,
|
|
19
|
+
name: space.name,
|
|
20
|
+
description: space.description,
|
|
21
|
+
slug: space.slug,
|
|
22
|
+
visibility: space.visibility,
|
|
23
|
+
createdAt: space.createdAt,
|
|
24
|
+
updatedAt: space.updatedAt,
|
|
25
|
+
deletedAt: space.deletedAt,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export function filterGroup(group) {
|
|
29
|
+
return {
|
|
30
|
+
id: group.id,
|
|
31
|
+
name: group.name,
|
|
32
|
+
description: group.description,
|
|
33
|
+
workspaceId: group.workspaceId,
|
|
34
|
+
createdAt: group.createdAt,
|
|
35
|
+
updatedAt: group.updatedAt,
|
|
36
|
+
deletedAt: group.deletedAt,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function filterPage(page, content, subpages) {
|
|
40
|
+
return {
|
|
41
|
+
id: page.id,
|
|
42
|
+
title: page.title,
|
|
43
|
+
parentPageId: page.parentPageId,
|
|
44
|
+
spaceId: page.spaceId,
|
|
45
|
+
isLocked: page.isLocked,
|
|
46
|
+
createdAt: page.createdAt,
|
|
47
|
+
updatedAt: page.updatedAt,
|
|
48
|
+
deletedAt: page.deletedAt,
|
|
49
|
+
// Include converted markdown content if valid string (even empty)
|
|
50
|
+
...(typeof content === "string" && { content }),
|
|
51
|
+
// Include subpages if provided
|
|
52
|
+
...(subpages &&
|
|
53
|
+
subpages.length > 0 && {
|
|
54
|
+
subpages: subpages.map((p) => ({ id: p.id, title: p.title })),
|
|
55
|
+
}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function filterSearchResult(result) {
|
|
59
|
+
return {
|
|
60
|
+
id: result.id,
|
|
61
|
+
title: result.title,
|
|
62
|
+
parentPageId: result.parentPageId,
|
|
63
|
+
createdAt: result.createdAt,
|
|
64
|
+
updatedAt: result.updatedAt,
|
|
65
|
+
rank: result.rank,
|
|
66
|
+
highlight: result.highlight,
|
|
67
|
+
spaceId: result.space?.id,
|
|
68
|
+
spaceName: result.space?.name,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert ProseMirror/TipTap JSON content to Markdown
|
|
3
|
+
* Supports all Docmost-specific node types and extensions
|
|
4
|
+
*/
|
|
5
|
+
export function convertProseMirrorToMarkdown(content) {
|
|
6
|
+
if (!content || !content.content)
|
|
7
|
+
return "";
|
|
8
|
+
const processNode = (node) => {
|
|
9
|
+
const type = node.type;
|
|
10
|
+
const nodeContent = node.content || [];
|
|
11
|
+
switch (type) {
|
|
12
|
+
case "doc":
|
|
13
|
+
return nodeContent.map(processNode).join("\n\n");
|
|
14
|
+
case "paragraph":
|
|
15
|
+
const text = nodeContent.map(processNode).join("");
|
|
16
|
+
const align = node.attrs?.textAlign;
|
|
17
|
+
if (align && align !== "left") {
|
|
18
|
+
return `<div align="${align}">${text}</div>`;
|
|
19
|
+
}
|
|
20
|
+
return text || "";
|
|
21
|
+
case "heading":
|
|
22
|
+
const level = node.attrs?.level || 1;
|
|
23
|
+
const headingText = nodeContent.map(processNode).join("");
|
|
24
|
+
return "#".repeat(level) + " " + headingText;
|
|
25
|
+
case "text":
|
|
26
|
+
let textContent = node.text || "";
|
|
27
|
+
// Apply marks (bold, italic, code, etc.)
|
|
28
|
+
if (node.marks) {
|
|
29
|
+
for (const mark of node.marks) {
|
|
30
|
+
switch (mark.type) {
|
|
31
|
+
case "bold":
|
|
32
|
+
textContent = `**${textContent}**`;
|
|
33
|
+
break;
|
|
34
|
+
case "italic":
|
|
35
|
+
textContent = `*${textContent}*`;
|
|
36
|
+
break;
|
|
37
|
+
case "code":
|
|
38
|
+
textContent = `\`${textContent}\``;
|
|
39
|
+
break;
|
|
40
|
+
case "link":
|
|
41
|
+
textContent = `[${textContent}](${mark.attrs?.href || ""})`;
|
|
42
|
+
break;
|
|
43
|
+
case "strike":
|
|
44
|
+
textContent = `~~${textContent}~~`;
|
|
45
|
+
break;
|
|
46
|
+
case "underline":
|
|
47
|
+
textContent = `<u>${textContent}</u>`;
|
|
48
|
+
break;
|
|
49
|
+
case "subscript":
|
|
50
|
+
textContent = `<sub>${textContent}</sub>`;
|
|
51
|
+
break;
|
|
52
|
+
case "superscript":
|
|
53
|
+
textContent = `<sup>${textContent}</sup>`;
|
|
54
|
+
break;
|
|
55
|
+
case "highlight":
|
|
56
|
+
const color = mark.attrs?.color || "yellow";
|
|
57
|
+
textContent = `<mark style="background-color: ${color}">${textContent}</mark>`;
|
|
58
|
+
break;
|
|
59
|
+
case "textStyle":
|
|
60
|
+
if (mark.attrs?.color) {
|
|
61
|
+
textContent = `<span style="color: ${mark.attrs.color}">${textContent}</span>`;
|
|
62
|
+
}
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return textContent;
|
|
68
|
+
case "codeBlock":
|
|
69
|
+
const language = node.attrs?.language || "";
|
|
70
|
+
const code = nodeContent.map(processNode).join("");
|
|
71
|
+
return "```" + language + "\n" + code + "\n```";
|
|
72
|
+
case "bulletList":
|
|
73
|
+
return nodeContent
|
|
74
|
+
.map((item) => processListItem(item, "-"))
|
|
75
|
+
.join("\n");
|
|
76
|
+
case "orderedList":
|
|
77
|
+
return nodeContent
|
|
78
|
+
.map((item, index) => processListItem(item, `${index + 1}.`))
|
|
79
|
+
.join("\n");
|
|
80
|
+
case "taskList":
|
|
81
|
+
return nodeContent.map((item) => processTaskItem(item)).join("\n");
|
|
82
|
+
case "taskItem":
|
|
83
|
+
const checked = node.attrs?.checked || false;
|
|
84
|
+
const checkbox = checked ? "[x]" : "[ ]";
|
|
85
|
+
return `- ${checkbox} ${nodeContent.map(processNode).join("\n")}`;
|
|
86
|
+
case "listItem":
|
|
87
|
+
return nodeContent.map(processNode).join("\n");
|
|
88
|
+
case "blockquote":
|
|
89
|
+
return nodeContent.map((n) => "> " + processNode(n)).join("\n");
|
|
90
|
+
case "horizontalRule":
|
|
91
|
+
return "---";
|
|
92
|
+
case "hardBreak":
|
|
93
|
+
return "\n";
|
|
94
|
+
case "image":
|
|
95
|
+
const imgAlt = node.attrs?.alt || "";
|
|
96
|
+
const imgSrc = node.attrs?.src || "";
|
|
97
|
+
const imgCaption = node.attrs?.caption || "";
|
|
98
|
+
return `${imgCaption ? `\n*${imgCaption}*` : ""}`;
|
|
99
|
+
case "video":
|
|
100
|
+
const videoSrc = node.attrs?.src || "";
|
|
101
|
+
return `🎥 [Video](${videoSrc})`;
|
|
102
|
+
case "youtube":
|
|
103
|
+
const youtubeUrl = node.attrs?.src || "";
|
|
104
|
+
return `📺 [YouTube Video](${youtubeUrl})`;
|
|
105
|
+
case "table":
|
|
106
|
+
return nodeContent.map(processNode).join("\n");
|
|
107
|
+
case "tableRow":
|
|
108
|
+
return "| " + nodeContent.map(processNode).join(" | ") + " |";
|
|
109
|
+
case "tableCell":
|
|
110
|
+
case "tableHeader":
|
|
111
|
+
return nodeContent.map(processNode).join("");
|
|
112
|
+
case "callout":
|
|
113
|
+
const calloutType = node.attrs?.type || "info";
|
|
114
|
+
const calloutContent = nodeContent.map(processNode).join("\n");
|
|
115
|
+
return `:::${calloutType.toLowerCase()}\n${calloutContent}\n:::`;
|
|
116
|
+
case "details":
|
|
117
|
+
return nodeContent.map(processNode).join("\n");
|
|
118
|
+
case "detailsSummary":
|
|
119
|
+
const summaryText = nodeContent.map(processNode).join("");
|
|
120
|
+
return `<details>\n<summary>${summaryText}</summary>\n`;
|
|
121
|
+
case "detailsContent":
|
|
122
|
+
const detailsText = nodeContent.map(processNode).join("\n");
|
|
123
|
+
return `${detailsText}\n</details>`;
|
|
124
|
+
case "mathInline":
|
|
125
|
+
const inlineMath = node.attrs?.text || "";
|
|
126
|
+
return `$${inlineMath}$`;
|
|
127
|
+
case "mathBlock":
|
|
128
|
+
const blockMath = node.attrs?.text || "";
|
|
129
|
+
return `$$\n${blockMath}\n$$`;
|
|
130
|
+
case "mention":
|
|
131
|
+
const mentionLabel = node.attrs?.label || node.attrs?.id || "";
|
|
132
|
+
return `@${mentionLabel}`;
|
|
133
|
+
case "attachment":
|
|
134
|
+
const attachmentName = node.attrs?.fileName || "attachment";
|
|
135
|
+
const attachmentUrl = node.attrs?.src || "";
|
|
136
|
+
return `📎 [${attachmentName}](${attachmentUrl})`;
|
|
137
|
+
case "drawio":
|
|
138
|
+
return `📊 [Draw.io Diagram]`;
|
|
139
|
+
case "excalidraw":
|
|
140
|
+
return `✏️ [Excalidraw Drawing]`;
|
|
141
|
+
case "embed":
|
|
142
|
+
const embedUrl = node.attrs?.src || "";
|
|
143
|
+
return `🔗 [Embedded Content](${embedUrl})`;
|
|
144
|
+
case "subpages":
|
|
145
|
+
return "{{SUBPAGES}}";
|
|
146
|
+
default:
|
|
147
|
+
// Fallback: process children
|
|
148
|
+
return nodeContent.map(processNode).join("");
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const processListItem = (item, prefix) => {
|
|
152
|
+
const itemContent = item.content || [];
|
|
153
|
+
const lines = itemContent.map(processNode);
|
|
154
|
+
return lines
|
|
155
|
+
.map((line, i) => i === 0 ? `${prefix} ${line}` : ` ${line}`)
|
|
156
|
+
.join("\n");
|
|
157
|
+
};
|
|
158
|
+
const processTaskItem = (item) => {
|
|
159
|
+
const checked = item.attrs?.checked || false;
|
|
160
|
+
const checkbox = checked ? "[x]" : "[ ]";
|
|
161
|
+
const itemContent = item.content || [];
|
|
162
|
+
const text = itemContent.map(processNode).join("");
|
|
163
|
+
return `- ${checkbox} ${text}`;
|
|
164
|
+
};
|
|
165
|
+
return processNode(content).trim();
|
|
166
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
2
|
+
import Image from "@tiptap/extension-image";
|
|
3
|
+
import Link from "@tiptap/extension-link";
|
|
4
|
+
// Define extensions compatible with standard Markdown features
|
|
5
|
+
// We use the default Tiptap extensions to handle basic content
|
|
6
|
+
export const tiptapExtensions = [
|
|
7
|
+
StarterKit.configure({
|
|
8
|
+
// Explicitly enable features that might be disabled in some contexts
|
|
9
|
+
codeBlock: {},
|
|
10
|
+
heading: {},
|
|
11
|
+
}),
|
|
12
|
+
Image.configure({
|
|
13
|
+
inline: true,
|
|
14
|
+
}),
|
|
15
|
+
Link.configure({
|
|
16
|
+
openOnClick: false,
|
|
17
|
+
}),
|
|
18
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "docmost-mcp",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A Model Context Protocol (MCP) server for Docmost, allowing AI agents to manage documentation spaces and pages.",
|
|
5
|
+
"main": "build/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"docmost-mcp": "build/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"build"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"prepublishOnly": "tsc",
|
|
15
|
+
"start": "node build/index.js",
|
|
16
|
+
"watch": "tsc --watch",
|
|
17
|
+
"inspector": "npx @modelcontextprotocol/inspector --config mcp_config.json"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"docmost",
|
|
22
|
+
"documentation",
|
|
23
|
+
"ai",
|
|
24
|
+
"agent"
|
|
25
|
+
],
|
|
26
|
+
"author": "Moritz Krause",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@hocuspocus/provider": "^3.4.4",
|
|
31
|
+
"@hocuspocus/transformer": "^3.4.4",
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.25.3",
|
|
33
|
+
"@tiptap/core": "^3.18.0",
|
|
34
|
+
"@tiptap/extension-image": "^3.18.0",
|
|
35
|
+
"@tiptap/extension-link": "^3.18.0",
|
|
36
|
+
"@tiptap/html": "^3.18.0",
|
|
37
|
+
"@tiptap/starter-kit": "^3.18.0",
|
|
38
|
+
"@types/jsdom": "^27.0.0",
|
|
39
|
+
"axios": "^1.6.0",
|
|
40
|
+
"form-data": "^4.0.0",
|
|
41
|
+
"jsdom": "^27.4.0",
|
|
42
|
+
"marked": "^17.0.1",
|
|
43
|
+
"ws": "^8.19.0",
|
|
44
|
+
"yjs": "^13.6.29",
|
|
45
|
+
"zod": "^3.22.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/form-data": "^2.5.0",
|
|
49
|
+
"@types/node": "^20.0.0",
|
|
50
|
+
"typescript": "^5.0.0"
|
|
51
|
+
}
|
|
52
|
+
}
|