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 +181 -0
- package/certs/cert.conf +17 -0
- package/certs/cert.pem +19 -0
- package/certs/key.pem +28 -0
- package/index.js +313 -0
- package/manifest.xml +21 -0
- package/package.json +19 -0
- package/taskpane-app.js +1329 -0
- package/taskpane.html +50 -0
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`.
|
package/certs/cert.conf
ADDED
|
@@ -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
|
+
}
|