mcp-word-bridge 3.3.0 → 3.4.1

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
@@ -1,5 +1,8 @@
1
1
  # MCP Word Bridge
2
2
 
3
+ [![Tests](https://github.com/likelion/mcp-word-bridge/actions/workflows/tests.yml/badge.svg)](https://github.com/likelion/mcp-word-bridge/actions/workflows/tests.yml)
4
+ [![codecov](https://codecov.io/gh/likelion/mcp-word-bridge/branch/main/graph/badge.svg)](https://codecov.io/gh/likelion/mcp-word-bridge)
5
+
3
6
  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
7
 
5
8
  ## Architecture
@@ -53,20 +56,31 @@ That's it. The MCP server starts automatically when your MCP client loads the co
53
56
 
54
57
  ```
55
58
  mcp-word-bridge/
56
- ├── index.js # Single entry point (MCP + bridge server)
59
+ ├── index.js # Entry point: HTTPS server + MCP handlers + WebSocket relay
60
+ ├── lib/
61
+ │ ├── tools.js # Tool definitions (87) and action mapping
62
+ │ ├── equations.js # LaTeX→OMML pipeline (fixDelimiters, fixNaryOperands, latexToOmml)
63
+ │ └── usage-guide.js # MCP resource content (usage patterns for LLMs)
64
+ ├── taskpane-app.js # Client-side Word JS API logic (runs in add-in)
57
65
  ├── install-manifest.js # Manifest sideloader (npx mcp-word-bridge-install)
58
66
  ├── taskpane.html # Served to Word add-in
59
- ├── taskpane-app.js # Client-side Word JS API logic
60
67
  ├── certs/
61
68
  │ ├── cert.pem # Self-signed TLS cert
62
69
  │ ├── key.pem # TLS private key
63
70
  │ └── cert.conf # OpenSSL config for cert regeneration
64
71
  ├── manifest.xml # Office add-in manifest
72
+ ├── test/
73
+ │ ├── mcp-protocol.test.js # Tool schema & mapping tests (CI)
74
+ │ ├── equations.test.js # LaTeX→OMML pipeline tests (CI)
75
+ │ ├── validation.test.js # Input validation tests (CI)
76
+ │ └── live/ # Integration tests against real Word (manual)
77
+ │ ├── run-all.test.js
78
+ │ └── bridge-client.js
65
79
  ├── package.json
66
80
  └── README.md
67
81
  ```
68
82
 
69
- ## Tools (84)
83
+ ## Tools (87)
70
84
 
71
85
  ### Document
72
86
  | Tool | Description |
@@ -75,6 +89,8 @@ mcp-word-bridge/
75
89
  | `word_get_document_properties` | Get metadata (title, author, path, timestamps) |
76
90
  | `word_set_document_properties` | Set metadata fields |
77
91
  | `word_save` | Save document to disk |
92
+ | `word_clear` | Clear all document body content. Leaves one empty Normal paragraph. |
93
+ | `word_create_document` | Create and open a new blank document in a new Word window |
78
94
  | `word_get_word_count` | Get word, character, and paragraph counts |
79
95
  | `word_get_styles` | List available styles |
80
96
  | `word_get_coauthors` | Get co-authoring status and active authors |
@@ -220,6 +236,11 @@ mcp-word-bridge/
220
236
  | `word_insert_table_of_contents` | Insert a TOC based on headings |
221
237
  | `word_get_fields` | Get all fields (hyperlinks, TOC, page numbers) |
222
238
 
239
+ ### Equations
240
+ | Tool | Description |
241
+ |------|-------------|
242
+ | `word_insert_equation` | Insert a LaTeX equation as a native editable Word equation. Supports display (centered block) and inline modes. |
243
+
223
244
  ## How it works
224
245
 
225
246
  1. The MCP client spawns `index.js` via stdio
@@ -229,6 +250,24 @@ mcp-word-bridge/
229
250
  5. Changes go through Word's editing pipeline — cursor follows edits like a human typing
230
251
  6. When the MCP client terminates the process, the bridge server stops automatically
231
252
 
253
+ ## Resources
254
+
255
+ The server exposes an MCP resource at `word-bridge://usage-guide` containing patterns, workflows, and pitfalls for LLMs operating on Word documents. MCP clients that support resources can read this for context on how to use the tools effectively.
256
+
257
+ ## Equations
258
+
259
+ `word_insert_equation` converts LaTeX math to native Word equations via:
260
+
261
+ ```
262
+ LaTeX → temml → MathML → mathml2omml → OMML → OOXML → Word
263
+ ```
264
+
265
+ - **Display mode** (`displayMode: true`, default): centered block equation on its own line
266
+ - **Inline mode** (`displayMode: false`): inserted at the current cursor position within a paragraph
267
+ - Supports: fractions, roots, integrals, sums, products, matrices, Greek letters, piecewise functions, aligned systems, and all standard LaTeX math
268
+ - Equations are fully editable in Word's built-in equation editor
269
+ - Invalid LaTeX returns a descriptive error message
270
+
232
271
  ## TLS Certificate
233
272
 
234
273
  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:
package/index.js CHANGED
@@ -3,8 +3,6 @@
3
3
  * MCP Word Bridge — Unified Server
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
- *
7
- * v3.3.0 — 84 tools
8
6
  */
9
7
  const https = require('https');
10
8
  const fs = require('fs');
@@ -14,6 +12,10 @@ const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
14
12
  const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
15
13
  const { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } = require('@modelcontextprotocol/sdk/types.js');
16
14
 
15
+ const { latexToOmml, buildEquationOoxml } = require('./lib/equations.js');
16
+ const { tools, toolActionMap } = require('./lib/tools.js');
17
+ const USAGE_GUIDE = require('./lib/usage-guide.js');
18
+
17
19
  const PORT = parseInt(process.env.MCP_WORD_BRIDGE_PORT || '3000', 10);
18
20
  const CERTS_DIR = path.join(__dirname, 'certs');
19
21
 
@@ -101,7 +103,7 @@ wss.on('connection', (ws, req) => {
101
103
  const pending = bridgePending.get(msg.id);
102
104
  if (pending) { pending.resolve(msg); bridgePending.delete(msg.id); }
103
105
  }
104
- } catch (e) {}
106
+ } catch (_e) {}
105
107
  });
106
108
  ws.on('close', () => { process.stderr.write('[bridge] Taskpane disconnected\n'); taskpaneSocket = null; });
107
109
  }
@@ -131,167 +133,8 @@ function sendToTaskpane(action, params) {
131
133
  });
132
134
  }
133
135
 
134
- // ═══════════════════════════════════════════════════════════════════════════════
135
- // PART 2: MCP Tool Definitions
136
- // ═══════════════════════════════════════════════════════════════════════════════
137
-
138
- const tools = [
139
- // 1. DOCUMENT
140
- { 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: {} } },
141
- { name: 'word_get_document_properties', description: 'Get all document metadata including title, author, path, changeTrackingMode, template, security, and timestamps.', inputSchema: { type: 'object', properties: {} } },
142
- { 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' } } } },
143
- { name: 'word_save', description: 'Save the document to disk.', inputSchema: { type: 'object', properties: {} } },
144
- { name: 'word_get_word_count', description: 'Get word, character, and paragraph counts.', inputSchema: { type: 'object', properties: {} } },
145
- { name: 'word_get_styles', description: 'Get available document styles.', inputSchema: { type: 'object', properties: {} } },
146
- { name: 'word_get_coauthors', description: 'Get current co-authors and coauthoring status.', inputSchema: { type: 'object', properties: {} } },
147
- { 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'] } },
148
- // 2. PARAGRAPHS
149
- { 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' } } } },
150
- { 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'] } },
151
- { 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'] } },
152
- { name: 'word_delete_paragraph', description: 'Delete a paragraph by its index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
153
- { 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'] } },
154
- { 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'] } },
155
- // 3. SEARCH & TEXT
156
- { 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'] } },
157
- { 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'] } },
158
- { 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'] } },
159
- { name: 'word_get_selection_info', description: 'Get the current selection text with full font and style details.', inputSchema: { type: 'object', properties: {} } },
160
- { 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'] } },
161
- { 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'] } },
162
- // 4. FORMATTING
163
- { 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'] } },
164
- { 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'] } },
165
- { 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'] } },
166
- // 5. TABLES
167
- { 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'] } },
168
- { name: 'word_get_tables', description: 'Get all tables with row counts, styles, and cell values.', inputSchema: { type: 'object', properties: {} } },
169
- { 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'] } },
170
- { 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'] } },
171
- { 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'] } },
172
- { 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'] } },
173
- { 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'] } },
174
- { 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'] } },
175
- { 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'] } },
176
- { 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'] } },
177
- // 6. LISTS
178
- { 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'] } },
179
- { 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'] } },
180
- { 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'] } },
181
- // 7. COMMENTS
182
- { 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'] } },
183
- { name: 'word_get_comments', description: 'Get all comments with ID, author, content, date, and resolved status.', inputSchema: { type: 'object', properties: {} } },
184
- { name: 'word_get_comment_replies', description: 'Get all replies for a specific comment by ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
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
- { name: 'word_resolve_comment', description: 'Mark a comment as resolved by its ID.', inputSchema: { type: 'object', properties: { commentId: { type: 'string' } }, required: ['commentId'] } },
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: {} } },
190
- // 8. FOOTNOTES & ENDNOTES
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'] } },
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'] } },
193
- { 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'] } },
194
- { name: 'word_get_footnotes', description: 'Get all footnotes with index and text content.', inputSchema: { type: 'object', properties: {} } },
195
- { name: 'word_get_endnotes', description: 'Get all endnotes with index and text content.', inputSchema: { type: 'object', properties: {} } },
196
- { name: 'word_delete_footnote', description: 'Delete a footnote by its index (0-based).', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
197
- { name: 'word_delete_endnote', description: 'Delete an endnote by its index (0-based).', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
198
- // 9. TRACK CHANGES
199
- { name: 'word_get_tracked_changes', description: 'Get all tracked changes with index, type, author, date, and text.', inputSchema: { type: 'object', properties: {} } },
200
- { name: 'word_accept_tracked_change', description: 'Accept a tracked change by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
201
- { name: 'word_reject_tracked_change', description: 'Reject a tracked change by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
202
- { name: 'word_accept_all_tracked_changes', description: 'Accept all tracked changes.', inputSchema: { type: 'object', properties: {} } },
203
- { name: 'word_reject_all_tracked_changes', description: 'Reject all tracked changes.', inputSchema: { type: 'object', properties: {} } },
204
- // 10. CONTENT CONTROLS
205
- { name: 'word_get_content_controls', description: 'Get all content controls with id, tag, title, type, and text.', inputSchema: { type: 'object', properties: {} } },
206
- { 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)' } } } },
207
- { 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'] } },
208
- // 11. BOOKMARKS
209
- { name: 'word_get_bookmarks', description: 'Get all bookmark names.', inputSchema: { type: 'object', properties: {} } },
210
- { 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'] } },
211
- { name: 'word_delete_bookmark', description: 'Delete a bookmark by name.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
212
- { name: 'word_go_to_bookmark', description: 'Navigate to a bookmark and select its text range.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
213
- { name: 'word_get_bookmark_text', description: 'Get the text content within a named bookmark.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
214
- // 12. HYPERLINKS
215
- { 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'] } },
216
- { name: 'word_get_hyperlinks', description: 'List all hyperlinks with URL, display text, and whether they are internal (TOC) links.', inputSchema: { type: 'object', properties: {} } },
217
- { 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'] } },
218
- // 13. HEADERS & FOOTERS
219
- { 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'] } },
220
- { 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'] } },
221
- // 14. IMAGES
222
- { 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'] } },
223
- { name: 'word_get_images', description: 'List all inline images with dimensions, alt text, and hyperlinks.', inputSchema: { type: 'object', properties: {} } },
224
- { name: 'word_delete_image', description: 'Delete an inline image by its index (0-based).', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
225
- // 15. PAGE LAYOUT & SECTIONS
226
- { name: 'word_get_page_layout', description: 'Get page layout (margins, orientation, paper size) for a section.', inputSchema: { type: 'object', properties: { sectionIndex: { type: 'number' } } } },
227
- { 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' } } } },
228
- { name: 'word_get_sections', description: 'List all sections with their page setup (margins, orientation, paper size).', inputSchema: { type: 'object', properties: {} } },
229
- { 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.' } } } },
230
- { 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' } } } },
231
- // 16. CUSTOM PROPERTIES
232
- { name: 'word_get_custom_properties', description: 'Get all custom document properties (key-value pairs with types).', inputSchema: { type: 'object', properties: {} } },
233
- { 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'] } },
234
- { name: 'word_delete_custom_property', description: 'Delete a custom document property by key.', inputSchema: { type: 'object', properties: { key: { type: 'string' } }, required: ['key'] } },
235
- // 17. ADVANCED INSERTION & FIELDS
236
- { 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'] } },
237
- { 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'] } },
238
- { 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' } } } },
239
- { name: 'word_get_fields', description: 'Get all fields in the document (hyperlinks, TOC entries, page numbers, etc).', inputSchema: { type: 'object', properties: {} } },
240
- ];
241
-
242
- // ═══════════════════════════════════════════════════════════════════════════════
243
- // PART 3: Tool → Action Mapping & MCP Server
244
- // ═══════════════════════════════════════════════════════════════════════════════
245
-
246
- const toolActionMap = {
247
- word_get_text: 'getDocumentText', word_get_document_properties: 'getDocumentProperties',
248
- word_set_document_properties: 'setDocumentProperties', word_save: 'saveDocument',
249
- word_get_word_count: 'getWordCount', word_get_styles: 'getStyles',
250
- word_get_coauthors: 'getCoauthors', word_set_change_tracking: 'setChangeTracking',
251
- word_get_paragraphs: 'getParagraphs', word_get_paragraph_by_index: 'getParagraphByIndex',
252
- word_insert_paragraph: 'insertParagraph', word_delete_paragraph: 'deleteParagraph',
253
- word_set_paragraph_style: 'setParagraphStyle', word_set_paragraph_spacing: 'setParagraphSpacing',
254
- word_search: 'search', word_search_and_replace: 'searchAndReplace',
255
- word_insert_text: 'insertText', word_get_selection_info: 'getSelectionInfo',
256
- word_insert_text_at_selection: 'insertTextAtSelection', word_insert_line_break: 'insertLineBreak',
257
- word_format_text: 'formatRange', word_clear_formatting: 'clearFormatting',
258
- word_get_font_info: 'getFontInfo',
259
- word_insert_table: 'insertTable', word_get_tables: 'getTables',
260
- word_get_table_data: 'getTableData', word_set_table_cell: 'setTableCell',
261
- word_add_table_row: 'addTableRow', word_delete_table_row: 'deleteTableRow',
262
- word_merge_table_cells: 'mergeTableCells', word_split_table_cell: 'splitTableCell',
263
- word_set_table_style: 'setTableStyle', word_set_table_cell_shading: 'setTableCellShading',
264
- word_insert_list: 'insertList', word_get_list_info: 'getListInfo', word_set_list_level: 'setListLevel',
265
- word_add_comment: 'addComment', word_get_comments: 'getComments',
266
- word_get_comment_replies: 'getCommentReplies', word_reply_to_comment: 'replyToComment',
267
- word_resolve_comment: 'resolveComment', word_delete_comment: 'deleteComment',
268
- word_get_comment_anchor: 'getCommentAnchor', word_get_comments_with_anchor: 'getCommentsWithAnchor',
269
- word_insert_footnote: 'insertFootnote', word_insert_footnote_at_index: 'insertFootnoteAtIndex',
270
- word_insert_endnote: 'insertEndnote', word_get_footnotes: 'getFootnotes',
271
- word_get_endnotes: 'getEndnotes', word_delete_footnote: 'deleteFootnote', word_delete_endnote: 'deleteEndnote',
272
- word_get_tracked_changes: 'getTrackedChanges', word_accept_tracked_change: 'acceptTrackedChange',
273
- word_reject_tracked_change: 'rejectTrackedChange', word_accept_all_tracked_changes: 'acceptAllTrackedChanges',
274
- word_reject_all_tracked_changes: 'rejectAllTrackedChanges',
275
- word_get_content_controls: 'getContentControls', word_insert_content_control: 'insertContentControl',
276
- word_set_content_control_text: 'setContentControlText',
277
- word_get_bookmarks: 'getBookmarks', word_insert_bookmark: 'insertBookmark',
278
- word_delete_bookmark: 'deleteBookmark', word_go_to_bookmark: 'goToBookmark',
279
- word_get_bookmark_text: 'getBookmarkText',
280
- word_insert_hyperlink: 'insertHyperlink', word_get_hyperlinks: 'getHyperlinks',
281
- word_remove_hyperlink: 'removeHyperlink',
282
- word_get_header_footer: 'getHeaderFooter', word_set_header_footer: 'setHeaderFooter',
283
- word_insert_image: 'insertImage', word_get_images: 'getImages', word_delete_image: 'deleteImage',
284
- word_get_page_layout: 'getPageLayout', word_set_page_layout: 'setPageLayout',
285
- word_get_sections: 'getSections', word_insert_page_break: 'insertPageBreak',
286
- word_insert_section_break: 'insertSectionBreak',
287
- word_get_custom_properties: 'getCustomProperties', word_set_custom_property: 'setCustomProperty',
288
- word_delete_custom_property: 'deleteCustomProperty',
289
- word_insert_html: 'insertHtml', word_insert_ooxml: 'insertOoxml',
290
- word_insert_table_of_contents: 'insertTableOfContents', word_get_fields: 'getFields',
291
- };
292
-
293
136
  const mcpServer = new Server(
294
- { name: 'mcp-word-bridge', version: '3.3.0' },
137
+ { name: 'mcp-word-bridge', version: '3.4.1' },
295
138
  { capabilities: { tools: {}, resources: {} } }
296
139
  );
297
140
 
@@ -299,6 +142,34 @@ mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
299
142
 
300
143
  mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
301
144
  const { name, arguments: args } = request.params;
145
+
146
+ // Special handling for word_insert_equation (server-side LaTeX→OMML conversion)
147
+ if (name === 'word_insert_equation') {
148
+ try {
149
+ const latex = args.latex;
150
+ const displayMode = args.displayMode !== false;
151
+
152
+ // Convert LaTeX → OMML via lib/equations.js
153
+ let result;
154
+ try {
155
+ const { mml2omml } = require('mathml2omml');
156
+ result = latexToOmml(latex, displayMode, mml2omml);
157
+ } catch (e) {
158
+ return { content: [{ type: 'text', text: e.message.startsWith('LaTeX parse error') || e.message.startsWith('"latex"') ? e.message : 'Error: ' + e.message }], isError: true };
159
+ }
160
+
161
+ const cleanOmml = result.omml;
162
+ const ooxml = buildEquationOoxml(cleanOmml, displayMode);
163
+
164
+ const action = displayMode ? 'insertOoxml' : 'insertOoxmlAtSelection';
165
+ const params = displayMode ? { ooxml, location: args.location || 'End' } : { ooxml };
166
+ await sendToTaskpane(action, params);
167
+ return { content: [{ type: 'text', text: JSON.stringify({ success: true, displayMode, latex }) }] };
168
+ } catch (e) {
169
+ return { content: [{ type: 'text', text: 'Error: ' + e.message }], isError: true };
170
+ }
171
+ }
172
+
302
173
  const action = toolActionMap[name];
303
174
  if (!action) return { content: [{ type: 'text', text: 'Unknown tool: ' + name }], isError: true };
304
175
  try {
@@ -311,93 +182,6 @@ mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
311
182
 
312
183
  // ═══════════════════════════════════════════════════════════════════════════════
313
184
  // 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
185
 
402
186
  mcpServer.setRequestHandler(ListResourcesRequestSchema, async () => ({
403
187
  resources: [
@@ -30,7 +30,7 @@ if (process.platform === 'darwin') {
30
30
  fs.copyFileSync(manifestSrc, dest);
31
31
  console.log('✓ Manifest installed to: ' + dest);
32
32
  console.log(' Restart Word, then: Insert → My Add-ins → Developer Add-ins');
33
- } catch (e) {
33
+ } catch (_e) {
34
34
  console.log('Could not auto-install manifest on Windows.');
35
35
  console.log('Manual steps:');
36
36
  console.log(' 1. Copy this file to a local folder: ' + manifestSrc);