mcp-word-bridge 3.2.3 → 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 |
@@ -227,21 +229,23 @@ mcp-word-bridge/
227
229
  5. Changes go through Word's editing pipeline — cursor follows edits like a human typing
228
230
  6. When the MCP client terminates the process, the bridge server stops automatically
229
231
 
230
- ## Regenerating TLS Certificates
232
+ ## TLS Certificate
231
233
 
232
- A self-signed localhost certificate is **auto-generated on first run** if `certs/cert.pem` and `certs/key.pem` don't exist. No manual step needed.
234
+ A self-signed localhost certificate is **auto-generated on first run** if `certs/cert.pem` and `certs/key.pem` don't exist. The server prints the exact trust command with the resolved path:
233
235
 
234
- To trust the cert (required for Word to connect without warnings):
235
-
236
- **macOS:**
237
- ```bash
238
- sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain node_modules/mcp-word-bridge/certs/cert.pem
239
236
  ```
237
+ [mcp-word-bridge] ✓ Certificate generated.
238
+ [mcp-word-bridge]
239
+ [mcp-word-bridge] To trust the certificate (required once for Word to connect):
240
+ [mcp-word-bridge] security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db "/path/to/certs/cert.pem"
241
+ ```
242
+
243
+ Run the printed command once. No `sudo` required — it adds to your user login keychain.
240
244
 
241
- **To regenerate manually:**
245
+ **To regenerate:**
242
246
  ```bash
243
247
  rm certs/cert.pem certs/key.pem
244
- node index.js # will regenerate on startup
248
+ # restart the MCP server — it will regenerate and print the trust command again
245
249
  ```
246
250
 
247
251
  ## Known Limitations & Word API Behavior
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');
@@ -40,8 +40,17 @@ function ensureCerts() {
40
40
  ].join('\n'));
41
41
  }
42
42
  execSync('openssl req -x509 -newkey rsa:2048 -keyout "' + keyPath + '" -out "' + certPath + '" -days 3650 -nodes -config "' + confPath + '"', { stdio: 'pipe' });
43
- process.stderr.write('[mcp-word-bridge] Certificate generated at ' + CERTS_DIR + '\n');
44
- process.stderr.write('[mcp-word-bridge] NOTE: You may need to trust this cert in your OS keychain for Word to connect.\n');
43
+ process.stderr.write('[mcp-word-bridge] Certificate generated.\n');
44
+ process.stderr.write('[mcp-word-bridge]\n');
45
+ process.stderr.write('[mcp-word-bridge] To trust the certificate (required once for Word to connect):\n');
46
+ if (process.platform === 'darwin') {
47
+ process.stderr.write('[mcp-word-bridge] security add-trusted-cert -r trustRoot -k ~/Library/Keychains/login.keychain-db "' + certPath + '"\n');
48
+ } else if (process.platform === 'win32') {
49
+ process.stderr.write('[mcp-word-bridge] certutil -user -addstore Root "' + certPath + '"\n');
50
+ } else {
51
+ process.stderr.write('[mcp-word-bridge] sudo cp "' + certPath + '" /usr/local/share/ca-certificates/ && sudo update-ca-certificates\n');
52
+ }
53
+ process.stderr.write('[mcp-word-bridge]\n');
45
54
  }
46
55
 
47
56
  ensureCerts();
@@ -176,6 +185,8 @@ const tools = [
176
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'] } },
177
186
  { name: 'word_resolve_comment', description: 'Mark a comment as resolved by its ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
178
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: {} } },
179
190
  // 8. FOOTNOTES & ENDNOTES
180
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'] } },
181
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'] } },
@@ -254,6 +265,7 @@ const toolActionMap = {
254
265
  word_add_comment: 'addComment', word_get_comments: 'getComments',
255
266
  word_get_comment_replies: 'getCommentReplies', word_reply_to_comment: 'replyToComment',
256
267
  word_resolve_comment: 'resolveComment', word_delete_comment: 'deleteComment',
268
+ word_get_comment_anchor: 'getCommentAnchor', word_get_comments_with_anchor: 'getCommentsWithAnchor',
257
269
  word_insert_footnote: 'insertFootnote', word_insert_footnote_at_index: 'insertFootnoteAtIndex',
258
270
  word_insert_endnote: 'insertEndnote', word_get_footnotes: 'getFootnotes',
259
271
  word_get_endnotes: 'getEndnotes', word_delete_footnote: 'deleteFootnote', word_delete_endnote: 'deleteEndnote',
@@ -279,8 +291,8 @@ const toolActionMap = {
279
291
  };
280
292
 
281
293
  const mcpServer = new Server(
282
- { name: 'mcp-word-bridge', version: '3.2.0' },
283
- { capabilities: { tools: {} } }
294
+ { name: 'mcp-word-bridge', version: '3.3.0' },
295
+ { capabilities: { tools: {}, resources: {} } }
284
296
  );
285
297
 
286
298
  mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
@@ -297,18 +309,138 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
297
309
  }
298
310
  });
299
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
+
300
421
  // ═══════════════════════════════════════════════════════════════════════════════
301
422
  // PART 4: Startup & Shutdown
302
423
  // ═══════════════════════════════════════════════════════════════════════════════
303
424
 
425
+ let shuttingDown = false;
304
426
  function shutdown() {
427
+ if (shuttingDown) return;
428
+ shuttingDown = true;
305
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
306
433
  httpsServer.close();
307
- process.exit(0);
434
+ httpsServer.closeAllConnections();
435
+ // Ensure exit even if something holds the event loop
436
+ setTimeout(() => process.exit(0), 500);
308
437
  }
309
438
 
310
439
  process.on('SIGTERM', shutdown);
311
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);
312
444
 
313
445
  async function main() {
314
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.3",
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;