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 +14 -10
- package/index.js +139 -7
- package/package.json +1 -1
- package/taskpane-app.js +30 -0
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ mcp-word-bridge/
|
|
|
66
66
|
└── README.md
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
-
## Tools (
|
|
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
|
-
##
|
|
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.
|
|
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
|
|
245
|
+
**To regenerate:**
|
|
242
246
|
```bash
|
|
243
247
|
rm certs/cert.pem certs/key.pem
|
|
244
|
-
|
|
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.
|
|
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
|
|
44
|
-
process.stderr.write('[mcp-word-bridge]
|
|
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.
|
|
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
|
-
|
|
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
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;
|