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 +42 -3
- package/index.js +34 -250
- package/install-manifest.js +1 -1
- package/lib/equations.js +278 -0
- package/lib/tools.js +163 -0
- package/lib/usage-guide.js +106 -0
- package/package.json +24 -3
- package/taskpane-app.js +79 -14
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# MCP Word Bridge
|
|
2
2
|
|
|
3
|
+
[](https://github.com/likelion/mcp-word-bridge/actions/workflows/tests.yml)
|
|
4
|
+
[](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 #
|
|
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 (
|
|
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 (
|
|
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.
|
|
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: [
|
package/install-manifest.js
CHANGED
|
@@ -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 (
|
|
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);
|