mcp-word-bridge 3.2.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 ADDED
@@ -0,0 +1,181 @@
1
+ # MCP Word Bridge
2
+
3
+ MCP server for live Word document editing via Office Add-in. Enables programmatic editing of Word documents through the Word JavaScript API, with changes appearing as user edits in co-authoring sessions.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ MCP Client ←stdio→ index.js ←WebSocket→ Taskpane (Office Add-in) ←→ Word JS API ←→ Word Document
9
+ ```
10
+
11
+ Single process. The MCP client spawns `index.js`, which starts both the HTTPS bridge server (for the add-in) and the MCP server (on stdio). Everything starts and stops together.
12
+
13
+ ## Setup
14
+
15
+ ### 1. Sideload the add-in manifest
16
+
17
+ Copy the manifest to Word's sideload directory:
18
+
19
+ ```bash
20
+ cp manifest.xml ~/Library/Containers/com.microsoft.Word/Data/Documents/wef/
21
+ ```
22
+
23
+ ### 2. Add MCP config
24
+
25
+ Add to your MCP client configuration (e.g. `.kiro/settings/mcp.json`):
26
+
27
+ ```json
28
+ {
29
+ "mcpServers": {
30
+ "word-bridge": {
31
+ "command": "node",
32
+ "args": ["/path/to/word-addin/index.js"],
33
+ "disabled": false
34
+ }
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### 3. Open Word and activate the add-in
40
+
41
+ Open Word → Home → Add-ins → MCP Word Bridge
42
+
43
+ That's it. The MCP server starts automatically when your MCP client loads the config, and stops when it unloads.
44
+
45
+ ## Environment Variables
46
+
47
+ | Variable | Default | Description |
48
+ |----------|---------|-------------|
49
+ | `MCP_WORD_BRIDGE_PORT` | `3000` | HTTPS port for the bridge server. Must match the manifest `SourceLocation`. |
50
+
51
+ ## File Layout
52
+
53
+ ```
54
+ word-addin/
55
+ ├── index.js # Single entry point (MCP + bridge server)
56
+ ├── taskpane.html # Served to Word add-in
57
+ ├── taskpane-app.js # Client-side Word JS API logic
58
+ ├── certs/
59
+ │ ├── cert.pem # Self-signed TLS cert
60
+ │ └── key.pem # TLS private key
61
+ ├── manifest.xml # Office add-in manifest (sideloaded)
62
+ ├── cert.conf # OpenSSL config for cert regeneration
63
+ ├── package.json
64
+ └── README.md
65
+ ```
66
+
67
+ ## Tools (82)
68
+
69
+ **Document:** get_text, get_document_properties, set_document_properties, save, get_word_count, get_styles, get_coauthors, set_change_tracking
70
+
71
+ **Paragraphs:** get_paragraphs, get_paragraph_by_index, insert_paragraph, delete_paragraph, set_paragraph_style, set_paragraph_spacing
72
+
73
+ **Search & Text:** search, search_and_replace, insert_text, get_selection_info, insert_text_at_selection, insert_line_break
74
+
75
+ **Formatting:** format_text, clear_formatting, get_font_info
76
+
77
+ **Tables:** insert_table, get_tables, get_table_data, set_table_cell, add_table_row, delete_table_row, merge_table_cells, split_table_cell, set_table_style, set_table_cell_shading
78
+
79
+ **Lists:** insert_list, get_list_info, set_list_level
80
+
81
+ **Comments:** add_comment, get_comments, get_comment_replies, reply_to_comment, resolve_comment, delete_comment
82
+
83
+ **Footnotes/Endnotes:** insert_footnote, insert_footnote_at_index, insert_endnote, get_footnotes, get_endnotes, delete_footnote, delete_endnote
84
+
85
+ **Track Changes:** get_tracked_changes, accept_tracked_change, reject_tracked_change, accept_all_tracked_changes, reject_all_tracked_changes
86
+
87
+ **Content Controls:** get_content_controls, insert_content_control, set_content_control_text
88
+
89
+ **Bookmarks:** get_bookmarks, insert_bookmark, delete_bookmark, go_to_bookmark, get_bookmark_text
90
+
91
+ **Hyperlinks:** insert_hyperlink, get_hyperlinks, remove_hyperlink
92
+
93
+ **Headers/Footers:** get_header_footer, set_header_footer
94
+
95
+ **Images:** insert_image, get_images, delete_image
96
+
97
+ **Page Layout:** get_page_layout, set_page_layout, get_sections, insert_page_break, insert_section_break
98
+
99
+ **Custom Properties:** get_custom_properties, set_custom_property, delete_custom_property
100
+
101
+ **Advanced:** insert_html, insert_ooxml, insert_table_of_contents, get_fields
102
+
103
+ ## How it works
104
+
105
+ 1. The MCP client spawns `index.js` via stdio
106
+ 2. `index.js` starts an HTTPS server on port 3000 (serves taskpane + WebSocket relay)
107
+ 3. The Word add-in taskpane connects via WebSocket
108
+ 4. MCP tool calls are forwarded to the taskpane, which executes them via the Word JS API
109
+ 5. Changes go through Word's editing pipeline — cursor follows edits like a human typing
110
+ 6. When the MCP client terminates the process, the bridge server stops automatically
111
+
112
+ ## Regenerating TLS Certificates
113
+
114
+ The self-signed cert is required for the HTTPS connection to the taskpane. To regenerate:
115
+
116
+ ```bash
117
+ openssl req -x509 -newkey rsa:2048 -keyout certs/key.pem -out certs/cert.pem -days 3650 -nodes -config cert.conf
118
+ ```
119
+
120
+ Then trust the cert in Keychain Access (macOS) to avoid browser warnings.
121
+
122
+ ## Known Limitations & Word API Behavior
123
+
124
+ ### Search defaults
125
+
126
+ All search-based operations (search, search_and_replace, format_text, insert_footnote, add_comment, etc.) are **case-insensitive by default**. Pass `matchCase: true` for exact case matching.
127
+
128
+ ### TOC paragraphs
129
+
130
+ `get_paragraphs` includes a `isTocEntry` boolean field to distinguish Table of Contents entries from body content. TOC entries have styles starting with "TOC" (e.g., "TOC 1", "TOC 2").
131
+
132
+ ### Alignment values
133
+
134
+ The Word JS API uses specific enum strings for paragraph alignment. The bridge accepts common aliases and normalizes them:
135
+
136
+ | You can pass | API receives |
137
+ |---|---|
138
+ | `Left` | `Left` |
139
+ | `Center` or `Centered` | `Centered` |
140
+ | `Right` | `Right` |
141
+ | `Justify` or `Justified` | `Justified` |
142
+
143
+ ### Content controls and search
144
+
145
+ In heavily-mutated documents (many rapid operations in a single session), text adjacent to content controls may occasionally become invisible to `search`. This is a transient Word API state issue. If search fails to find text you know exists, saving and reopening the document typically resolves it.
146
+
147
+ ### CheckBox content controls
148
+
149
+ CheckBox content controls are atomic widgets — they REPLACE the anchor text with a checkbox glyph (☐), unlike RichText and PlainText which wrap the text. `set_content_control_text` cannot modify CheckBox controls (they only support checked/unchecked state).
150
+
151
+ ### Table cell paragraphs
152
+
153
+ Table cells always contain at least one paragraph. `delete_paragraph` on a table cell paragraph clears the cell text but cannot remove the structural cell itself. `insert_section_break` and `insert_page_break` cannot target paragraphs inside table cells (Word constraint).
154
+
155
+ ### Tracked changes coalescing
156
+
157
+ Adjacent insertions by the same author at the same timestamp are merged into a single tracked change by Word. Two separate `insert_paragraph` calls may appear as one revision in `get_tracked_changes`.
158
+
159
+ ### Tracked changes and replacements
160
+
161
+ When `search_and_replace` creates a tracked change with tracking enabled, the Word API may only expose the "Added" half of the replacement. The "Deleted" half can appear later (often with empty text) after the "Added" change is accepted. This is a Word API limitation. Use `accept_all_tracked_changes` when you don't need granular control.
162
+
163
+ ### TOC and duplicate text
164
+
165
+ After inserting a Table of Contents, heading text appears twice in the document (once in the TOC, once in the body). Search-based operations will match TOC entries first. Use the `occurrence` parameter (0-indexed) to target the correct instance.
166
+
167
+ ### Hyperlink indices
168
+
169
+ `get_hyperlinks` returns indices from the underlying fields collection (which includes non-hyperlink fields). Indices may be non-contiguous — this is by design for cross-referencing with `get_fields`.
170
+
171
+ ### Document path
172
+
173
+ `path` in `get_document_properties` is only reliable for documents saved to a user-chosen location with a `.docx` extension. For unsaved documents or auto-saved container documents, it returns `null`. Save the document explicitly (File → Save As) if you need the file path.
174
+
175
+ ### Line spacing units
176
+
177
+ `lineSpacing` in `set_paragraph_spacing` and `get_paragraph_by_index` is in **points** (not a multiplier). For a 12pt font: 12 = single spacing, 18 = 1.5x, 24 = double spacing.
178
+
179
+ ### Paragraph ordering with concurrent inserts
180
+
181
+ When multiple paragraphs are inserted at `"End"` in rapid succession, Word may reorder them based on heading level or style hierarchy. For guaranteed ordering, insert sequentially and verify with `get_paragraphs`.
@@ -0,0 +1,17 @@
1
+ [req]
2
+ default_bits = 2048
3
+ prompt = no
4
+ default_md = sha256
5
+ distinguished_name = dn
6
+ x509_extensions = v3_req
7
+
8
+ [dn]
9
+ CN = localhost
10
+
11
+ [v3_req]
12
+ basicConstraints = CA:TRUE
13
+ subjectAltName = @alt_names
14
+
15
+ [alt_names]
16
+ DNS.1 = localhost
17
+ IP.1 = 127.0.0.1
package/certs/cert.pem ADDED
@@ -0,0 +1,19 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDATCCAemgAwIBAgIUXHIdF3kFBwDnYn+HtAfpiihdkx4wDQYJKoZIhvcNAQEL
3
+ BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI2MDYwMTEzMDQxMFoXDTI3MDYw
4
+ MTEzMDQxMFowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
5
+ AAOCAQ8AMIIBCgKCAQEA30z83NusMTz0TTHfQN2lb7vQsBR+RcI6fNMEu9Diz2lO
6
+ +NyvMhCY44esfr/h/LaT2t49HcuQF/1BN0UlbtKxRHU7Ex8lu6cPwYrYZgwcL+0p
7
+ jyYdHeCb10kzGagOn86PLt6Q6TG+Rmiqi1P4Q6+bfsXGRj6tQkR5SxgMZFC4claM
8
+ O7o9h1IibstyN+FP/5jZoVJOa0/qPe76292PwFYJXAen0J9ho2PLrTL78hVegG3v
9
+ B0IqBtvf4fARnNbT+VLZMf+zYTmYh3rVEh2FHg9FejO1LWQpab4b6kA9HN4Uj7f6
10
+ Fgjf/quIihLoMNGkZ5FrKpOy+UnTS5w0ZrBUmxhvpwIDAQABo0swSTAMBgNVHRME
11
+ BTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9zdIcEfwAAATAdBgNVHQ4EFgQUHGnT
12
+ m6TAtBX/3kEcyqsP1MjY8+AwDQYJKoZIhvcNAQELBQADggEBAFXEtaUBwTxp/nfs
13
+ 4r6L7xpbTERLx+/4R/8RNP09m9W31SsGHoTcVmqTZz9QtKzWdFUH5xxREOEvHnGA
14
+ a6VAljMo6PYYcZlZH83CWwDbdlKdm7PVxEbtPWtz8B43lUVnBaRU6+RORTVSjvnU
15
+ KfRtsLP30qctT8/ZjPWEcT2+5gOczYzECYtH5bVJdmr+rWJRVORwnEBVUnxWKJ0h
16
+ 1vK9yQ3dfJlMV6aHDmkSncyYwHvbV6rTn4aiaNW148ehF++NQ7/6bX2M4w6rcHfi
17
+ tfRjoOnyCvN6km0PYbd7pKQzUVhJPUFMYPELiJpD8KxjjIL9fbjf+nQz069HgLYW
18
+ vyKVAvE=
19
+ -----END CERTIFICATE-----
package/certs/key.pem ADDED
@@ -0,0 +1,28 @@
1
+ -----BEGIN PRIVATE KEY-----
2
+ MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDfTPzc26wxPPRN
3
+ Md9A3aVvu9CwFH5Fwjp80wS70OLPaU743K8yEJjjh6x+v+H8tpPa3j0dy5AX/UE3
4
+ RSVu0rFEdTsTHyW7pw/BithmDBwv7SmPJh0d4JvXSTMZqA6fzo8u3pDpMb5GaKqL
5
+ U/hDr5t+xcZGPq1CRHlLGAxkULhyVow7uj2HUiJuy3I34U//mNmhUk5rT+o97vrb
6
+ 3Y/AVglcB6fQn2GjY8utMvvyFV6Abe8HQioG29/h8BGc1tP5Utkx/7NhOZiHetUS
7
+ HYUeD0V6M7UtZClpvhvqQD0c3hSPt/oWCN/+q4iKEugw0aRnkWsqk7L5SdNLnDRm
8
+ sFSbGG+nAgMBAAECggEADoDBEZUg52fGlDbvgZaDtrCTmsQR+XTmeJH6BjrIaGE2
9
+ FFq89Dr4uxpmPSs4QcIX80io3oUInE5CDJVsm3iKs+ALULAessPkdZUPff0+XWyB
10
+ XP9EN9sNNBvYekuce4ueaBRjhAnLimYk4Xy4LKk8p6yvtoM+nIW2/QYYU/GcMSzH
11
+ wuq6U1aX4RopFKlNDHHCQa6+kqdVs/qKICu9Xd74GDtJ3RnNuw0OjHE09KSIEL0W
12
+ ifiE9tZ1yQhxWsDFnhYKVwd2yYARiy4afP7ieLwEoIN4vngsz7H5RUJhFmEiyzbn
13
+ P3kaMeaExNBYOjMXmqsGhiY6NoWzG3Gz7GTofIU1hQKBgQD6NW8fH6pzWvWiQ08r
14
+ +ATnrPtMaoFVRUCpwMlJnM02cPXPIatM5y25fIuWkxWHa8YUXDcDacN5/R3mEdfn
15
+ qTV3NrraXHBwaZprqikgWRCTCMypmNuYH0n+yA9i1m2Te4PyK4z9uaHC96+li6U3
16
+ woceGKixzg3f5Q8JQy8XO5MBdQKBgQDkeB2D+6R7tA35pTgIjzZwtcK9PEOhIa8f
17
+ WsdwSuh74PkAYnid7ksVzHhHo6dbWYKy8KA4TevTDepLWNQjkChsaYX+dcYOgsIW
18
+ 4ixrSbOlgqCtVtTP98ytSHaJspgCCQE2SzvC0IcZqYk9BOcH45TcFIyWfV15D3mw
19
+ pqhPXDtNKwKBgEMAuCcvhaeqfgjb2YG+wyF/UzRdeRDqoKxUshKCaPnhOhIjxAmu
20
+ BrKbRY4nCSbgl4SwRRMm6W/rdmw77wNcbrLj9xmuk3Wm8fFO+gBtmWCmhJgOFRAh
21
+ oOEXlfcz0NgjxWu+ed0gLs9VILZGNRI/h4tpsxMaSODiKCqk0SF5lJ5ZAoGBAJDR
22
+ 6rOsoTigi3NBXWFfljyfmk9lkeDjfyQ64My3TuKnWm75/Ebvs7yfnWabwAvRk11l
23
+ 1cma6u8flPIp3l6klFsUEJGZie/MxsbGmy1uzGcPhFYcAk3JX34/vpPOFzjDCHen
24
+ /LuifuCvbIS3RNLlWYifpfYGhWelfZeSLIIRjq19AoGAUnDp4GFohF8/zExNj4lv
25
+ dGD3GO6Qj401PbhBWjba22JD4V8yHFCPJOsdsiC/KCNeRirbupM+vivxXbNGuNr6
26
+ g+H9sJrOJ4jKZBpjW3T/WVMpQhN498zKKqtUs0GYdDV9aFq4ywTDE0F3/QiWq6RM
27
+ JD0eQifgAJsBmX+bcnej+ws=
28
+ -----END PRIVATE KEY-----
package/index.js ADDED
@@ -0,0 +1,313 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * MCP Word Bridge — Unified Server
4
+ * Single entry point: starts HTTPS bridge + MCP server in one process.
5
+ * The MCP client spawns this; everything starts and stops together.
6
+ *
7
+ * v3.2.0 — 82 tools
8
+ */
9
+ const https = require('https');
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const { WebSocketServer, WebSocket } = require('ws');
13
+ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
14
+ const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
15
+ const { CallToolRequestSchema, ListToolsRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
16
+
17
+ const PORT = parseInt(process.env.MCP_WORD_BRIDGE_PORT || '3000', 10);
18
+ const CERTS_DIR = path.join(__dirname, 'certs');
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════════
21
+ // PART 1: HTTPS + WebSocket Bridge Server
22
+ // ═══════════════════════════════════════════════════════════════════════════════
23
+
24
+ const sslOptions = {
25
+ key: fs.readFileSync(path.join(CERTS_DIR, 'key.pem')),
26
+ cert: fs.readFileSync(path.join(CERTS_DIR, 'cert.pem'))
27
+ };
28
+
29
+ const MIME = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.png': 'image/png', '.json': 'application/json' };
30
+
31
+ const httpsServer = https.createServer(sslOptions, (req, res) => {
32
+ let urlPath = req.url.split('?')[0];
33
+ let filePath = urlPath === '/' ? '/taskpane.html' : urlPath;
34
+ filePath = path.join(__dirname, filePath);
35
+ const ext = path.extname(filePath);
36
+ const contentType = MIME[ext] || 'application/octet-stream';
37
+ fs.readFile(filePath, (err, data) => {
38
+ if (err) {
39
+ if (ext === '.png') {
40
+ const pixel = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==', 'base64');
41
+ res.writeHead(200, { 'Content-Type': 'image/png' });
42
+ res.end(pixel);
43
+ return;
44
+ }
45
+ res.writeHead(404);
46
+ res.end('Not found');
47
+ return;
48
+ }
49
+ res.writeHead(200, { 'Content-Type': contentType });
50
+ res.end(data);
51
+ });
52
+ });
53
+
54
+ // WebSocket relay: taskpane ↔ bridge (MCP server)
55
+ const wss = new WebSocketServer({ server: httpsServer });
56
+ let taskpaneSocket = null;
57
+ const bridgePending = new Map();
58
+
59
+ wss.on('connection', (ws, req) => {
60
+ if (req.url === '/taskpane') {
61
+ taskpaneSocket = ws;
62
+ process.stderr.write('[bridge] Taskpane connected\n');
63
+ ws.on('message', (data) => {
64
+ try {
65
+ const msg = JSON.parse(data);
66
+ if (msg.type === 'response' && msg.id) {
67
+ const pending = bridgePending.get(msg.id);
68
+ if (pending) { pending.resolve(msg); bridgePending.delete(msg.id); }
69
+ }
70
+ } catch (e) {}
71
+ });
72
+ ws.on('close', () => { process.stderr.write('[bridge] Taskpane disconnected\n'); taskpaneSocket = null; });
73
+ }
74
+ // The /bridge endpoint is no longer needed — MCP server calls sendToTaskpane directly
75
+ });
76
+
77
+ // Direct bridge function (replaces WebSocket self-connection)
78
+ let mcpRequestId = 0;
79
+ function sendToTaskpane(action, params) {
80
+ return new Promise((resolve, reject) => {
81
+ if (!taskpaneSocket || taskpaneSocket.readyState !== WebSocket.OPEN) {
82
+ reject(new Error('Word taskpane not connected. Open the MCP Word Bridge add-in in Word.'));
83
+ return;
84
+ }
85
+ const id = String(++mcpRequestId);
86
+ const heavyOps = ['insertHtml', 'insertOoxml', 'getStyles', 'insertTableOfContents'];
87
+ const timeoutMs = heavyOps.includes(action) ? 60000 : 30000;
88
+ const timeout = setTimeout(() => {
89
+ bridgePending.delete(id);
90
+ reject(new Error('Operation timed out after ' + (timeoutMs / 1000) + 's. The document may be too large or Word is busy. Try again.'));
91
+ }, timeoutMs);
92
+ bridgePending.set(id, {
93
+ resolve: (msg) => { clearTimeout(timeout); if (msg.error) reject(new Error(msg.error)); else resolve(msg.result); },
94
+ reject: (e) => { clearTimeout(timeout); reject(e); }
95
+ });
96
+ taskpaneSocket.send(JSON.stringify({ id, action, params: params || {} }));
97
+ });
98
+ }
99
+
100
+ // ═══════════════════════════════════════════════════════════════════════════════
101
+ // PART 2: MCP Tool Definitions
102
+ // ═══════════════════════════════════════════════════════════════════════════════
103
+
104
+ const tools = [
105
+ // 1. DOCUMENT
106
+ { name: 'word_get_text', description: 'Get full plain text of the active document. Use for overview; for structured content use get_paragraphs.', inputSchema: { type: 'object', properties: {} } },
107
+ { name: 'word_get_document_properties', description: 'Get all document metadata including title, author, path, changeTrackingMode, template, security, and timestamps.', inputSchema: { type: 'object', properties: {} } },
108
+ { name: 'word_set_document_properties', description: 'Set document metadata (title, subject, author, keywords, comments, category, company, manager, format).', inputSchema: { type: 'object', properties: { title: { type: 'string' }, subject: { type: 'string' }, author: { type: 'string' }, keywords: { type: 'string' }, comments: { type: 'string' }, category: { type: 'string' }, company: { type: 'string' }, manager: { type: 'string' }, format: { type: 'string' } } } },
109
+ { name: 'word_save', description: 'Save the document to disk.', inputSchema: { type: 'object', properties: {} } },
110
+ { name: 'word_get_word_count', description: 'Get word, character, and paragraph counts.', inputSchema: { type: 'object', properties: {} } },
111
+ { name: 'word_get_styles', description: 'Get available document styles.', inputSchema: { type: 'object', properties: {} } },
112
+ { name: 'word_get_coauthors', description: 'Get current co-authors and coauthoring status.', inputSchema: { type: 'object', properties: {} } },
113
+ { name: 'word_set_change_tracking', description: 'Set change tracking mode. Use TrackAll to show edits as tracked changes.', inputSchema: { type: 'object', properties: { mode: { type: 'string', enum: ['TrackAll', 'TrackMineOnly', 'Off'] } }, required: ['mode'] } },
114
+ // 2. PARAGRAPHS
115
+ { name: 'word_get_paragraphs', description: 'Get paragraphs with text, style, alignment, isTocEntry. Optional start/end index range for pagination.', inputSchema: { type: 'object', properties: { start: { type: 'number' }, end: { type: 'number' } } } },
116
+ { name: 'word_get_paragraph_by_index', description: 'Get full details of a single paragraph including font, spacing, indentation, and outline level.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
117
+ { name: 'word_insert_paragraph', description: 'Insert a styled paragraph at Start or End. Specify style (e.g. "Heading 1", "Heading 2", "Normal") and optional alignment (Left, Center, Right, Justified).', inputSchema: { type: 'object', properties: { text: { type: 'string' }, location: { type: 'string', enum: ['Start', 'End'] }, style: { type: 'string' }, alignment: { type: 'string', description: 'Paragraph alignment: Left, Center, Right, or Justified' } }, required: ['text'] } },
118
+ { name: 'word_delete_paragraph', description: 'Delete a paragraph by its index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
119
+ { name: 'word_set_paragraph_style', description: 'Change the style or alignment of a paragraph by index. Alignment accepts: Left, Center, Right, Justified.', inputSchema: { type: 'object', properties: { index: { type: 'number' }, style: { type: 'string' }, alignment: { type: 'string', description: 'Paragraph alignment: Left, Center, Right, or Justified' } }, required: ['index'] } },
120
+ { name: 'word_set_paragraph_spacing', description: 'Set line spacing (in points, e.g. 12=single for 12pt font, 24=double), before/after spacing (points), and indentation (points) on a paragraph by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' }, lineSpacing: { type: 'number', description: 'Line spacing in points (e.g. 12=single for 12pt, 18=1.5x, 24=double)' }, spaceBefore: { type: 'number', description: 'Space before paragraph in points' }, spaceAfter: { type: 'number', description: 'Space after paragraph in points' }, firstLineIndent: { type: 'number', description: 'First line indent in points' }, leftIndent: { type: 'number', description: 'Left indent in points' }, rightIndent: { type: 'number', description: 'Right indent in points' } }, required: ['index'] } },
121
+ // 3. SEARCH & TEXT
122
+ { name: 'word_search', description: 'Search for text in the document (case-insensitive by default). Returns match count and up to 30 matches with their text.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, matchCase: { type: 'boolean', description: 'Case-sensitive search. Default: false' }, matchWholeWord: { type: 'boolean' } }, required: ['query'] } },
123
+ { name: 'word_search_and_replace', description: 'Find and replace all occurrences of text (case-insensitive by default). Returns replacement count.', inputSchema: { type: 'object', properties: { find: { type: 'string' }, replace: { type: 'string' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false' }, matchWholeWord: { type: 'boolean' } }, required: ['find', 'replace'] } },
124
+ { name: 'word_insert_text', description: 'Insert text before or after a search match. Provide "after" OR "before" (not both) as the anchor string to locate.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, after: { type: 'string' }, before: { 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: ['text'] } },
125
+ { name: 'word_get_selection_info', description: 'Get the current selection text with full font and style details.', inputSchema: { type: 'object', properties: {} } },
126
+ { name: 'word_insert_text_at_selection', description: 'Insert text at the current cursor position, or replace the current selection (set replace=true).', inputSchema: { type: 'object', properties: { text: { type: 'string' }, replace: { type: 'boolean', description: 'Replace current selection instead of appending. Default: false' } }, required: ['text'] } },
127
+ { name: 'word_insert_line_break', description: 'Insert a soft line break (Shift+Enter) before or after a text match.', inputSchema: { type: 'object', properties: { anchorText: { type: 'string' }, before: { type: 'boolean' }, 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'] } },
128
+ // 4. FORMATTING
129
+ { name: 'word_format_text', description: 'Apply formatting (bold, italic, color, size, font) to a text match found by search.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, bold: { type: 'boolean' }, italic: { type: 'boolean' }, underline: { type: 'boolean' }, strikeThrough: { type: 'boolean' }, color: { type: 'string', description: 'Hex color e.g. #FF0000' }, highlightColor: { type: 'string' }, size: { type: 'number' }, name: { type: 'string', description: 'Font name' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['text'] } },
130
+ { name: 'word_clear_formatting', description: 'Clear direct formatting from a text match, reverting it to the paragraph style defaults.', inputSchema: { type: 'object', properties: { 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: ['text'] } },
131
+ { name: 'word_get_font_info', description: 'Inspect font properties (name, size, bold, italic, color) of a text match.', inputSchema: { type: 'object', properties: { 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: ['text'] } },
132
+ // 5. TABLES
133
+ { name: 'word_insert_table', description: 'Insert a table with data. Provide rows, cols, and data as array of arrays (e.g. [["A","B"],["C","D"]]).', inputSchema: { type: 'object', properties: { rows: { type: 'number' }, cols: { type: 'number' }, data: { type: 'array', items: { type: 'array', items: { type: 'string' } } }, location: { type: 'string', enum: ['Start', 'End'] }, style: { type: 'string' }, headerRowCount: { type: 'number' } }, required: ['rows', 'cols'] } },
134
+ { name: 'word_get_tables', description: 'Get all tables with row counts, styles, and cell values.', inputSchema: { type: 'object', properties: {} } },
135
+ { name: 'word_get_table_data', description: 'Get all cell values from a specific table by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
136
+ { name: 'word_set_table_cell', description: 'Set text in a specific table cell by tableIndex, row, and col.', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, row: { type: 'number' }, col: { type: 'number' }, text: { type: 'string' } }, required: ['tableIndex', 'row', 'col', 'text'] } },
137
+ { name: 'word_add_table_row', description: 'Add a row to a table with optional cell values.', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, values: { type: 'array', items: { type: 'string' } }, location: { type: 'string', enum: ['Start', 'End'] } }, required: ['tableIndex'] } },
138
+ { name: 'word_delete_table_row', description: 'Delete a row from a table by tableIndex and rowIndex.', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, rowIndex: { type: 'number' } }, required: ['tableIndex', 'rowIndex'] } },
139
+ { name: 'word_merge_table_cells', description: 'Merge a rectangular range of cells (topRow/firstCell to bottomRow/lastCell).', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, topRow: { type: 'number' }, firstCell: { type: 'number' }, bottomRow: { type: 'number' }, lastCell: { type: 'number' } }, required: ['tableIndex', 'topRow', 'firstCell', 'bottomRow', 'lastCell'] } },
140
+ { name: 'word_split_table_cell', description: 'Split a table cell into multiple rows/columns.', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, row: { type: 'number' }, col: { type: 'number' }, rowCount: { type: 'number' }, colCount: { type: 'number' } }, required: ['tableIndex', 'row', 'col'] } },
141
+ { name: 'word_set_table_style', description: 'Apply a built-in table style (e.g. "Grid Table 4 - Accent 1").', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, style: { type: 'string' } }, required: ['tableIndex', 'style'] } },
142
+ { name: 'word_set_table_cell_shading', description: 'Set background color on a table cell.', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, row: { type: 'number' }, col: { type: 'number' }, color: { type: 'string', description: 'Hex color e.g. #FFD700' } }, required: ['tableIndex', 'row', 'col', 'color'] } },
143
+ // 6. LISTS
144
+ { name: 'word_insert_list', description: 'Insert a bulleted or numbered list from an array of item strings.', inputSchema: { type: 'object', properties: { items: { type: 'array', items: { type: 'string' } }, numbered: { type: 'boolean' }, location: { type: 'string', enum: ['Start', 'End'] } }, required: ['items'] } },
145
+ { name: 'word_get_list_info', description: 'Get list formatting details (level, numbering) for a paragraph by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
146
+ { name: 'word_set_list_level', description: 'Set indent level of a list item (0=top, 1=sub-item, etc).', inputSchema: { type: 'object', properties: { index: { type: 'number' }, level: { type: 'number' } }, required: ['index', 'level'] } },
147
+ // 7. COMMENTS
148
+ { name: 'word_add_comment', description: 'Add a review comment anchored to a text match in the document.', inputSchema: { type: 'object', properties: { anchorText: { type: 'string' }, comment: { 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', 'comment'] } },
149
+ { name: 'word_get_comments', description: 'Get all comments with ID, author, content, date, and resolved status.', inputSchema: { type: 'object', properties: {} } },
150
+ { name: 'word_get_comment_replies', description: 'Get all replies for a specific comment by ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
151
+ { 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'] } },
152
+ { name: 'word_resolve_comment', description: 'Mark a comment as resolved by its ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
153
+ { name: 'word_delete_comment', description: 'Delete a comment and its replies by ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
154
+ // 8. FOOTNOTES & ENDNOTES
155
+ { 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'] } },
156
+ { 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'] } },
157
+ { name: 'word_insert_endnote', description: 'Insert an endnote 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'] } },
158
+ { name: 'word_get_footnotes', description: 'Get all footnotes with index and text content.', inputSchema: { type: 'object', properties: {} } },
159
+ { name: 'word_get_endnotes', description: 'Get all endnotes with index and text content.', inputSchema: { type: 'object', properties: {} } },
160
+ { name: 'word_delete_footnote', description: 'Delete a footnote by its index (0-based).', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
161
+ { name: 'word_delete_endnote', description: 'Delete an endnote by its index (0-based).', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
162
+ // 9. TRACK CHANGES
163
+ { name: 'word_get_tracked_changes', description: 'Get all tracked changes with index, type, author, date, and text.', inputSchema: { type: 'object', properties: {} } },
164
+ { name: 'word_accept_tracked_change', description: 'Accept a tracked change by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
165
+ { name: 'word_reject_tracked_change', description: 'Reject a tracked change by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
166
+ { name: 'word_accept_all_tracked_changes', description: 'Accept all tracked changes.', inputSchema: { type: 'object', properties: {} } },
167
+ { name: 'word_reject_all_tracked_changes', description: 'Reject all tracked changes.', inputSchema: { type: 'object', properties: {} } },
168
+ // 10. CONTENT CONTROLS
169
+ { name: 'word_get_content_controls', description: 'Get all content controls with id, tag, title, type, and text.', inputSchema: { type: 'object', properties: {} } },
170
+ { name: 'word_insert_content_control', description: 'Wrap a text match in a content control (RichText or PlainText preserve the anchor text; CheckBox REPLACES the anchor text with a checkbox widget).', inputSchema: { type: 'object', properties: { anchorText: { type: 'string' }, type: { type: 'string', enum: ['RichText', 'PlainText', 'CheckBox'] }, title: { type: 'string' }, tag: { type: 'string' }, color: { 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)' } } } },
171
+ { name: 'word_set_content_control_text', description: 'Set text in a content control by ID or tag.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, tag: { type: 'string' }, text: { type: 'string' } }, required: ['text'] } },
172
+ // 11. BOOKMARKS
173
+ { name: 'word_get_bookmarks', description: 'Get all bookmark names.', inputSchema: { type: 'object', properties: {} } },
174
+ { name: 'word_insert_bookmark', description: 'Insert a bookmark at anchor text.', inputSchema: { type: 'object', properties: { name: { type: 'string' }, anchorText: { 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: ['name', 'anchorText'] } },
175
+ { name: 'word_delete_bookmark', description: 'Delete a bookmark by name.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
176
+ { name: 'word_go_to_bookmark', description: 'Navigate to a bookmark and select its text range.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
177
+ { name: 'word_get_bookmark_text', description: 'Get the text content within a named bookmark.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
178
+ // 12. HYPERLINKS
179
+ { name: 'word_insert_hyperlink', description: 'Insert a hyperlink on existing text.', inputSchema: { type: 'object', properties: { anchorText: { type: 'string' }, url: { 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', 'url'] } },
180
+ { name: 'word_get_hyperlinks', description: 'List all hyperlinks with URL, display text, and whether they are internal (TOC) links.', inputSchema: { type: 'object', properties: {} } },
181
+ { name: 'word_remove_hyperlink', description: 'Remove a hyperlink from text (keeps the text, removes the link).', inputSchema: { type: 'object', properties: { anchorText: { 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'] } },
182
+ // 13. HEADERS & FOOTERS
183
+ { name: 'word_get_header_footer', description: 'Get header or footer text.', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['header', 'footer'] }, sectionIndex: { type: 'number' }, headerType: { type: 'string', enum: ['Primary', 'FirstPage', 'EvenPages'] } }, required: ['type'] } },
184
+ { name: 'word_set_header_footer', description: 'Set header or footer text.', inputSchema: { type: 'object', properties: { type: { type: 'string', enum: ['header', 'footer'] }, text: { type: 'string' }, sectionIndex: { type: 'number' }, headerType: { type: 'string', enum: ['Primary', 'FirstPage', 'EvenPages'] } }, required: ['type', 'text'] } },
185
+ // 14. IMAGES
186
+ { name: 'word_insert_image', description: 'Insert an image from base64 data.', inputSchema: { type: 'object', properties: { base64: { type: 'string', description: 'Base64-encoded image data' }, location: { type: 'string', enum: ['Start', 'End'] }, width: { type: 'number' }, height: { type: 'number' }, altText: { type: 'string' } }, required: ['base64'] } },
187
+ { name: 'word_get_images', description: 'List all inline images with dimensions, alt text, and hyperlinks.', inputSchema: { type: 'object', properties: {} } },
188
+ { name: 'word_delete_image', description: 'Delete an inline image by its index (0-based).', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
189
+ // 15. PAGE LAYOUT & SECTIONS
190
+ { name: 'word_get_page_layout', description: 'Get page layout (margins, orientation, paper size) for a section.', inputSchema: { type: 'object', properties: { sectionIndex: { type: 'number' } } } },
191
+ { name: 'word_set_page_layout', description: 'Set page margins (in points), orientation, or paper size for a section.', inputSchema: { type: 'object', properties: { orientation: { type: 'string', enum: ['Portrait', 'Landscape'] }, topMargin: { type: 'number' }, bottomMargin: { type: 'number' }, leftMargin: { type: 'number' }, rightMargin: { type: 'number' }, paperSize: { type: 'string', enum: ['Letter', 'A4', 'A3', 'Legal', 'Custom'] }, sectionIndex: { type: 'number' } } } },
192
+ { name: 'word_get_sections', description: 'List all sections with their page setup (margins, orientation, paper size).', inputSchema: { type: 'object', properties: {} } },
193
+ { name: 'word_insert_page_break', description: 'Insert a page break after a paragraph. Omit paragraphIndex for end of document.', inputSchema: { type: 'object', properties: { paragraphIndex: { type: 'number', description: 'Paragraph index to insert break after. Omit for end of document.' } } } },
194
+ { name: 'word_insert_section_break', description: 'Insert a section break after a paragraph.', inputSchema: { type: 'object', properties: { paragraphIndex: { type: 'number' }, breakType: { type: 'string', enum: ['SectionNext', 'SectionContinuous', 'SectionEven', 'SectionOdd'], description: 'Default: SectionNext' } } } },
195
+ // 16. CUSTOM PROPERTIES
196
+ { name: 'word_get_custom_properties', description: 'Get all custom document properties (key-value pairs with types).', inputSchema: { type: 'object', properties: {} } },
197
+ { name: 'word_set_custom_property', description: 'Set a custom document property. Creates or updates the key-value pair.', inputSchema: { type: 'object', properties: { key: { type: 'string' }, value: { type: 'string' } }, required: ['key', 'value'] } },
198
+ { name: 'word_delete_custom_property', description: 'Delete a custom document property by key.', inputSchema: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] } },
199
+ // 17. ADVANCED INSERTION & FIELDS
200
+ { name: 'word_insert_html', description: 'Insert HTML content that Word converts to native formatting. Supports headings, bold, italic, links, tables.', inputSchema: { type: 'object', properties: { html: { type: 'string' }, location: { type: 'string', enum: ['Start', 'End'] } }, required: ['html'] } },
201
+ { name: 'word_insert_ooxml', description: 'Insert raw Office Open XML for precise formatting control when HTML is insufficient.', inputSchema: { type: 'object', properties: { ooxml: { type: 'string' }, location: { type: 'string', enum: ['Start', 'End'] } }, required: ['ooxml'] } },
202
+ { name: 'word_insert_table_of_contents', description: 'Insert a table of contents based on heading styles.', inputSchema: { type: 'object', properties: { location: { type: 'string', enum: ['Start', 'End'] }, switches: { type: 'string' } } } },
203
+ { name: 'word_get_fields', description: 'Get all fields in the document (hyperlinks, TOC entries, page numbers, etc).', inputSchema: { type: 'object', properties: {} } },
204
+ ];
205
+
206
+ // ═══════════════════════════════════════════════════════════════════════════════
207
+ // PART 3: Tool → Action Mapping & MCP Server
208
+ // ═══════════════════════════════════════════════════════════════════════════════
209
+
210
+ const toolActionMap = {
211
+ word_get_text: 'getDocumentText', word_get_document_properties: 'getDocumentProperties',
212
+ word_set_document_properties: 'setDocumentProperties', word_save: 'saveDocument',
213
+ word_get_word_count: 'getWordCount', word_get_styles: 'getStyles',
214
+ word_get_coauthors: 'getCoauthors', word_set_change_tracking: 'setChangeTracking',
215
+ word_get_paragraphs: 'getParagraphs', word_get_paragraph_by_index: 'getParagraphByIndex',
216
+ word_insert_paragraph: 'insertParagraph', word_delete_paragraph: 'deleteParagraph',
217
+ word_set_paragraph_style: 'setParagraphStyle', word_set_paragraph_spacing: 'setParagraphSpacing',
218
+ word_search: 'search', word_search_and_replace: 'searchAndReplace',
219
+ word_insert_text: 'insertText', word_get_selection_info: 'getSelectionInfo',
220
+ word_insert_text_at_selection: 'insertTextAtSelection', word_insert_line_break: 'insertLineBreak',
221
+ word_format_text: 'formatRange', word_clear_formatting: 'clearFormatting',
222
+ word_get_font_info: 'getFontInfo',
223
+ word_insert_table: 'insertTable', word_get_tables: 'getTables',
224
+ word_get_table_data: 'getTableData', word_set_table_cell: 'setTableCell',
225
+ word_add_table_row: 'addTableRow', word_delete_table_row: 'deleteTableRow',
226
+ word_merge_table_cells: 'mergeTableCells', word_split_table_cell: 'splitTableCell',
227
+ word_set_table_style: 'setTableStyle', word_set_table_cell_shading: 'setTableCellShading',
228
+ word_insert_list: 'insertList', word_get_list_info: 'getListInfo', word_set_list_level: 'setListLevel',
229
+ word_add_comment: 'addComment', word_get_comments: 'getComments',
230
+ word_get_comment_replies: 'getCommentReplies', word_reply_to_comment: 'replyToComment',
231
+ word_resolve_comment: 'resolveComment', word_delete_comment: 'deleteComment',
232
+ word_insert_footnote: 'insertFootnote', word_insert_footnote_at_index: 'insertFootnoteAtIndex',
233
+ word_insert_endnote: 'insertEndnote', word_get_footnotes: 'getFootnotes',
234
+ word_get_endnotes: 'getEndnotes', word_delete_footnote: 'deleteFootnote', word_delete_endnote: 'deleteEndnote',
235
+ word_get_tracked_changes: 'getTrackedChanges', word_accept_tracked_change: 'acceptTrackedChange',
236
+ word_reject_tracked_change: 'rejectTrackedChange', word_accept_all_tracked_changes: 'acceptAllTrackedChanges',
237
+ word_reject_all_tracked_changes: 'rejectAllTrackedChanges',
238
+ word_get_content_controls: 'getContentControls', word_insert_content_control: 'insertContentControl',
239
+ word_set_content_control_text: 'setContentControlText',
240
+ word_get_bookmarks: 'getBookmarks', word_insert_bookmark: 'insertBookmark',
241
+ word_delete_bookmark: 'deleteBookmark', word_go_to_bookmark: 'goToBookmark',
242
+ word_get_bookmark_text: 'getBookmarkText',
243
+ word_insert_hyperlink: 'insertHyperlink', word_get_hyperlinks: 'getHyperlinks',
244
+ word_remove_hyperlink: 'removeHyperlink',
245
+ word_get_header_footer: 'getHeaderFooter', word_set_header_footer: 'setHeaderFooter',
246
+ word_insert_image: 'insertImage', word_get_images: 'getImages', word_delete_image: 'deleteImage',
247
+ word_get_page_layout: 'getPageLayout', word_set_page_layout: 'setPageLayout',
248
+ word_get_sections: 'getSections', word_insert_page_break: 'insertPageBreak',
249
+ word_insert_section_break: 'insertSectionBreak',
250
+ word_get_custom_properties: 'getCustomProperties', word_set_custom_property: 'setCustomProperty',
251
+ word_delete_custom_property: 'deleteCustomProperty',
252
+ word_insert_html: 'insertHtml', word_insert_ooxml: 'insertOoxml',
253
+ word_insert_table_of_contents: 'insertTableOfContents', word_get_fields: 'getFields',
254
+ };
255
+
256
+ const mcpServer = new Server(
257
+ { name: 'mcp-word-bridge', version: '3.2.0' },
258
+ { capabilities: { tools: {} } }
259
+ );
260
+
261
+ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
262
+
263
+ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
264
+ const { name, arguments: args } = request.params;
265
+ const action = toolActionMap[name];
266
+ if (!action) return { content: [{ type: 'text', text: 'Unknown tool: ' + name }], isError: true };
267
+ try {
268
+ const result = await sendToTaskpane(action, args || {});
269
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
270
+ } catch (e) {
271
+ return { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true };
272
+ }
273
+ });
274
+
275
+ // ═══════════════════════════════════════════════════════════════════════════════
276
+ // PART 4: Startup & Shutdown
277
+ // ═══════════════════════════════════════════════════════════════════════════════
278
+
279
+ function shutdown() {
280
+ process.stderr.write('[mcp-word-bridge] Shutting down...\n');
281
+ httpsServer.close();
282
+ process.exit(0);
283
+ }
284
+
285
+ process.on('SIGTERM', shutdown);
286
+ process.on('SIGINT', shutdown);
287
+
288
+ async function main() {
289
+ // Start HTTPS bridge server
290
+ await new Promise((resolve, reject) => {
291
+ httpsServer.on('error', (err) => {
292
+ if (err.code === 'EADDRINUSE') {
293
+ reject(new Error('Port ' + PORT + ' is already in use. Stop the other process or set MCP_WORD_BRIDGE_PORT env var.'));
294
+ } else {
295
+ reject(err);
296
+ }
297
+ });
298
+ httpsServer.listen(PORT, () => {
299
+ process.stderr.write('[mcp-word-bridge] Bridge server listening on https://localhost:' + PORT + '\n');
300
+ resolve();
301
+ });
302
+ });
303
+
304
+ // Start MCP server on stdio
305
+ const transport = new StdioServerTransport();
306
+ await mcpServer.connect(transport);
307
+ process.stderr.write('[mcp-word-bridge] MCP server ready (' + tools.length + ' tools)\n');
308
+ }
309
+
310
+ main().catch(e => {
311
+ process.stderr.write('[mcp-word-bridge] Fatal: ' + e.message + '\n');
312
+ process.exit(1);
313
+ });
package/manifest.xml ADDED
@@ -0,0 +1,21 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:type="TaskPaneApp">
5
+ <Id>a1b2c3d4-e5f6-7890-abcd-ef1234567890</Id>
6
+ <Version>1.0.0</Version>
7
+ <ProviderName>Leonid Mokrushin</ProviderName>
8
+ <DefaultLocale>en-US</DefaultLocale>
9
+ <DisplayName DefaultValue="MCP Word Bridge"/>
10
+ <Description DefaultValue="MCP server bridge for live Word document editing"/>
11
+ <IconUrl DefaultValue="https://localhost:3000/icon-32.png"/>
12
+ <HighResolutionIconUrl DefaultValue="https://localhost:3000/icon-64.png"/>
13
+ <SupportUrl DefaultValue="https://localhost:3000"/>
14
+ <Hosts>
15
+ <Host Name="Document"/>
16
+ </Hosts>
17
+ <DefaultSettings>
18
+ <SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
19
+ </DefaultSettings>
20
+ <Permissions>ReadWriteDocument</Permissions>
21
+ </OfficeApp>
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "mcp-word-bridge",
3
+ "version": "3.2.0",
4
+ "description": "MCP server for live Word document editing via Office Add-in",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "mcp-word-bridge": "index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": ["mcp", "word", "office", "add-in", "document", "editing"],
13
+ "author": "Leonid Mokrushin <likelion@gmail.com>",
14
+ "license": "MIT",
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.29.0",
17
+ "ws": "^8.16.0"
18
+ }
19
+ }