From c520713291a18cf90183233067cfcafe5fb76fb1 Mon Sep 17 00:00:00 2001 From: Alejandro Lembke Barrientos Date: Sun, 31 May 2026 02:45:43 +0000 Subject: [PATCH] feat: add comment support (list, get, create, update, delete, check_new) Adds 6 new MCP tools for managing Docmost page comments: - list_comments: list all comments on a page (content as Markdown) - get_comment: retrieve a single comment by ID - create_comment: create page-level or inline comment (Markdown input) - update_comment: update comment content (creator only) - delete_comment: delete a comment (creator or space admin) - check_new_comments: poll for new comments since a timestamp Implementation details: - New markdownToTiptapJson() utility in src/lib/markdown-to-json.ts - New filterComment() in src/lib/filters.ts - Supports inline comments, threaded replies via parentCommentId - README updated with comment tools documentation Co-Authored-By: Claude Sonnet 4.6 --- README.md | 9 ++ package-lock.json | 189 ++++------------------------ src/index.ts | 310 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 167 deletions(-) diff --git a/README.md b/README.md index f9b0f00..6cb0ac6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,15 @@ A Model Context Protocol (MCP) server for [Docmost](https://docmost.com/), enabl - **`list_pages`**: List pages within a space (ordered by `updatedAt` descending). - **`get_page`**: Retrieve full content and metadata of a specific page. +### Comments + +- **`list_comments`**: List all comments on a page with content converted to Markdown. +- **`get_comment`**: Retrieve a single comment by ID. +- **`create_comment`**: Create a page-level or inline comment. Content is provided as Markdown and automatically converted to ProseMirror JSON. Supports replies via `parentCommentId`. +- **`update_comment`**: Update an existing comment's content (creator only). +- **`delete_comment`**: Delete a comment (creator or space admin only). +- **`check_new_comments`**: Check for new comments across a space (or a page subtree) since a given timestamp. Efficiently filters by `updatedAt` before fetching comments. + ### Technical Details - **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. diff --git a/package-lock.json b/package-lock.json index 26f367b..e864360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@tiptap/core": "^3.18.0", "@tiptap/extension-image": "^3.18.0", "@tiptap/extension-link": "^3.18.0", + "@tiptap/extension-table": "^3.18.0", "@tiptap/html": "^3.18.0", "@tiptap/starter-kit": "^3.18.0", "@types/jsdom": "^27.0.0", @@ -315,16 +316,10 @@ } } }, - "node_modules/@remirror/core-constants": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", - "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", - "license": "MIT" - }, "node_modules/@tiptap/core": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.18.0.tgz", - "integrity": "sha512-Gczd4GbK1DNgy/QUPElMVozoa0GW9mW8E31VIi7Q4a9PHHz8PcrxPmuWwtJ2q0PF8MWpOSLuBXoQTWaXZRPRnQ==", + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.24.0.tgz", + "integrity": "sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==", "license": "MIT", "peer": true, "funding": { @@ -332,7 +327,7 @@ "url": "https://github.com/sponsors/ueberdosis" }, "peerDependencies": { - "@tiptap/pm": "^3.18.0" + "@tiptap/pm": "3.24.0" } }, "node_modules/@tiptap/extension-blockquote": { @@ -603,6 +598,20 @@ "@tiptap/core": "^3.18.0" } }, + "node_modules/@tiptap/extension-table": { + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/extension-table/-/extension-table-3.24.0.tgz", + "integrity": "sha512-lr5elob3uJnB+ltgqPDEeVQmIPRx6JoS0I6z93tOgKsI2mIsaw5ErghteeiCTpExdyax7aWR0fn5pZzLVDQL8A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.24.0", + "@tiptap/pm": "3.24.0" + } + }, "node_modules/@tiptap/extension-text": { "version": "3.18.0", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.18.0.tgz", @@ -660,28 +669,23 @@ } }, "node_modules/@tiptap/pm": { - "version": "3.18.0", - "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.18.0.tgz", - "integrity": "sha512-8RoI5gW0xBVCsuxahpK8vx7onAw6k2/uR3hbGBBnH+HocDMaAZKot3nTyY546ij8ospIC1mnQ7k4BhVUZesZDQ==", + "version": "3.24.0", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.24.0.tgz", + "integrity": "sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==", "license": "MIT", "peer": true, "dependencies": { "prosemirror-changeset": "^2.3.0", - "prosemirror-collab": "^1.3.1", "prosemirror-commands": "^1.6.2", "prosemirror-dropcursor": "^1.8.1", "prosemirror-gapcursor": "^1.3.2", "prosemirror-history": "^1.4.1", "prosemirror-inputrules": "^1.4.0", "prosemirror-keymap": "^1.2.2", - "prosemirror-markdown": "^1.13.1", - "prosemirror-menu": "^1.2.4", "prosemirror-model": "^1.24.1", - "prosemirror-schema-basic": "^1.2.3", "prosemirror-schema-list": "^1.5.0", "prosemirror-state": "^1.4.3", "prosemirror-tables": "^1.6.4", - "prosemirror-trailing-node": "^3.0.0", "prosemirror-transform": "^1.10.2", "prosemirror-view": "^1.38.1" }, @@ -748,28 +752,6 @@ "parse5": "^7.0.0" } }, - "node_modules/@types/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", - "license": "MIT" - }, - "node_modules/@types/markdown-it": { - "version": "14.1.2", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", - "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", - "license": "MIT", - "dependencies": { - "@types/linkify-it": "^5", - "@types/mdurl": "^2" - } - }, - "node_modules/@types/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.19.30", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", @@ -855,12 +837,6 @@ } } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -1018,12 +994,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/crelt": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", - "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", - "license": "MIT" - }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -1221,18 +1191,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -1796,15 +1754,6 @@ "url": "https://github.com/sponsors/dmonad" } }, - "node_modules/linkify-it": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", - "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", - "license": "MIT", - "dependencies": { - "uc.micro": "^2.0.0" - } - }, "node_modules/linkifyjs": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", @@ -1820,23 +1769,6 @@ "node": "20 || >=22" } }, - "node_modules/markdown-it": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", - "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.0", - "mdurl": "^2.0.0", - "punycode.js": "^2.3.1", - "uc.micro": "^2.1.0" - }, - "bin": { - "markdown-it": "bin/markdown-it.mjs" - } - }, "node_modules/marked": { "version": "17.0.1", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", @@ -1864,12 +1796,6 @@ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "license": "CC0-1.0" }, - "node_modules/mdurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", - "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", - "license": "MIT" - }, "node_modules/media-typer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", @@ -2049,15 +1975,6 @@ "prosemirror-transform": "^1.0.0" } }, - "node_modules/prosemirror-collab": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", - "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", - "license": "MIT", - "dependencies": { - "prosemirror-state": "^1.0.0" - } - }, "node_modules/prosemirror-commands": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", @@ -2124,29 +2041,6 @@ "w3c-keyname": "^2.2.0" } }, - "node_modules/prosemirror-markdown": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.3.tgz", - "integrity": "sha512-3E+Et6cdXIH0EgN2tGYQ+EBT7N4kMiZFsW+hzx+aPtOmADDHWCdd2uUQb7yklJrfUYUOjEEu22BiN6UFgPe4cQ==", - "license": "MIT", - "dependencies": { - "@types/markdown-it": "^14.0.0", - "markdown-it": "^14.0.0", - "prosemirror-model": "^1.25.0" - } - }, - "node_modules/prosemirror-menu": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", - "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", - "license": "MIT", - "dependencies": { - "crelt": "^1.0.0", - "prosemirror-commands": "^1.0.0", - "prosemirror-history": "^1.0.0", - "prosemirror-state": "^1.0.0" - } - }, "node_modules/prosemirror-model": { "version": "1.25.4", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", @@ -2157,15 +2051,6 @@ "orderedmap": "^2.0.0" } }, - "node_modules/prosemirror-schema-basic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", - "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", - "license": "MIT", - "dependencies": { - "prosemirror-model": "^1.25.0" - } - }, "node_modules/prosemirror-schema-list": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", @@ -2202,21 +2087,6 @@ "prosemirror-view": "^1.41.4" } }, - "node_modules/prosemirror-trailing-node": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", - "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", - "license": "MIT", - "dependencies": { - "@remirror/core-constants": "3.0.0", - "escape-string-regexp": "^4.0.0" - }, - "peerDependencies": { - "prosemirror-model": "^1.22.1", - "prosemirror-state": "^1.4.2", - "prosemirror-view": "^1.33.8" - } - }, "node_modules/prosemirror-transform": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", @@ -2266,15 +2136,6 @@ "node": ">=6" } }, - "node_modules/punycode.js": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", - "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -2610,12 +2471,6 @@ "node": ">=14.17" } }, - "node_modules/uc.micro": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", - "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", - "license": "MIT" - }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/src/index.ts b/src/index.ts index 7063329..c75f464 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,9 +8,11 @@ import { filterSpace, filterGroup, filterPage, + filterComment, filterSearchResult, } from "./lib/filters.js"; import { convertProseMirrorToMarkdown } from "./lib/markdown-converter.js"; +import { markdownToTiptapJson } from "./lib/markdown-to-json.js"; import { readFileSync } from "fs"; import { fileURLToPath } from "url"; import { dirname, join } from "path"; @@ -331,6 +333,172 @@ class DocmostClient { ); return Promise.all(promises); } + + // --- Comment Methods --- + + async listComments(pageId: string) { + await this.ensureAuthenticated(); + let allComments: any[] = []; + let cursor: string | null = null; + + do { + const payload: Record = { pageId, limit: 100 }; + if (cursor) payload.cursor = cursor; + + const response = await this.client.post("/comments", payload); + const data = response.data.data || response.data; + const items = data.items || []; + allComments = allComments.concat(items); + cursor = data.meta?.nextCursor || null; + } while (cursor); + + return allComments.map((comment: any) => { + const markdown = comment.content + ? convertProseMirrorToMarkdown(comment.content) + : ""; + return filterComment(comment, markdown); + }); + } + + async getComment(commentId: string) { + await this.ensureAuthenticated(); + const response = await this.client.post("/comments/info", { commentId }); + const comment = response.data.data || response.data; + const markdown = comment.content + ? convertProseMirrorToMarkdown(comment.content) + : ""; + return { + data: filterComment(comment, markdown), + success: true, + }; + } + + async createComment( + pageId: string, + content: string, + type: "page" | "inline" = "page", + selection?: string, + parentCommentId?: string, + ) { + await this.ensureAuthenticated(); + const jsonContent = await markdownToTiptapJson(content); + const payload: Record = { + pageId, + content: JSON.stringify(jsonContent), + type, + }; + if (selection) payload.selection = selection; + if (parentCommentId) payload.parentCommentId = parentCommentId; + + const response = await this.client.post("/comments/create", payload); + const comment = response.data.data || response.data; + const markdown = comment.content + ? convertProseMirrorToMarkdown(comment.content) + : content; + return { + data: filterComment(comment, markdown), + success: true, + }; + } + + async updateComment(commentId: string, content: string) { + await this.ensureAuthenticated(); + const jsonContent = await markdownToTiptapJson(content); + await this.client.post("/comments/update", { + commentId, + content: JSON.stringify(jsonContent), + }); + return { + success: true, + commentId, + message: "Comment updated successfully.", + }; + } + + async deleteComment(commentId: string) { + await this.ensureAuthenticated(); + return this.client + .post("/comments/delete", { commentId }) + .then((res) => res.data); + } + + /** + * Check for new comments across pages in a space (optionally scoped to a subtree). + * 1. Fetch all pages in space, filter by updatedAt > since + * 2. If parentPageId given, keep only descendants of that page + * 3. For matching pages, fetch comments and filter by createdAt > since + */ + async checkNewComments(spaceId: string, since: string, parentPageId?: string) { + await this.ensureAuthenticated(); + + const sinceDate = new Date(since); + + // 1. Get all pages in the space + const allPages = await this.paginateAll("/pages/recent", { spaceId }); + + // 2. If parentPageId specified, build set of descendant page IDs + let allowedPageIds: Set | null = null; + if (parentPageId) { + allowedPageIds = new Set(); + const pageMap = new Map(); + for (const page of allPages) { + const pid = page.parentPageId || "__root__"; + if (!pageMap.has(pid)) pageMap.set(pid, []); + pageMap.get(pid)!.push(page); + } + const queue = [parentPageId]; + allowedPageIds.add(parentPageId); + while (queue.length > 0) { + const current = queue.shift()!; + const children = pageMap.get(current) || []; + for (const child of children) { + allowedPageIds.add(child.id); + queue.push(child.id); + } + } + } + + // 3. Filter pages by updatedAt > since and optional subtree + const recentlyUpdated = allPages.filter((page: any) => { + if (new Date(page.updatedAt) <= sinceDate) return false; + if (allowedPageIds && !allowedPageIds.has(page.id)) return false; + return true; + }); + + // 4. Fetch comments for each updated page and filter by createdAt > since + const results: any[] = []; + for (const page of recentlyUpdated) { + try { + const comments = await this.listComments(page.id); + const newComments = comments.filter( + (c: any) => new Date(c.createdAt) > sinceDate, + ); + if (newComments.length > 0) { + results.push({ + pageId: page.id, + pageTitle: page.title, + comments: newComments, + }); + } + } catch (e: any) { + // Skip pages with errors (e.g. deleted between calls) + } + } + + const totalNewComments = results.reduce( + (sum, r) => sum + r.comments.length, + 0, + ); + + return { + since, + scope: parentPageId ? `subtree of ${parentPageId}` : `space ${spaceId}`, + checkedPages: recentlyUpdated.length, + pagesWithNewComments: results.length, + totalNewComments, + comments: results, + }; + } } const docmostClient = new DocmostClient(API_URL); @@ -547,6 +715,148 @@ server.registerTool( }, ); +// --- Comment Tools --- + +// Tool: list_comments +server.registerTool( + "list_comments", + { + description: + "List all comments on a page. Returns comments with content converted to Markdown.", + inputSchema: { + pageId: z.string().describe("ID of the page to list comments for"), + }, + }, + async ({ pageId }) => { + const comments = await docmostClient.listComments(pageId); + return jsonContent(comments); + }, +); + +// Tool: get_comment +server.registerTool( + "get_comment", + { + description: "Get a single comment by ID with content as Markdown.", + inputSchema: { + commentId: z.string().describe("ID of the comment"), + }, + }, + async ({ commentId }) => { + const comment = await docmostClient.getComment(commentId); + return jsonContent(comment); + }, +); + +// Tool: create_comment +server.registerTool( + "create_comment", + { + description: + "Create a new comment on a page. Content is provided as Markdown and automatically converted to the required format.", + inputSchema: { + pageId: z.string().describe("ID of the page to comment on"), + content: z.string().describe("Comment content in Markdown format"), + type: z + .enum(["page", "inline"]) + .optional() + .describe( + "Comment type: 'page' for general page comment (default), 'inline' for text selection comment", + ), + selection: z + .string() + .optional() + .describe( + "Selected text for inline comments (max 250 chars). Required when type is 'inline'.", + ), + parentCommentId: z + .string() + .optional() + .describe("Parent comment ID to create a reply (max 2 nesting levels)"), + }, + }, + async ({ pageId, content, type, selection, parentCommentId }) => { + const result = await docmostClient.createComment( + pageId, + content, + type || "page", + selection, + parentCommentId, + ); + return jsonContent(result); + }, +); + +// Tool: update_comment +server.registerTool( + "update_comment", + { + description: + "Update an existing comment's content. Only the comment creator can update it. Content is provided as Markdown.", + inputSchema: { + commentId: z.string().describe("ID of the comment to update"), + content: z.string().describe("New comment content in Markdown format"), + }, + }, + async ({ commentId, content }) => { + const result = await docmostClient.updateComment(commentId, content); + return jsonContent(result); + }, +); + +// Tool: delete_comment +server.registerTool( + "delete_comment", + { + description: + "Delete a comment. Only the comment creator or space admin can delete it.", + inputSchema: { + commentId: z.string().describe("ID of the comment to delete"), + }, + }, + async ({ commentId }) => { + await docmostClient.deleteComment(commentId); + return { + content: [ + { type: "text", text: `Successfully deleted comment ${commentId}` }, + ], + }; + }, +); + +// Tool: check_new_comments +server.registerTool( + "check_new_comments", + { + description: + "Check for new comments across pages in a space since a given timestamp. " + + "Optionally scope to a page subtree (folder). Returns only comments created after the specified time.", + inputSchema: { + spaceId: z.string().describe("Space ID to check for new comments"), + since: z + .string() + .describe( + "ISO 8601 timestamp — only return comments created after this time (e.g. '2026-03-10T00:00:00Z')", + ), + parentPageId: z + .string() + .optional() + .describe( + "Optional root page ID to scope the check to a subtree (folder). " + + "Only pages under this parent will be checked.", + ), + }, + }, + async ({ spaceId, since, parentPageId }) => { + const result = await docmostClient.checkNewComments( + spaceId, + since, + parentPageId, + ); + return jsonContent(result); + }, +); + async function run() { const transport = new StdioServerTransport(); await server.connect(transport);