mcp-word-bridge 3.2.4 → 3.3.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/README.md CHANGED
@@ -66,7 +66,7 @@ mcp-word-bridge/
66
66
  └── README.md
67
67
  ```
68
68
 
69
- ## Tools (82)
69
+ ## Tools (84)
70
70
 
71
71
  ### Document
72
72
  | Tool | Description |
@@ -137,6 +137,8 @@ mcp-word-bridge/
137
137
  | `word_reply_to_comment` | Reply to a comment |
138
138
  | `word_resolve_comment` | Mark a comment as resolved |
139
139
  | `word_delete_comment` | Delete a comment and its replies |
140
+ | `word_get_comment_anchor` | Get the document text a comment is anchored to |
141
+ | `word_get_comments_with_anchor` | Get all comments with their anchor text included |
140
142
 
141
143
  ### Footnotes & Endnotes
142
144
  | Tool | Description |
package/index.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * Single entry point: starts HTTPS bridge + MCP server in one process.
5
5
  * The MCP client spawns this; everything starts and stops together.
6
6
  *
7
- * v3.2.0 — 82 tools
7
+ * v3.3.0 — 84 tools
8
8
  */
9
9
  const https = require('https');
10
10
  const fs = require('fs');
@@ -12,7 +12,7 @@ const path = require('path');
12
12
  const { WebSocketServer, WebSocket } = require('ws');
13
13
  const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
14
14
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
15
- const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
15
+ const { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
16
16
 
17
17
  const PORT = parseInt(process.env.MCP_WORD_BRIDGE_PORT || '3000', 10);
18
18
  const CERTS_DIR = path.join(__dirname, 'certs');
@@ -185,6 +185,8 @@ const tools = [
185
185
  { name: 'word_reply_to_comment', description: 'Reply to a comment by its ID (from get_comments).', inputSchema: { type: 'object', properties: { commentId: { type: 'string' }, text: { type: 'string' } }, required: ['commentId', 'text'] } },
186
186
  { name: 'word_resolve_comment', description: 'Mark a comment as resolved by its ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
187
187
  { name: 'word_delete_comment', description: 'Delete a comment and its replies by ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
188
+ { name: 'word_get_comment_anchor', description: 'Get the document text that a comment is anchored to (the highlighted/marked text the comment refers to).', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
189
+ { name: 'word_get_comments_with_anchor', description: 'Get all comments with ID, author, content, date, resolved status, AND the anchor text each comment is attached to.', inputSchema: { type: 'object', properties: {} } },
188
190
  // 8. FOOTNOTES & ENDNOTES
189
191
  { name: 'word_insert_footnote', description: 'Insert a footnote anchored to a text match.', inputSchema: { type: 'object', properties: { anchorText: { type: 'string' }, text: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['anchorText', 'text'] } },
190
192
  { name: 'word_insert_footnote_at_index', description: 'Insert a footnote at the end of a paragraph by index (no search needed).', inputSchema: { type: 'object', properties: { paragraphIndex: { type: 'number' }, text: { type: 'string' } }, required: ['paragraphIndex', 'text'] } },
@@ -263,6 +265,7 @@ const toolActionMap = {
263
265
  word_add_comment: 'addComment', word_get_comments: 'getComments',
264
266
  word_get_comment_replies: 'getCommentReplies', word_reply_to_comment: 'replyToComment',
265
267
  word_resolve_comment: 'resolveComment', word_delete_comment: 'deleteComment',
268
+ word_get_comment_anchor: 'getCommentAnchor', word_get_comments_with_anchor: 'getCommentsWithAnchor',
266
269
  word_insert_footnote: 'insertFootnote', word_insert_footnote_at_index: 'insertFootnoteAtIndex',
267
270
  word_insert_endnote: 'insertEndnote', word_get_footnotes: 'getFootnotes',
268
271
  word_get_endnotes: 'getEndnotes', word_delete_footnote: 'deleteFootnote', word_delete_endnote: 'deleteEndnote',
@@ -288,8 +291,8 @@ const toolActionMap = {
288
291
  };
289
292
 
290
293
  const mcpServer = new Server(
291
- { name: 'mcp-word-bridge', version: '3.2.0' },
292
- { capabilities: { tools: {} } }
294
+ { name: 'mcp-word-bridge', version: '3.3.0' },
295
+ { capabilities: { tools: {}, resources: {} } }
293
296
  );
294
297
 
295
298
  mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -306,18 +309,138 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
306
309
  }
307
310
  });
308
311
 
312
+ // ═══════════════════════════════════════════════════════════════════════════════
313
+ // PART 3b: Resources — Usage Guide for LLMs
314
+ // ═══════════════════════════════════════════════════════════════════════════════
315
+
316
+ const USAGE_GUIDE = `# MCP Word Bridge — Usage Guide
317
+
318
+ Controls a live Word document through an Office Add-in. All operations execute immediately in Word.
319
+
320
+ ## Reading Content
321
+ - \`word_get_paragraphs\` — structured content (text, style, alignment, isTocEntry). Paginate with start/end.
322
+ - \`word_get_text\` — quick plain-text dump (no structure)
323
+ - \`word_search\` — locate text before operating on it
324
+
325
+ ## Editing Text
326
+ - \`word_search_and_replace\` — bulk find/replace
327
+ - \`word_insert_text\` — insert before/after a search match (use \`occurrence\` for Nth match)
328
+ - \`word_insert_text_at_selection\` — insert at cursor or replace selection
329
+ - Verify edits with \`word_search\` or \`word_get_paragraphs\`
330
+
331
+ ## Search Behavior (applies to ALL search-based tools)
332
+ - Case-insensitive by default. Pass \`matchCase: true\` for exact match.
333
+ - Affected tools: search, search_and_replace, format_text, insert_text, insert_footnote, add_comment, insert_hyperlink, insert_bookmark, insert_content_control, clear_formatting, get_font_info, insert_line_break, remove_hyperlink, insert_endnote.
334
+
335
+ ## Alignment Values
336
+ The bridge normalizes aliases: Left, Center/Centered, Right, Justify/Justified.
337
+
338
+ ## Change Tracking
339
+ - Call \`word_set_change_tracking({mode:"TrackAll"})\` BEFORE edits for tracked changes
340
+ - Adjacent insertions by the same author may coalesce into a single tracked change
341
+ - \`search_and_replace\` with tracking may only expose the "Added" half; use \`accept_all_tracked_changes\` if granular control isn't needed
342
+
343
+ ## Comments — CRITICAL PATTERNS
344
+
345
+ ### Reading
346
+ - \`word_get_comments_with_anchor\` — preferred: returns comments + their anchored document text
347
+ - \`word_get_comment_replies\` — reply thread for a specific comment
348
+
349
+ ### Comment + text editing interaction
350
+ **Problem:** \`word_search_and_replace\` on text anchoring a comment collapses the anchor (shrinks to empty). The comment survives but loses its positional context.
351
+
352
+ **Safe pattern:**
353
+ 1. \`word_get_comments_with_anchor\` — identify commented text
354
+ 2. If replacement overlaps a comment anchor:
355
+ a. \`word_reply_to_comment\` explaining resolution (if appropriate)
356
+ b. \`word_resolve_comment\`
357
+ c. THEN replace the text
358
+ 3. Resolved thread is preserved as a record
359
+
360
+ **Avoid:** replacing text first (anchor collapses), or deleting+recreating comments (loses author/date/thread).
361
+
362
+ ### Adding
363
+ - \`word_add_comment\` — anchor to a text match
364
+ - \`word_reply_to_comment\` — reply to existing thread
365
+ - \`word_resolve_comment\` — hides in Word UI, preserves history
366
+
367
+ ## Tables
368
+ - 0-based indexing: tableIndex, row, col
369
+ - \`word_get_tables\` for overview, \`word_get_table_data\` for a specific table's cells
370
+ - Table cell paragraphs can't be structurally deleted (only cleared)
371
+ - Can't insert page/section breaks inside table cells
372
+
373
+ ## Footnotes & Endnotes
374
+ - \`word_insert_footnote\` — anchor to text match
375
+ - \`word_insert_footnote_at_index\` — anchor to paragraph by index (no search needed)
376
+
377
+ ## Page Layout
378
+ - Margins in points (72 pt = 1 inch)
379
+ - \`lineSpacing\` in \`set_paragraph_spacing\` is in points, not a multiplier (12pt font: 12=single, 18=1.5x, 24=double)
380
+
381
+ ## Content Controls
382
+ - RichText/PlainText: wraps the anchor text (non-destructive)
383
+ - CheckBox: REPLACES anchor text with a checkbox glyph (destructive, cannot be modified with set_content_control_text)
384
+
385
+ ## TOC Behavior
386
+ After inserting a Table of Contents, heading text appears twice in the document (once in TOC, once in body). Search matches TOC entries first — use \`occurrence\` parameter to target the body instance.
387
+
388
+ ## Error Messages
389
+ - "Word taskpane not connected" — user must open the MCP Word Bridge add-in in Word
390
+ - "Anchor not found" — search text not found in document
391
+ - "Occurrence N not found" — match index out of range
392
+ - Timeout: 30s default, 60s for HTML/OOXML/styles/TOC insertion
393
+
394
+ ## Best Practices
395
+ 1. Read before writing — understand document structure first
396
+ 2. \`word_get_comments_with_anchor\` before bulk replacements to avoid anchor damage
397
+ 3. Enable change tracking for collaborative documents
398
+ 4. \`word_save\` explicitly after significant changes
399
+ 5. Resolve comments rather than deleting them (preserves audit trail)
400
+ `;
401
+
402
+ mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
403
+ resources: [
404
+ {
405
+ uri: 'word-bridge://usage-guide',
406
+ name: 'Word Bridge Usage Guide',
407
+ description: 'Patterns, workflows, and pitfalls for LLMs operating on Word documents via this MCP server',
408
+ mimeType: 'text/markdown'
409
+ }
410
+ ]
411
+ }));
412
+
413
+ mcpServer.setRequestHandler(ReadResourceRequestSchema, async (request) => {
414
+ const { uri } = request.params;
415
+ if (uri === 'word-bridge://usage-guide') {
416
+ return { contents: [{ uri, mimeType: 'text/markdown', text: USAGE_GUIDE }] };
417
+ }
418
+ throw new Error('Resource not found: ' + uri);
419
+ });
420
+
309
421
  // ═══════════════════════════════════════════════════════════════════════════════
310
422
  // PART 4: Startup & Shutdown
311
423
  // ═══════════════════════════════════════════════════════════════════════════════
312
424
 
425
+ let shuttingDown = false;
313
426
  function shutdown() {
427
+ if (shuttingDown) return;
428
+ shuttingDown = true;
314
429
  process.stderr.write('[mcp-word-bridge] Shutting down...\n');
430
+ // Close all WebSocket connections immediately
431
+ wss.clients.forEach((ws) => ws.terminate());
432
+ // Close the HTTPS server and force-destroy all open sockets
315
433
  httpsServer.close();
316
- process.exit(0);
434
+ httpsServer.closeAllConnections();
435
+ // Ensure exit even if something holds the event loop
436
+ setTimeout(() => process.exit(0), 500);
317
437
  }
318
438
 
319
439
  process.on('SIGTERM', shutdown);
320
440
  process.on('SIGINT', shutdown);
441
+ // MCP clients terminate by closing stdin — StdioServerTransport doesn't handle this
442
+ process.stdin.on('end', shutdown);
443
+ process.stdin.on('close', shutdown);
321
444
 
322
445
  async function main() {
323
446
  // Start HTTPS bridge server
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-word-bridge",
3
- "version": "3.2.4",
3
+ "version": "3.3.0",
4
4
  "description": "MCP server for live Word document editing via Office Add-in",
5
5
  "main": "index.js",
6
6
  "bin": {
package/taskpane-app.js CHANGED
@@ -1113,6 +1113,36 @@ commands.getCommentReplies = (p) => Word.run(async (ctx) => {
1113
1113
  throw new Error('Comment not found: ' + p.commentId);
1114
1114
  });
1115
1115
 
1116
+ // == COMMENT ANCHOR TEXT ==
1117
+ commands.getCommentAnchor = (p) => Word.run(async (ctx) => {
1118
+ const comments = ctx.document.body.getComments();
1119
+ comments.load('id');
1120
+ await ctx.sync();
1121
+ let target = null;
1122
+ for (const c of comments.items) {
1123
+ if (String(c.id) === String(p.commentId)) { target = c; break; }
1124
+ }
1125
+ if (!target) throw new Error('Comment not found: ' + p.commentId);
1126
+ const range = target.getRange();
1127
+ range.load('text');
1128
+ await ctx.sync();
1129
+ return { commentId: p.commentId, anchorText: range.text };
1130
+ });
1131
+
1132
+ commands.getCommentsWithAnchor = () => Word.run(async (ctx) => {
1133
+ const comments = ctx.document.body.getComments();
1134
+ comments.load('id,authorName,content,creationDate,resolved');
1135
+ await ctx.sync();
1136
+ const items = [];
1137
+ for (const c of comments.items) {
1138
+ const range = c.getRange();
1139
+ range.load('text');
1140
+ await ctx.sync();
1141
+ items.push({ id: c.id, author: c.authorName, content: c.content, date: c.creationDate, resolved: c.resolved, anchorText: range.text });
1142
+ }
1143
+ return { count: items.length, comments: items };
1144
+ });
1145
+
1116
1146
  // == CHANGE TRACKING MODE ==
1117
1147
  commands.getChangeTrackingMode = () => Word.run(async (ctx) => {
1118
1148
  const doc = ctx.document;