Compare commits

..

2 Commits

Author SHA1 Message Date
a8b40641ac feat: add comment support (list, get, create, update, delete, check_new) 2026-05-30 20:46:35 -06:00
c520713291 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 <noreply@anthropic.com>
2026-05-31 02:45:43 +00:00
3 changed files with 341 additions and 167 deletions

View File

@@ -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). - **`list_pages`**: List pages within a space (ordered by `updatedAt` descending).
- **`get_page`**: Retrieve full content and metadata of a specific page. - **`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 ### 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. - **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.

189
package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@tiptap/core": "^3.18.0", "@tiptap/core": "^3.18.0",
"@tiptap/extension-image": "^3.18.0", "@tiptap/extension-image": "^3.18.0",
"@tiptap/extension-link": "^3.18.0", "@tiptap/extension-link": "^3.18.0",
"@tiptap/extension-table": "^3.18.0",
"@tiptap/html": "^3.18.0", "@tiptap/html": "^3.18.0",
"@tiptap/starter-kit": "^3.18.0", "@tiptap/starter-kit": "^3.18.0",
"@types/jsdom": "^27.0.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": { "node_modules/@tiptap/core": {
"version": "3.18.0", "version": "3.24.0",
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.18.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.24.0.tgz",
"integrity": "sha512-Gczd4GbK1DNgy/QUPElMVozoa0GW9mW8E31VIi7Q4a9PHHz8PcrxPmuWwtJ2q0PF8MWpOSLuBXoQTWaXZRPRnQ==", "integrity": "sha512-GTAsXAI32p4hEZgPzvUv2RPrObxamy9AFhmhG10fXSvN/cDUs8naEYVIqDV3Sh99jMwQEbTFKW1E1mcspsY6ow==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"funding": { "funding": {
@@ -332,7 +327,7 @@
"url": "https://github.com/sponsors/ueberdosis" "url": "https://github.com/sponsors/ueberdosis"
}, },
"peerDependencies": { "peerDependencies": {
"@tiptap/pm": "^3.18.0" "@tiptap/pm": "3.24.0"
} }
}, },
"node_modules/@tiptap/extension-blockquote": { "node_modules/@tiptap/extension-blockquote": {
@@ -603,6 +598,20 @@
"@tiptap/core": "^3.18.0" "@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": { "node_modules/@tiptap/extension-text": {
"version": "3.18.0", "version": "3.18.0",
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.18.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.18.0.tgz",
@@ -660,28 +669,23 @@
} }
}, },
"node_modules/@tiptap/pm": { "node_modules/@tiptap/pm": {
"version": "3.18.0", "version": "3.24.0",
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.18.0.tgz", "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.24.0.tgz",
"integrity": "sha512-8RoI5gW0xBVCsuxahpK8vx7onAw6k2/uR3hbGBBnH+HocDMaAZKot3nTyY546ij8ospIC1mnQ7k4BhVUZesZDQ==", "integrity": "sha512-QQP/78ryOZDN99gNBV7dgh69/8AYaOYQYFklq/iR+ZRFaaL3+qqHFvPVJapGkzPdymBgNJ34xjFM8n5pJ4QmMg==",
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"prosemirror-changeset": "^2.3.0", "prosemirror-changeset": "^2.3.0",
"prosemirror-collab": "^1.3.1",
"prosemirror-commands": "^1.6.2", "prosemirror-commands": "^1.6.2",
"prosemirror-dropcursor": "^1.8.1", "prosemirror-dropcursor": "^1.8.1",
"prosemirror-gapcursor": "^1.3.2", "prosemirror-gapcursor": "^1.3.2",
"prosemirror-history": "^1.4.1", "prosemirror-history": "^1.4.1",
"prosemirror-inputrules": "^1.4.0", "prosemirror-inputrules": "^1.4.0",
"prosemirror-keymap": "^1.2.2", "prosemirror-keymap": "^1.2.2",
"prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4",
"prosemirror-model": "^1.24.1", "prosemirror-model": "^1.24.1",
"prosemirror-schema-basic": "^1.2.3",
"prosemirror-schema-list": "^1.5.0", "prosemirror-schema-list": "^1.5.0",
"prosemirror-state": "^1.4.3", "prosemirror-state": "^1.4.3",
"prosemirror-tables": "^1.6.4", "prosemirror-tables": "^1.6.4",
"prosemirror-trailing-node": "^3.0.0",
"prosemirror-transform": "^1.10.2", "prosemirror-transform": "^1.10.2",
"prosemirror-view": "^1.38.1" "prosemirror-view": "^1.38.1"
}, },
@@ -748,28 +752,6 @@
"parse5": "^7.0.0" "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": { "node_modules/@types/node": {
"version": "20.19.30", "version": "20.19.30",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", "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": { "node_modules/asynckit": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -1018,12 +994,6 @@
"url": "https://opencollective.com/express" "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": { "node_modules/cross-spawn": {
"version": "7.0.6", "version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -1221,18 +1191,6 @@
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT" "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": { "node_modules/etag": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
@@ -1796,15 +1754,6 @@
"url": "https://github.com/sponsors/dmonad" "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": { "node_modules/linkifyjs": {
"version": "4.3.2", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz",
@@ -1820,23 +1769,6 @@
"node": "20 || >=22" "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": { "node_modules/marked": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz",
@@ -1864,12 +1796,6 @@
"integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
"license": "CC0-1.0" "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": { "node_modules/media-typer": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
@@ -2049,15 +1975,6 @@
"prosemirror-transform": "^1.0.0" "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": { "node_modules/prosemirror-commands": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
@@ -2124,29 +2041,6 @@
"w3c-keyname": "^2.2.0" "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": { "node_modules/prosemirror-model": {
"version": "1.25.4", "version": "1.25.4",
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
@@ -2157,15 +2051,6 @@
"orderedmap": "^2.0.0" "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": { "node_modules/prosemirror-schema-list": {
"version": "1.5.1", "version": "1.5.1",
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
@@ -2202,21 +2087,6 @@
"prosemirror-view": "^1.41.4" "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": { "node_modules/prosemirror-transform": {
"version": "1.11.0", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz",
@@ -2266,15 +2136,6 @@
"node": ">=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": { "node_modules/qs": {
"version": "6.14.1", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
@@ -2610,12 +2471,6 @@
"node": ">=14.17" "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": { "node_modules/undici-types": {
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",

View File

@@ -8,9 +8,11 @@ import {
filterSpace, filterSpace,
filterGroup, filterGroup,
filterPage, filterPage,
filterComment,
filterSearchResult, filterSearchResult,
} from "./lib/filters.js"; } from "./lib/filters.js";
import { convertProseMirrorToMarkdown } from "./lib/markdown-converter.js"; import { convertProseMirrorToMarkdown } from "./lib/markdown-converter.js";
import { markdownToTiptapJson } from "./lib/markdown-to-json.js";
import { readFileSync } from "fs"; import { readFileSync } from "fs";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { dirname, join } from "path"; import { dirname, join } from "path";
@@ -331,6 +333,172 @@ class DocmostClient {
); );
return Promise.all(promises); 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<string, any> = { 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<string, any> = {
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<any>("/pages/recent", { spaceId });
// 2. If parentPageId specified, build set of descendant page IDs
let allowedPageIds: Set<string> | null = null;
if (parentPageId) {
allowedPageIds = new Set<string>();
const pageMap = new Map<string, any[]>();
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); 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() { async function run() {
const transport = new StdioServerTransport(); const transport = new StdioServerTransport();
await server.connect(transport); await server.connect(transport);