docmost-community-mcp 1.0.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/dist/tools.js ADDED
@@ -0,0 +1,783 @@
1
+ import { z } from "zod";
2
+ import { DocmostError, VersionError } from "./errors.js";
3
+ import { asItems, errorResult, exportFileName, normalizeLabel, pageSummary, proseMirrorToMarkdown, slugify, textResult, } from "./util.js";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { dirname, join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+ const pageId = z.string().min(1).describe("Page UUID or slugId");
8
+ const spaceId = z.string().min(1).describe("Space UUID or slug");
9
+ const limit = z.number().int().min(1).max(100).optional().describe("Page size, 1-100");
10
+ const cursor = z.string().optional().describe("Pagination cursor from meta.nextCursor");
11
+ function hints(kind) {
12
+ return {
13
+ readOnlyHint: kind === "read",
14
+ destructiveHint: kind === "destructive",
15
+ openWorldHint: true,
16
+ };
17
+ }
18
+ function wrap(client, kind, fn) {
19
+ return async (args) => {
20
+ try {
21
+ if (kind !== "read") {
22
+ client.assertMutable();
23
+ }
24
+ return textResult(await fn(args));
25
+ }
26
+ catch (error) {
27
+ return errorResult(error);
28
+ }
29
+ };
30
+ }
31
+ export function registerTools(server, client) {
32
+ registerPageTools(server, client);
33
+ registerSpaceTools(server, client);
34
+ registerCommentTools(server, client);
35
+ registerSearchTools(server, client);
36
+ registerWorkspaceTools(server, client);
37
+ registerAttachmentTools(server, client);
38
+ registerLabelTools(server, client);
39
+ registerMemberTools(server, client);
40
+ }
41
+ function registerPageTools(server, client) {
42
+ server.registerTool("search_pages", {
43
+ description: "Full-text search for pages the authenticated user can access. Optionally scope to a space.",
44
+ annotations: hints("read"),
45
+ inputSchema: {
46
+ query: z.string().min(1).describe("Search text"),
47
+ space_id: spaceId.optional(),
48
+ limit,
49
+ offset: z.number().int().min(0).optional().describe("Result offset"),
50
+ },
51
+ }, wrap(client, "read", async (args) => {
52
+ const spaceIdValue = args.space_id
53
+ ? await client.resolveSpaceId(String(args.space_id))
54
+ : undefined;
55
+ const result = asItems(await client.request("/search", {
56
+ query: args.query,
57
+ spaceId: spaceIdValue,
58
+ limit: args.limit,
59
+ offset: args.offset,
60
+ }));
61
+ return {
62
+ items: result.items.map((item) => {
63
+ const page = item;
64
+ return {
65
+ id: page.id,
66
+ slugId: page.slugId,
67
+ title: page.title,
68
+ icon: page.icon,
69
+ spaceId: page.spaceId,
70
+ highlight: page.highlight,
71
+ rank: page.rank,
72
+ space: page.space,
73
+ };
74
+ }),
75
+ meta: result.meta,
76
+ };
77
+ }));
78
+ server.registerTool("get_page", {
79
+ description: "Get a page's metadata and Markdown body. Use format=json for raw ProseMirror.",
80
+ annotations: hints("read"),
81
+ inputSchema: {
82
+ page_id: pageId,
83
+ format: z.enum(["markdown", "html", "json"]).optional().describe("Content format. Default markdown"),
84
+ },
85
+ }, wrap(client, "read", async (args) => {
86
+ const format = args.format ?? "markdown";
87
+ const page = (await client.request("/pages/info", {
88
+ pageId: args.page_id,
89
+ format,
90
+ }));
91
+ return {
92
+ ...pageSummary(page),
93
+ content: page.content,
94
+ creator: page.creator,
95
+ lastUpdatedBy: page.lastUpdatedBy,
96
+ };
97
+ }));
98
+ server.registerTool("create_page", {
99
+ description: "Create a page in a space. Body is Markdown and is persisted in place (Docmost v0.71+). Can nest under a parent.",
100
+ annotations: hints("write"),
101
+ inputSchema: {
102
+ space_id: spaceId,
103
+ title: z.string().min(1).describe("Page title"),
104
+ markdown: z.string().optional().describe("Page body as Markdown"),
105
+ parent_page_id: z.string().optional().describe("Parent page UUID to nest under"),
106
+ icon: z.string().optional().describe("Page icon, usually an emoji"),
107
+ },
108
+ }, wrap(client, "write", async (args) => {
109
+ await client.assertWritable();
110
+ const resolvedSpaceId = await client.resolveSpaceId(String(args.space_id));
111
+ const markdown = args.markdown;
112
+ const created = (await client.request("/pages/create", {
113
+ spaceId: resolvedSpaceId,
114
+ title: args.title,
115
+ parentPageId: args.parent_page_id,
116
+ icon: args.icon,
117
+ ...(markdown
118
+ ? { content: markdown, format: "markdown" }
119
+ : {}),
120
+ }));
121
+ if (markdown) {
122
+ try {
123
+ await client.confirmMarkdownWrite(created, markdown);
124
+ }
125
+ catch (error) {
126
+ if (!(error instanceof VersionError) || typeof created.id !== "string") {
127
+ throw error;
128
+ }
129
+ const updated = await client.request("/pages/update", {
130
+ pageId: created.id,
131
+ title: args.title,
132
+ content: markdown,
133
+ format: "markdown",
134
+ operation: "replace",
135
+ });
136
+ await client.confirmMarkdownWrite(updated, markdown);
137
+ return pageSummary(updated);
138
+ }
139
+ }
140
+ return pageSummary(created);
141
+ }));
142
+ server.registerTool("update_page", {
143
+ description: "Update a page title, icon, and/or Markdown body in place. Body writes use the server converter (v0.71+). operation defaults to replace.",
144
+ annotations: hints("write"),
145
+ inputSchema: {
146
+ page_id: pageId,
147
+ title: z.string().optional(),
148
+ icon: z.string().optional(),
149
+ markdown: z.string().optional().describe("New Markdown body"),
150
+ operation: z
151
+ .enum(["replace", "append", "prepend"])
152
+ .optional()
153
+ .describe("How to apply markdown. Default replace"),
154
+ },
155
+ }, wrap(client, "write", async (args) => {
156
+ if (args.markdown) {
157
+ await client.assertWritable();
158
+ }
159
+ const updated = await client.request("/pages/update", {
160
+ pageId: args.page_id,
161
+ title: args.title,
162
+ icon: args.icon,
163
+ ...(args.markdown
164
+ ? {
165
+ content: args.markdown,
166
+ format: "markdown",
167
+ operation: args.operation ?? "replace",
168
+ }
169
+ : {}),
170
+ });
171
+ if (args.markdown) {
172
+ await client.confirmMarkdownWrite(updated, String(args.markdown));
173
+ }
174
+ return pageSummary(updated);
175
+ }));
176
+ server.registerTool("list_pages", {
177
+ description: "List pages in a space. Default view=recent is recently updated pages (not the sidebar tree; General can look empty). Use view=tree for space-root pages in sidebar order, or list_child_pages for children of a page.",
178
+ annotations: hints("read"),
179
+ inputSchema: {
180
+ space_id: spaceId,
181
+ view: z
182
+ .enum(["recent", "tree"])
183
+ .optional()
184
+ .describe("recent (default) or tree for sidebar-root pages"),
185
+ limit,
186
+ cursor,
187
+ },
188
+ }, wrap(client, "read", async (args) => {
189
+ const resolvedSpaceId = await client.resolveSpaceId(String(args.space_id));
190
+ const view = args.view ?? "recent";
191
+ const result = asItems(await client.request(view === "tree" ? "/pages/sidebar-pages" : "/pages/recent", {
192
+ spaceId: resolvedSpaceId,
193
+ limit: args.limit ?? 50,
194
+ cursor: args.cursor,
195
+ }));
196
+ return {
197
+ view,
198
+ items: result.items.map(pageSummary),
199
+ meta: result.meta,
200
+ };
201
+ }));
202
+ server.registerTool("list_child_pages", {
203
+ description: "List direct child pages of a page, in sidebar order. Omit page_id and pass space_id for space-root pages.",
204
+ annotations: hints("read"),
205
+ inputSchema: {
206
+ page_id: pageId.optional(),
207
+ space_id: spaceId.optional(),
208
+ limit,
209
+ cursor,
210
+ },
211
+ }, wrap(client, "read", async (args) => {
212
+ if (!args.page_id && !args.space_id) {
213
+ throw new Error("Provide page_id or space_id");
214
+ }
215
+ const result = asItems(await client.request("/pages/sidebar-pages", {
216
+ pageId: args.page_id,
217
+ spaceId: args.space_id
218
+ ? await client.resolveSpaceId(String(args.space_id))
219
+ : undefined,
220
+ limit: args.limit ?? 50,
221
+ cursor: args.cursor,
222
+ }));
223
+ return {
224
+ items: result.items.map(pageSummary),
225
+ meta: result.meta,
226
+ };
227
+ }));
228
+ server.registerTool("duplicate_page", {
229
+ description: "Duplicate a page and its accessible sub-pages within the same space.",
230
+ annotations: hints("write"),
231
+ inputSchema: { page_id: pageId },
232
+ }, wrap(client, "write", async (args) => client.request("/pages/duplicate", { pageId: args.page_id })));
233
+ server.registerTool("copy_page_to_space", {
234
+ description: "Copy a page and its accessible sub-pages into a different space.",
235
+ annotations: hints("write"),
236
+ inputSchema: {
237
+ page_id: pageId,
238
+ space_id: spaceId.describe("Destination space"),
239
+ },
240
+ }, wrap(client, "write", async (args) => client.request("/pages/duplicate", {
241
+ pageId: args.page_id,
242
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
243
+ })));
244
+ server.registerTool("move_page", {
245
+ description: "Move a page under a new parent or to the space root. Position is computed unless you pass an explicit 5-12 character key.",
246
+ annotations: hints("write"),
247
+ inputSchema: {
248
+ page_id: pageId,
249
+ parent_page_id: z
250
+ .string()
251
+ .nullable()
252
+ .optional()
253
+ .describe("New parent page UUID. Null or omitted with root=true moves to space root"),
254
+ root: z.boolean().optional().describe("Move to the space root"),
255
+ position: z
256
+ .enum(["first", "last"])
257
+ .or(z.string().min(5).max(12))
258
+ .optional()
259
+ .describe("first, last, or an explicit fractional index"),
260
+ after_page_id: z.string().optional().describe("Place after this sibling page"),
261
+ },
262
+ }, wrap(client, "write", async (args) => {
263
+ const parentPageId = args.root ? null : args.parent_page_id;
264
+ const position = await client.computeMovePosition({
265
+ pageId: String(args.page_id),
266
+ parentPageId,
267
+ position: args.position,
268
+ afterPageId: args.after_page_id,
269
+ });
270
+ return client.request("/pages/move", {
271
+ pageId: args.page_id,
272
+ parentPageId,
273
+ position,
274
+ });
275
+ }));
276
+ server.registerTool("move_page_to_space", {
277
+ description: "Move a page and its accessible sub-pages to a different space.",
278
+ annotations: hints("write"),
279
+ inputSchema: {
280
+ page_id: pageId,
281
+ space_id: spaceId.describe("Destination space"),
282
+ },
283
+ }, wrap(client, "write", async (args) => client.request("/pages/move-to-space", {
284
+ pageId: args.page_id,
285
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
286
+ })));
287
+ server.registerTool("delete_page", {
288
+ description: "Move a page to trash, or permanently delete it. Permanent delete requires space admin.",
289
+ annotations: hints("destructive"),
290
+ inputSchema: {
291
+ page_id: pageId,
292
+ permanently: z.boolean().optional().describe("If true, permanently delete. Default false (trash)"),
293
+ },
294
+ }, wrap(client, "destructive", async (args) => {
295
+ await client.request("/pages/delete", {
296
+ pageId: args.page_id,
297
+ permanentlyDelete: Boolean(args.permanently),
298
+ });
299
+ return {
300
+ pageId: args.page_id,
301
+ deleted: true,
302
+ permanent: Boolean(args.permanently),
303
+ };
304
+ }));
305
+ server.registerTool("restore_page", {
306
+ description: "Restore a soft-deleted page from trash.",
307
+ annotations: hints("write"),
308
+ inputSchema: { page_id: pageId },
309
+ }, wrap(client, "write", async (args) => pageSummary(await client.request("/pages/restore", { pageId: args.page_id }))));
310
+ server.registerTool("list_trash", {
311
+ description: "List soft-deleted pages in a space.",
312
+ annotations: hints("read"),
313
+ inputSchema: { space_id: spaceId, limit, cursor },
314
+ }, wrap(client, "read", async (args) => {
315
+ const result = asItems(await client.request("/pages/trash", {
316
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
317
+ limit: args.limit ?? 50,
318
+ cursor: args.cursor,
319
+ }));
320
+ return { items: result.items.map(pageSummary), meta: result.meta };
321
+ }));
322
+ server.registerTool("get_page_history", {
323
+ description: "List revision history for a page.",
324
+ annotations: hints("read"),
325
+ inputSchema: { page_id: pageId, limit, cursor },
326
+ }, wrap(client, "read", async (args) => asItems(await client.request("/pages/history", {
327
+ pageId: args.page_id,
328
+ limit: args.limit ?? 20,
329
+ cursor: args.cursor,
330
+ }))));
331
+ server.registerTool("get_history_version", {
332
+ description: "Get a specific page history version by history ID.",
333
+ annotations: hints("read"),
334
+ inputSchema: {
335
+ history_id: z.string().uuid().describe("History version UUID"),
336
+ },
337
+ }, wrap(client, "read", async (args) => client.request("/pages/history/info", { historyId: args.history_id })));
338
+ server.registerTool("get_breadcrumbs", {
339
+ description: "Get the ancestor path from space root to a page.",
340
+ annotations: hints("read"),
341
+ inputSchema: { page_id: pageId },
342
+ }, wrap(client, "read", async (args) => client.request("/pages/breadcrumbs", { pageId: args.page_id })));
343
+ server.registerTool("get_backlinks", {
344
+ description: "List incoming or outgoing page links.",
345
+ annotations: hints("read"),
346
+ inputSchema: {
347
+ page_id: pageId,
348
+ direction: z.enum(["incoming", "outgoing"]).describe("incoming or outgoing"),
349
+ limit,
350
+ cursor,
351
+ },
352
+ }, wrap(client, "read", async (args) => asItems(await client.request("/pages/backlinks", {
353
+ pageId: args.page_id,
354
+ direction: args.direction,
355
+ limit: args.limit ?? 50,
356
+ cursor: args.cursor,
357
+ }))));
358
+ server.registerTool("export_page", {
359
+ description: "Export a page to a local file. A page without children is often a .md or .html file; include_children usually returns a zip. Writes to output_path or a temp file and returns the path plus content type.",
360
+ annotations: hints("read"),
361
+ inputSchema: {
362
+ page_id: pageId,
363
+ format: z.enum(["markdown", "html"]).optional(),
364
+ include_children: z.boolean().optional(),
365
+ include_attachments: z.boolean().optional(),
366
+ output_path: z.string().optional().describe("Where to write the exported file"),
367
+ },
368
+ }, wrap(client, "read", async (args) => exportZip(client, "/pages/export", {
369
+ pageId: args.page_id,
370
+ format: args.format ?? "markdown",
371
+ includeChildren: args.include_children,
372
+ includeAttachments: args.include_attachments,
373
+ }, args.output_path, `page-${args.page_id}`)));
374
+ }
375
+ function registerSpaceTools(server, client) {
376
+ server.registerTool("list_spaces", {
377
+ description: "List spaces the authenticated user can access.",
378
+ annotations: hints("read"),
379
+ inputSchema: { limit, cursor },
380
+ }, wrap(client, "read", async (args) => {
381
+ const result = asItems(await client.request("/spaces", {
382
+ limit: args.limit ?? 100,
383
+ cursor: args.cursor,
384
+ }));
385
+ return {
386
+ items: result.items.map((item) => spaceSummary(item)),
387
+ meta: result.meta,
388
+ };
389
+ }));
390
+ server.registerTool("get_space", {
391
+ description: "Get details for a space, including the current user's membership.",
392
+ annotations: hints("read"),
393
+ inputSchema: { space_id: spaceId },
394
+ }, wrap(client, "read", async (args) => {
395
+ const id = await client.resolveSpaceId(String(args.space_id));
396
+ return spaceSummary(await client.request("/spaces/info", { spaceId: id }));
397
+ }));
398
+ server.registerTool("create_space", {
399
+ description: "Create a space. Slug is generated from the name if omitted. Requires permission to manage spaces.",
400
+ annotations: hints("write"),
401
+ inputSchema: {
402
+ name: z.string().min(2).max(100),
403
+ slug: z.string().min(2).max(100).optional(),
404
+ description: z.string().optional(),
405
+ },
406
+ }, wrap(client, "write", async (args) => spaceSummary(await client.request("/spaces/create", {
407
+ name: args.name,
408
+ slug: args.slug ?? slugify(String(args.name)),
409
+ description: args.description,
410
+ }))));
411
+ server.registerTool("update_space", {
412
+ description: "Update a space name, slug, or description.",
413
+ annotations: hints("write"),
414
+ inputSchema: {
415
+ space_id: spaceId,
416
+ name: z.string().min(2).max(100).optional(),
417
+ slug: z.string().min(2).max(100).optional(),
418
+ description: z.string().optional(),
419
+ },
420
+ }, wrap(client, "write", async (args) => spaceSummary(await client.request("/spaces/update", {
421
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
422
+ name: args.name,
423
+ slug: args.slug,
424
+ description: args.description,
425
+ }))));
426
+ server.registerTool("delete_space", {
427
+ description: "Delete a space and its pages. This cannot be undone.",
428
+ annotations: hints("destructive"),
429
+ inputSchema: {
430
+ space_id: spaceId,
431
+ confirm: z.literal(true).describe("Must be true to confirm deletion"),
432
+ },
433
+ }, wrap(client, "destructive", async (args) => {
434
+ const id = await client.resolveSpaceId(String(args.space_id));
435
+ await client.request("/spaces/delete", { spaceId: id });
436
+ return { spaceId: id, deleted: true };
437
+ }));
438
+ server.registerTool("export_space", {
439
+ description: "Export a whole space as a zip of Markdown or HTML. Writes to output_path or a temp file.",
440
+ annotations: hints("read"),
441
+ inputSchema: {
442
+ space_id: spaceId,
443
+ format: z.enum(["markdown", "html"]).optional(),
444
+ include_attachments: z.boolean().optional(),
445
+ output_path: z.string().optional(),
446
+ },
447
+ }, wrap(client, "read", async (args) => {
448
+ const id = await client.resolveSpaceId(String(args.space_id));
449
+ return exportZip(client, "/spaces/export", {
450
+ spaceId: id,
451
+ format: args.format ?? "markdown",
452
+ includeAttachments: args.include_attachments,
453
+ }, args.output_path, `space-${id}`);
454
+ }));
455
+ }
456
+ function registerCommentTools(server, client) {
457
+ server.registerTool("get_comments", {
458
+ description: "List page-level comments. Content is returned as Markdown. Each comment is re-fetched so edits are not stale.",
459
+ annotations: hints("read"),
460
+ inputSchema: { page_id: pageId, limit, cursor },
461
+ }, wrap(client, "read", async (args) => {
462
+ const result = asItems(await client.request("/comments", {
463
+ pageId: args.page_id,
464
+ limit: args.limit ?? 50,
465
+ cursor: args.cursor,
466
+ }));
467
+ const items = await Promise.all(result.items.map((item) => hydrateComment(client, item)));
468
+ return {
469
+ items,
470
+ meta: result.meta,
471
+ };
472
+ }));
473
+ server.registerTool("create_comment", {
474
+ description: "Add a page-level comment. Inline selection comments are not supported.",
475
+ annotations: hints("write"),
476
+ inputSchema: {
477
+ page_id: pageId,
478
+ markdown: z.string().min(1).describe("Comment body as Markdown"),
479
+ parent_comment_id: z.string().uuid().optional().describe("Parent comment UUID to reply"),
480
+ },
481
+ }, wrap(client, "write", async (args) => commentSummary(await client.request("/comments/create", {
482
+ pageId: args.page_id,
483
+ content: client.commentContent(String(args.markdown)),
484
+ type: "page",
485
+ parentCommentId: args.parent_comment_id,
486
+ }))));
487
+ server.registerTool("update_comment", {
488
+ description: "Replace a comment body. You can update your own comments.",
489
+ annotations: hints("write"),
490
+ inputSchema: {
491
+ comment_id: z.string().uuid(),
492
+ markdown: z.string().min(1),
493
+ },
494
+ }, wrap(client, "write", async (args) => commentSummary(await client.request("/comments/update", {
495
+ commentId: args.comment_id,
496
+ content: client.commentContent(String(args.markdown)),
497
+ }))));
498
+ server.registerTool("delete_comment", {
499
+ description: "Delete a comment. Owners can delete their own; space admins can delete any comment.",
500
+ annotations: hints("destructive"),
501
+ inputSchema: { comment_id: z.string().uuid() },
502
+ }, wrap(client, "destructive", async (args) => {
503
+ await client.request("/comments/delete", { commentId: args.comment_id });
504
+ return { commentId: args.comment_id, deleted: true };
505
+ }));
506
+ }
507
+ function registerSearchTools(server, client) {
508
+ server.registerTool("search_attachments", {
509
+ description: "Search file attachments by name or indexed text. This is an Enterprise feature; Community Edition returns 403. Prefer get_attachment_info when you already have an attachment id.",
510
+ annotations: hints("read"),
511
+ inputSchema: {
512
+ query: z.string().min(1),
513
+ space_id: spaceId.optional(),
514
+ limit,
515
+ },
516
+ }, wrap(client, "read", async (args) => {
517
+ const spaceIdValue = args.space_id
518
+ ? await client.resolveSpaceId(String(args.space_id))
519
+ : undefined;
520
+ try {
521
+ return asItems(await client.request("/search-attachments", {
522
+ query: args.query,
523
+ spaceId: spaceIdValue,
524
+ limit: args.limit,
525
+ }));
526
+ }
527
+ catch (error) {
528
+ throw attachmentSearchError(error);
529
+ }
530
+ }));
531
+ server.registerTool("search_suggest", {
532
+ description: "Typeahead suggestions. Pages are included by default (set include_pages=false to skip). Users and groups default to off.",
533
+ annotations: hints("read"),
534
+ inputSchema: {
535
+ query: z.string().min(1),
536
+ space_id: spaceId.optional(),
537
+ include_users: z.boolean().optional(),
538
+ include_groups: z.boolean().optional(),
539
+ include_pages: z
540
+ .boolean()
541
+ .optional()
542
+ .describe("Include page titles. Default true"),
543
+ limit,
544
+ },
545
+ }, wrap(client, "read", async (args) => client.request("/search/suggest", {
546
+ query: args.query,
547
+ spaceId: args.space_id
548
+ ? await client.resolveSpaceId(String(args.space_id))
549
+ : undefined,
550
+ includeUsers: args.include_users ?? false,
551
+ includeGroups: args.include_groups ?? false,
552
+ includePages: args.include_pages ?? true,
553
+ limit: args.limit,
554
+ })));
555
+ }
556
+ function registerWorkspaceTools(server, client) {
557
+ server.registerTool("get_current_user", {
558
+ description: "Get the authenticated user and workspace context, detected Docmost version, and whether this session is read-only.",
559
+ annotations: hints("read"),
560
+ }, wrap(client, "read", async () => {
561
+ const me = (await client.request("/users/me", {}));
562
+ const probe = await client.sessionInfo();
563
+ return { ...me, ...probe };
564
+ }));
565
+ server.registerTool("list_workspace_members", {
566
+ description: "List workspace members.",
567
+ annotations: hints("read"),
568
+ inputSchema: {
569
+ limit,
570
+ cursor,
571
+ query: z.string().optional().describe("Optional member search text"),
572
+ },
573
+ }, wrap(client, "read", async (args) => asItems(await client.request("/workspace/members", {
574
+ limit: args.limit ?? 50,
575
+ cursor: args.cursor,
576
+ query: args.query,
577
+ }))));
578
+ }
579
+ function registerAttachmentTools(server, client) {
580
+ server.registerTool("upload_attachment", {
581
+ description: "Upload a local file to a page. Returns attachment metadata including the /api/files/:id/:name URL path.",
582
+ annotations: hints("write"),
583
+ inputSchema: {
584
+ page_id: pageId,
585
+ file_path: z.string().min(1).describe("Absolute path to a local file"),
586
+ },
587
+ }, wrap(client, "write", async (args) => client.uploadFile(String(args.page_id), String(args.file_path))));
588
+ server.registerTool("get_attachment_info", {
589
+ description: "Get metadata for an uploaded attachment.",
590
+ annotations: hints("read"),
591
+ inputSchema: {
592
+ attachment_id: z.string().uuid(),
593
+ },
594
+ }, wrap(client, "read", async (args) => client.request("/files/info", { attachmentId: args.attachment_id })));
595
+ }
596
+ function registerLabelTools(server, client) {
597
+ server.registerTool("list_page_labels", {
598
+ description: "List labels on a page.",
599
+ annotations: hints("read"),
600
+ inputSchema: { page_id: pageId, limit, cursor },
601
+ }, wrap(client, "read", async (args) => asItems(await client.request("/pages/labels", {
602
+ pageId: args.page_id,
603
+ limit: args.limit ?? 50,
604
+ cursor: args.cursor,
605
+ }))));
606
+ server.registerTool("add_page_labels", {
607
+ description: "Add one or more labels to a page. Names are normalized to lowercase kebab-case.",
608
+ annotations: hints("write"),
609
+ inputSchema: {
610
+ page_id: pageId,
611
+ names: z.array(z.string().min(1)).min(1).max(25),
612
+ },
613
+ }, wrap(client, "write", async (args) => client.request("/pages/labels/add", {
614
+ pageId: args.page_id,
615
+ names: args.names.map(normalizeLabel).filter(Boolean),
616
+ })));
617
+ server.registerTool("remove_page_label", {
618
+ description: "Remove a label from a page by label ID.",
619
+ annotations: hints("write"),
620
+ inputSchema: {
621
+ page_id: pageId,
622
+ label_id: z.string().uuid(),
623
+ },
624
+ }, wrap(client, "write", async (args) => {
625
+ await client.request("/pages/labels/remove", {
626
+ pageId: args.page_id,
627
+ labelId: args.label_id,
628
+ });
629
+ return { pageId: args.page_id, labelId: args.label_id, removed: true };
630
+ }));
631
+ }
632
+ function registerMemberTools(server, client) {
633
+ server.registerTool("list_space_members", {
634
+ description: "List members and groups in a space.",
635
+ annotations: hints("read"),
636
+ inputSchema: { space_id: spaceId, limit, cursor },
637
+ }, wrap(client, "read", async (args) => asItems(await client.request("/spaces/members", {
638
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
639
+ limit: args.limit ?? 50,
640
+ cursor: args.cursor,
641
+ }))));
642
+ server.registerTool("add_space_members", {
643
+ description: "Add users and/or groups to a space with a role.",
644
+ annotations: hints("write"),
645
+ inputSchema: {
646
+ space_id: spaceId,
647
+ role: z.enum(["admin", "writer", "reader"]),
648
+ user_ids: z.array(z.string().uuid()).optional(),
649
+ group_ids: z.array(z.string().uuid()).optional(),
650
+ },
651
+ }, wrap(client, "write", async (args) => {
652
+ const userIds = args.user_ids ?? [];
653
+ const groupIds = args.group_ids ?? [];
654
+ if (userIds.length === 0 && groupIds.length === 0) {
655
+ throw new Error("Provide user_ids or group_ids");
656
+ }
657
+ return client.request("/spaces/members/add", {
658
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
659
+ role: args.role,
660
+ userIds,
661
+ groupIds,
662
+ });
663
+ }));
664
+ server.registerTool("remove_space_member", {
665
+ description: "Remove a user or group from a space.",
666
+ annotations: hints("destructive"),
667
+ inputSchema: {
668
+ space_id: spaceId,
669
+ user_id: z.string().uuid().optional(),
670
+ group_id: z.string().uuid().optional(),
671
+ },
672
+ }, wrap(client, "destructive", async (args) => {
673
+ if (Boolean(args.user_id) === Boolean(args.group_id)) {
674
+ throw new Error("Provide exactly one of user_id or group_id");
675
+ }
676
+ await client.request("/spaces/members/remove", {
677
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
678
+ userId: args.user_id,
679
+ groupId: args.group_id,
680
+ });
681
+ return { removed: true };
682
+ }));
683
+ server.registerTool("update_space_member_role", {
684
+ description: "Change a space member or group's role.",
685
+ annotations: hints("write"),
686
+ inputSchema: {
687
+ space_id: spaceId,
688
+ role: z.enum(["admin", "writer", "reader"]),
689
+ user_id: z.string().uuid().optional(),
690
+ group_id: z.string().uuid().optional(),
691
+ },
692
+ }, wrap(client, "write", async (args) => {
693
+ if (Boolean(args.user_id) === Boolean(args.group_id)) {
694
+ throw new Error("Provide exactly one of user_id or group_id");
695
+ }
696
+ return client.request("/spaces/members/change-role", {
697
+ spaceId: await client.resolveSpaceId(String(args.space_id)),
698
+ role: args.role,
699
+ userId: args.user_id,
700
+ groupId: args.group_id,
701
+ });
702
+ }));
703
+ }
704
+ function spaceSummary(space) {
705
+ if (!space || typeof space !== "object") {
706
+ return { space };
707
+ }
708
+ const record = space;
709
+ return {
710
+ id: record.id,
711
+ name: record.name,
712
+ slug: record.slug,
713
+ description: record.description,
714
+ hostname: record.hostname,
715
+ createdAt: record.createdAt,
716
+ updatedAt: record.updatedAt,
717
+ membership: record.membership,
718
+ memberCount: record.memberCount,
719
+ };
720
+ }
721
+ function commentSummary(comment) {
722
+ if (!comment || typeof comment !== "object") {
723
+ return { comment };
724
+ }
725
+ const record = comment;
726
+ const inner = record.comment && typeof record.comment === "object" ? record.comment : record;
727
+ const content = inner.content ?? inner.json ?? inner.body;
728
+ return {
729
+ id: inner.id,
730
+ pageId: inner.pageId,
731
+ parentCommentId: inner.parentCommentId,
732
+ type: inner.type,
733
+ creatorId: inner.creatorId,
734
+ createdAt: inner.createdAt,
735
+ updatedAt: inner.updatedAt,
736
+ editedAt: inner.editedAt,
737
+ resolvedAt: inner.resolvedAt,
738
+ content: proseMirrorToMarkdown(content),
739
+ creator: inner.creator,
740
+ };
741
+ }
742
+ async function hydrateComment(client, item) {
743
+ const summary = commentSummary(item);
744
+ const id = summary.id;
745
+ if (typeof id !== "string") {
746
+ return summary;
747
+ }
748
+ try {
749
+ const fresh = await client.request("/comments/info", { commentId: id });
750
+ return commentSummary(fresh);
751
+ }
752
+ catch {
753
+ return summary;
754
+ }
755
+ }
756
+ function attachmentSearchError(error) {
757
+ const status = error instanceof DocmostError ? error.status : undefined;
758
+ const message = error instanceof Error ? error.message : String(error);
759
+ if (status === 403 || /requires a valid license/i.test(message)) {
760
+ return new DocmostError("search_attachments requires a Docmost Enterprise license. Community Edition cannot search attachments through this endpoint (HTTP 403). Use get_attachment_info if you already have an attachment id.", 403);
761
+ }
762
+ return error instanceof Error ? error : new Error(message);
763
+ }
764
+ async function exportZip(client, path, body, outputPath, fallbackName) {
765
+ const response = await client.request(path, body, { raw: true });
766
+ const bytes = Buffer.from(await response.arrayBuffer());
767
+ const fileName = exportFileName({
768
+ contentDisposition: response.headers.get("content-disposition"),
769
+ contentType: response.headers.get("content-type"),
770
+ bytes,
771
+ fallbackBase: fallbackName,
772
+ });
773
+ const dest = outputPath ?? join(tmpdir(), fileName);
774
+ await mkdir(dirname(dest), { recursive: true });
775
+ await writeFile(dest, bytes);
776
+ return {
777
+ path: dest,
778
+ fileName,
779
+ bytes: bytes.length,
780
+ contentType: response.headers.get("content-type"),
781
+ };
782
+ }
783
+ //# sourceMappingURL=tools.js.map