add_comment
Insert a comment or threaded reply into a Word document by specifying the paragraph and anchor text for a root comment, or a parent comment ID for a reply.
Instructions
Add a comment or threaded reply to a document. Provide target_paragraph_id + anchor_text for root comments, or parent_comment_id for replies.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| file_path | Yes | Path to the DOCX file. | |
| target_paragraph_id | No | Paragraph ID to anchor the comment to (for root comments). | |
| anchor_text | No | Text within the paragraph to anchor the comment to. If omitted, anchors to entire paragraph. | |
| parent_comment_id | No | Parent comment ID for threaded replies. | |
| author | Yes | Comment author name. | |
| text | Yes | Comment body text. | |
| initials | No | Author initials (defaults to first letter of author name). |
Implementation Reference
- MCP tool handler for add_comment. Accepts params: file_path, target_paragraph_id, anchor_text, parent_comment_id, author, text, initials. Supports two modes: (1) root comment anchored to a paragraph text range, (2) threaded reply using parent_comment_id. Resolves the session, finds the paragraph by ID, locates anchor_text range or uses full paragraph, then delegates to session.doc.addComment() or session.doc.addCommentReply().
export async function addComment( manager: SessionManager, params: { file_path?: string; target_paragraph_id?: string; anchor_text?: string; parent_comment_id?: number; author: string; text: string; initials?: string; }, ): Promise<ToolResponse> { const resolved = await resolveSessionForTool(manager, params, { toolName: 'add_comment' }); if (!resolved.ok) return resolved.response; const { session, metadata } = resolved; try { // Reply mode: parent_comment_id provided if (params.parent_comment_id != null) { const result = await session.doc.addCommentReply({ parentCommentId: params.parent_comment_id, author: params.author, text: params.text, initials: params.initials, }); manager.markEdited(session); return ok(mergeSessionResolutionMetadata({ comment_id: result.commentId, parent_comment_id: result.parentCommentId, mode: 'reply', file_path: manager.normalizePath(session.originalPath), }, metadata)); } // Root comment mode: target_paragraph_id required if (!params.target_paragraph_id) { return err( 'MISSING_PARAMETER', 'Either target_paragraph_id (for root comments) or parent_comment_id (for replies) is required.', 'Provide target_paragraph_id + optional anchor_text for root comments, or parent_comment_id for threaded replies.', ); } const pid = params.target_paragraph_id; const pEl = session.doc.getParagraphElementById(pid); if (!pEl) { return err( 'ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`, 'Use grep or read_file to find valid paragraph IDs.', ); } let start = 0; let end: number; if (params.anchor_text) { // Find anchor_text within the paragraph const paraText = session.doc.getParagraphTextById(pid) ?? ''; const match = findUniqueSubstringMatch(paraText, params.anchor_text); if (match.status === 'not_found') { return err( 'TEXT_NOT_FOUND', `anchor_text '${params.anchor_text}' not found in paragraph ${pid}`, 'Verify anchor_text is present in the target paragraph.', ); } if (match.status === 'multiple') { return err( 'MULTIPLE_MATCHES', `Found ${match.matchCount} matches for anchor_text in paragraph ${pid}`, 'Provide more specific anchor_text for a unique match.', ); } start = match.start; end = match.end; } else { // Anchor to entire paragraph const paraText = session.doc.getParagraphTextById(pid) ?? ''; end = paraText.length; } const result = await session.doc.addComment({ paragraphId: pid, start, end, author: params.author, text: params.text, initials: params.initials, }); manager.markEdited(session); return ok(mergeSessionResolutionMetadata({ comment_id: result.commentId, anchor_paragraph_id: pid, anchor_text: params.anchor_text ?? null, mode: 'root', file_path: manager.normalizePath(session.originalPath), }, metadata)); } catch (e: unknown) { return err('COMMENT_ERROR', errorMessage(e)); } } - Tool catalog registration with Zod input schema for add_comment. Defines fields: file_path (optional), target_paragraph_id (optional), anchor_text (optional), parent_comment_id (optional), author (required), text (required), initials (optional). Marked as destructiveHint: true.
{ name: 'add_comment', description: 'Add a comment or threaded reply to a document. Provide target_paragraph_id + anchor_text for root comments, or parent_comment_id for replies.', input: z.object({ ...FILE_FIELD, target_paragraph_id: z.string().optional().describe('Paragraph ID to anchor the comment to (for root comments).'), anchor_text: z.string().optional().describe('Text within the paragraph to anchor the comment to. If omitted, anchors to entire paragraph.'), parent_comment_id: z.number().optional().describe('Parent comment ID for threaded replies.'), author: z.string().describe('Comment author name.'), text: z.string().describe('Comment body text.'), initials: z.string().optional().describe('Author initials (defaults to first letter of author name).'), }), annotations: { readOnlyHint: false, destructiveHint: true }, }, - packages/docx-mcp/src/server.ts:20-20 (registration)Import of addComment handler from tools/add_comment.js.
import { addComment } from './tools/add_comment.js'; - packages/docx-mcp/src/server.ts:125-126 (registration)Dispatch case in server.ts that routes 'add_comment' tool calls to the addComment handler function.
case 'add_comment': return await addComment(sessions, args as Parameters<typeof addComment>[1]); - Core addComment function in docx-core that inserts a root comment into DOCX XML. Allocates comment ID, inserts commentRangeStart/commentRangeEnd markers in document body, adds a comment element to comments.xml, and ensures the author is in people.xml. Also includes addCommentReply for threaded replies and internal helpers (addCommentElement, insertCommentMarkers, linkReplyInCommentsExtended).
export type AddCommentParams = { paragraphEl: Element; start?: number; end?: number; author: string; text: string; initials?: string; }; export type AddCommentResult = { commentId: number; }; /** * Insert a root comment anchored to a text range within a paragraph. * * - Allocates next comment ID from existing comments.xml * - Inserts commentRangeStart/commentRangeEnd markers in document body * - Inserts commentReference run after range end * - Adds comment entry to comments.xml * - Adds author to people.xml if not present */ export async function addComment( documentXml: Document, zip: DocxZip, params: AddCommentParams, ): Promise<AddCommentResult> { const { paragraphEl, author, text, initials } = params; const start = params.start ?? 0; const end = params.end ?? getParagraphText(paragraphEl).length; if (start > end) { throw new Error(`Invalid comment range: start (${start}) must be <= end (${end})`); } // Load comments.xml const commentsXml = await zip.readText('word/comments.xml'); const commentsDoc = parseXml(commentsXml); // Allocate next comment ID const commentId = allocateNextCommentId(commentsDoc); // Insert range markers and reference in document body insertCommentMarkers(documentXml, paragraphEl, commentId, start, end); // Add comment element to comments.xml const paraId = generateParaId(); addCommentElement(commentsDoc, { id: commentId, author, initials: initials ?? author.charAt(0).toUpperCase(), text, paraId, }); zip.writeText('word/comments.xml', serializeXml(commentsDoc)); // Add author to people.xml await ensureAuthorInPeople(zip, author); return { commentId }; }