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 +3 -1
- package/index.js +128 -5
- 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 |
|
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');
|
|
@@ -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.
|
|
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
|
-
|
|
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
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;
|