prompt(mcp): fix inaccurate and misleading tool descriptions

Audit of all 41 tool descriptions against the actual implementation found
factually wrong or misleading texts:

- list_comments claimed '(paginated)' — it takes only pageId and returns ALL
  comments in one call (internal pagination); now also states that RESOLVED
  threads are included and how to filter them. In-app twin synced.
- search claimed the limit default is 'applied by the client' — the client
  deliberately omits it so the SERVER applies its default.
- create_page's '(automatically moves it to the correct hierarchy)' said
  nothing useful — now documents parentPageId nesting semantics; move_page
  drops the stale 'essential for organizing pages created via create_page'.
- share_page now warns the page becomes accessible to ANYONE with the URL.
- get_page (both transports) now explains inline <span data-comment-id> tags
  are comment anchors (incl. resolved) — markup, not page text.
- patch_node/delete_node/insert_node pointed only at the expensive page-JSON
  view for block ids — now route through the cheap page outline first.
- docmost_transform marks 'Примечания переводчика' as the DEFAULT
  notesHeading, overridable for non-Russian pages.

Checks: @docmost/mcp tests 450/450 (incl. the server-instructions guard);
server ai-chat-tools spec 20/20; mcp build/ artifacts rebuilt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 07:00:16 +03:00
parent 1fda0ec8b0
commit 351615e5bc
5 changed files with 52 additions and 26 deletions
@@ -303,7 +303,9 @@ export class AiChatToolsService {
getPage: tool({
description:
'Fetch a single page as Markdown by its page id. Returns the page ' +
'title and its Markdown content.',
'title and its Markdown content. Inline <span data-comment-id> tags ' +
'in the markdown are comment highlight anchors (also present for ' +
'RESOLVED threads) — treat them as markup, not page text.',
inputSchema: modelFriendlyInput({
pageId: z.string().describe('The id (or slugId) of the page.'),
}),
@@ -647,7 +649,9 @@ export class AiChatToolsService {
listComments: tool({
description:
'List all comments on a page (content as Markdown).',
'List ALL comments on a page in one call, including RESOLVED ' +
'threads — filter by resolvedAt when you need only open ones. ' +
'Content is returned as Markdown.',
inputSchema: modelFriendlyInput({
pageId: z.string().describe('The id of the page.'),
}),
+17 -8
View File
@@ -116,7 +116,9 @@ export function createDocmostMcpServer(config) {
server.registerTool("get_page", {
description: "Get page details with content converted to Markdown. The conversion is " +
"LOSSY (block ids, exact table/callout structure are approximated); for a " +
"lossless representation use get_page_json.",
"lossless representation use get_page_json. Inline <span data-comment-id> " +
"tags in the markdown are comment highlight anchors (also present for " +
"RESOLVED threads) — treat them as markup, not page text.",
inputSchema: {
pageId: z.string().min(1),
},
@@ -213,7 +215,8 @@ export function createDocmostMcpServer(config) {
});
// Tool: create_page
server.registerTool("create_page", {
description: "Create a new page with content (automatically moves it to the correct hierarchy).",
description: "Create a new page from Markdown in a space. Pass parentPageId to nest " +
"it under a parent; omit it to create at the space root.",
inputSchema: {
title: z.string().min(1).describe("Title of the page"),
content: z.string().min(1).describe("Markdown content"),
@@ -435,7 +438,8 @@ export function createDocmostMcpServer(config) {
// agent; this transport keeps the plain public-URL wording.
server.registerTool("share_page", {
description: "Make a page publicly accessible (idempotent) and return its public " +
"URL. The URL format is <app>/share/<key>/p/<slugId>.",
"URL. The URL format is <app>/share/<key>/p/<slugId>. This exposes the " +
"page content to ANYONE with the URL — do it only when explicitly asked.",
inputSchema: {
pageId: z.string().min(1).describe("ID of the page to share"),
searchIndexing: z
@@ -459,7 +463,7 @@ export function createDocmostMcpServer(config) {
});
// Tool: move_page
server.registerTool("move_page", {
description: "Move a page to a new parent (nesting) or root. Essential for organizing pages created via 'create_page'.",
description: "Move a page under a new parent (nesting) or to the space root.",
inputSchema: {
pageId: z.string().min(1),
parentPageId: z
@@ -511,7 +515,9 @@ export function createDocmostMcpServer(config) {
// --- Comment tools (ported from upstream PR #3 by Max Nikitin) ---
// Tool: list_comments
server.registerTool("list_comments", {
description: "List all comments on a page (paginated). Content is returned as Markdown.",
description: "List ALL comments on a page in one call (pagination is handled " +
"internally), including RESOLVED threads — filter by resolvedAt when you " +
"need only open ones. Content is returned as Markdown.",
inputSchema: {
pageId: z.string().describe("ID of the page"),
},
@@ -662,8 +668,9 @@ export function createDocmostMcpServer(config) {
// different schema (limit 1-20); this transport is a plain REST full-text search
// (limit up to 100). Different behaviour AND schema, so kept per-layer.
server.registerTool("search", {
description: "Search for pages and content. Results are bounded by `limit` " +
"(default applied by the client, max 100).",
description: "Full-text search for pages and content across the whole workspace. " +
"Results are bounded by `limit` (1-100; when omitted the server applies " +
"its own default).",
inputSchema: {
query: z.string().min(1).describe("Search query"),
limit: z
@@ -713,7 +720,9 @@ export function createDocmostMcpServer(config) {
"insertInlineFootnote(doc, {anchorText, text}) (author-inline footnote: " +
"marker + dedup'd definition, list derived). Footnote convention: markers are " +
"plain '[N]' text in the body; the notes are an orderedList under a " +
"heading whose text is 'Примечания переводчика'. The transform runs " +
"heading whose text is 'Примечания переводчика' (that is only the DEFAULT " +
"notesHeading — pass the notesHeading option to the helpers to use a " +
"heading matching the page's language). The transform runs " +
"sandboxed (no require/process/fs/network, 5s timeout) and must return a " +
"{type:'doc'} node.",
inputSchema: {
+6 -4
View File
@@ -78,8 +78,8 @@ export const SHARED_TOOL_SPECS = {
deleteNode: {
mcpName: 'delete_node',
inAppKey: 'deleteNode',
description: 'Remove a single block by its attrs.id (from the page-JSON view) WITHOUT ' +
'resending the whole document.',
description: 'Remove a single block by its attrs.id (from the page outline or ' +
'page-JSON view) WITHOUT resending the whole document.',
buildShape: (z) => ({
pageId: z.string().min(1),
nodeId: z.string().min(1),
@@ -99,7 +99,8 @@ export const SHARED_TOOL_SPECS = {
inAppKey: 'patchNode',
description: 'Replace a single content block identified by its attrs.id with a new ' +
'ProseMirror node, WITHOUT resending the whole document; the replacement ' +
'keeps the same node id. Get the block id from the page-JSON view, then ' +
'keeps the same node id. Get the block id from the page outline (cheap) ' +
'or the page-JSON view, then ' +
'pass a ProseMirror node to put in its place. Example node: a paragraph ' +
'{"type":"paragraph","content":[{"type":"text","text":"Hello"}]} or a ' +
'heading {"type":"heading","attrs":{"level":2},"content":' +
@@ -128,7 +129,8 @@ export const SHARED_TOOL_SPECS = {
description: 'Insert a block before/after another block (by attrs.id or anchor text) ' +
'or append it at the end (top level). For before/after you MUST provide ' +
'EXACTLY ONE of anchorNodeId or anchorText. Get anchor block ids from the ' +
'page-JSON view. Avoids resending the whole document. Can also insert ' +
'page outline or the page-JSON view. Avoids resending the whole document. ' +
'Can also insert ' +
'table structure: to add a tableRow, pass a tableRow node with position ' +
'before/after and anchor INSIDE the target table — anchorNodeId of any ' +
'block/cell in it, or anchorText matching the table; to add a ' +
+17 -8
View File
@@ -156,7 +156,9 @@ server.registerTool(
description:
"Get page details with content converted to Markdown. The conversion is " +
"LOSSY (block ids, exact table/callout structure are approximated); for a " +
"lossless representation use get_page_json.",
"lossless representation use get_page_json. Inline <span data-comment-id> " +
"tags in the markdown are comment highlight anchors (also present for " +
"RESOLVED threads) — treat them as markup, not page text.",
inputSchema: {
pageId: z.string().min(1),
},
@@ -297,7 +299,8 @@ server.registerTool(
"create_page",
{
description:
"Create a new page with content (automatically moves it to the correct hierarchy).",
"Create a new page from Markdown in a space. Pass parentPageId to nest " +
"it under a parent; omit it to create at the space root.",
inputSchema: {
title: z.string().min(1).describe("Title of the page"),
content: z.string().min(1).describe("Markdown content"),
@@ -596,7 +599,8 @@ server.registerTool(
{
description:
"Make a page publicly accessible (idempotent) and return its public " +
"URL. The URL format is <app>/share/<key>/p/<slugId>.",
"URL. The URL format is <app>/share/<key>/p/<slugId>. This exposes the " +
"page content to ANYONE with the URL — do it only when explicitly asked.",
inputSchema: {
pageId: z.string().min(1).describe("ID of the page to share"),
searchIndexing: z
@@ -628,7 +632,7 @@ server.registerTool(
"move_page",
{
description:
"Move a page to a new parent (nesting) or root. Essential for organizing pages created via 'create_page'.",
"Move a page under a new parent (nesting) or to the space root.",
inputSchema: {
pageId: z.string().min(1),
parentPageId: z
@@ -708,7 +712,9 @@ server.registerTool(
"list_comments",
{
description:
"List all comments on a page (paginated). Content is returned as Markdown.",
"List ALL comments on a page in one call (pagination is handled " +
"internally), including RESOLVED threads — filter by resolvedAt when you " +
"need only open ones. Content is returned as Markdown.",
inputSchema: {
pageId: z.string().describe("ID of the page"),
},
@@ -924,8 +930,9 @@ server.registerTool(
"search",
{
description:
"Search for pages and content. Results are bounded by `limit` " +
"(default applied by the client, max 100).",
"Full-text search for pages and content across the whole workspace. " +
"Results are bounded by `limit` (1-100; when omitted the server applies " +
"its own default).",
inputSchema: {
query: z.string().min(1).describe("Search query"),
limit: z
@@ -981,7 +988,9 @@ server.registerTool(
"insertInlineFootnote(doc, {anchorText, text}) (author-inline footnote: " +
"marker + dedup'd definition, list derived). Footnote convention: markers are " +
"plain '[N]' text in the body; the notes are an orderedList under a " +
"heading whose text is 'Примечания переводчика'. The transform runs " +
"heading whose text is 'Примечания переводчика' (that is only the DEFAULT " +
"notesHeading — pass the notesHeading option to the helpers to use a " +
"heading matching the page's language). The transform runs " +
"sandboxed (no require/process/fs/network, 5s timeout) and must return a " +
"{type:'doc'} node.",
inputSchema: {
+6 -4
View File
@@ -116,8 +116,8 @@ export const SHARED_TOOL_SPECS = {
mcpName: 'delete_node',
inAppKey: 'deleteNode',
description:
'Remove a single block by its attrs.id (from the page-JSON view) WITHOUT ' +
'resending the whole document.',
'Remove a single block by its attrs.id (from the page outline or ' +
'page-JSON view) WITHOUT resending the whole document.',
buildShape: (z) => ({
pageId: z.string().min(1),
nodeId: z.string().min(1),
@@ -139,7 +139,8 @@ export const SHARED_TOOL_SPECS = {
description:
'Replace a single content block identified by its attrs.id with a new ' +
'ProseMirror node, WITHOUT resending the whole document; the replacement ' +
'keeps the same node id. Get the block id from the page-JSON view, then ' +
'keeps the same node id. Get the block id from the page outline (cheap) ' +
'or the page-JSON view, then ' +
'pass a ProseMirror node to put in its place. Example node: a paragraph ' +
'{"type":"paragraph","content":[{"type":"text","text":"Hello"}]} or a ' +
'heading {"type":"heading","attrs":{"level":2},"content":' +
@@ -174,7 +175,8 @@ export const SHARED_TOOL_SPECS = {
'Insert a block before/after another block (by attrs.id or anchor text) ' +
'or append it at the end (top level). For before/after you MUST provide ' +
'EXACTLY ONE of anchorNodeId or anchorText. Get anchor block ids from the ' +
'page-JSON view. Avoids resending the whole document. Can also insert ' +
'page outline or the page-JSON view. Avoids resending the whole document. ' +
'Can also insert ' +
'table structure: to add a tableRow, pass a tableRow node with position ' +
'before/after and anchor INSIDE the target table — anchorNodeId of any ' +
'block/cell in it, or anchorText matching the table; to add a ' +