mcp-word-bridge 4.0.5 → 4.1.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 +13 -0
- package/dist/server.js +80 -35
- package/dist/server.js.map +4 -4
- package/dist/taskpane-app.js +118 -19
- package/dist/taskpane-app.js.map +3 -3
- package/manifest.xml +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -289,6 +289,19 @@ src/
|
|
|
289
289
|
└── equations.ts # LaTeX→OMML conversion pipeline
|
|
290
290
|
```
|
|
291
291
|
|
|
292
|
+
## Known Limitations
|
|
293
|
+
|
|
294
|
+
Platform constraints in the Word JavaScript API that cannot be resolved in this project:
|
|
295
|
+
|
|
296
|
+
| Area | Limitation | Upstream Issue |
|
|
297
|
+
|------|------------|----------------|
|
|
298
|
+
| Tracked Changes | `word_get_tracked_changes` returns empty `text` for deletions. The `Word.TrackedChange.text` property returns `""` when `type` is `"Deleted"`. Only additions and formatting changes include text content. | [office-js#5188](https://github.com/OfficeDev/office-js/issues/5188) |
|
|
299
|
+
| Tracked Changes | `getTrackedChanges()` throws if the document contains "moved" tracked changes (drag-and-drop reorders). These produce paired "moved from"/"moved to" entries that the API cannot handle. | [office-js#5535](https://github.com/OfficeDev/office-js/issues/5535) |
|
|
300
|
+
| Highlight Colors | `highlightColor` only supports 17 named colors (Yellow, Green, Cyan, Magenta, Blue, Red, DarkBlue, DarkCyan, DarkGreen, DarkMagenta, DarkRed, DarkYellow, Gray25, Gray50, Black, White, NoHighlight). The Word API documentation claims `#RRGGBB` is accepted, but Desktop silently maps hex values to the nearest named color. This tool rejects hex to prevent silent mismatch — use `color` for arbitrary RGB. | [office-js#4638](https://github.com/OfficeDev/office-js/issues/4638) |
|
|
301
|
+
| Paragraph Style | TOC field entries and certain table paragraphs return `style: ""` (empty string) instead of the actual style name. Paragraphs are flagged with `isTocEntry: true` when detected as TOC entries. Use this flag rather than the style field for identification. | [office-js#5934](https://github.com/OfficeDev/office-js/issues/5934) |
|
|
302
|
+
| Page Info | `word_get_page_info` requires WordApiDesktop 1.2+ (Word for Windows/Mac desktop). Not available on Word for the web. | |
|
|
303
|
+
| Undo | The Word JavaScript API does not expose a programmatic `undo()` method. Changes made by the add-in appear in Word's undo stack (Ctrl+Z works for the user), but there is no way to trigger undo from code. | |
|
|
304
|
+
|
|
292
305
|
## License
|
|
293
306
|
|
|
294
307
|
MIT
|
package/dist/server.js
CHANGED
|
@@ -18863,15 +18863,6 @@ var ToolError = class extends Error {
|
|
|
18863
18863
|
};
|
|
18864
18864
|
|
|
18865
18865
|
// src/server/validation.ts
|
|
18866
|
-
var WORD_SPECIAL_CODES = /\^(p|w|t|l|m|b|n|s|d|a|e|f|g|v|~|\^|\-|13|11|14|12|07|09|30|31|32|34|36|37|38|39|40|41|42|43|44|45|46|47|92|94|127|129|130|131|132|133|134|135|136|137|138|139|140|141|142|143|144|145|146|147|148|149|150|151|152|153|154|155|156|157|158|159|160|161|162|163|164|165|166|167|168|169|170|171|172|173|174|175|176|177|178|179|180|181|182|183|184|185|186|187|188|189|190|191|192|193|194|195|196|197|198|199|200|201|202|203|204|205|206|207|208|209|210|211|212|213|214|215|216|217|218|219|220|221|222|223|224|225|226|227|228|229|230|231|232|233|234|235|236|237|238|239|240|241|242|243|244|245|246|247|248|249|250|251|252|253|254|255)/;
|
|
18867
|
-
function checkNoSpecialCodes(text2, paramName) {
|
|
18868
|
-
const match = text2.match(WORD_SPECIAL_CODES);
|
|
18869
|
-
if (match) {
|
|
18870
|
-
throw new ToolError(
|
|
18871
|
-
`${paramName} contains Word special code "${match[0]}" which can corrupt document structure. Use literal text only. Common special codes: ^p (paragraph mark), ^t (tab), ^w (whitespace), ^13 (paragraph mark).`
|
|
18872
|
-
);
|
|
18873
|
-
}
|
|
18874
|
-
}
|
|
18875
18866
|
function checkNonEmpty(value, name) {
|
|
18876
18867
|
if (!value || typeof value !== "string" || value.trim() === "") {
|
|
18877
18868
|
throw new ToolError(`${name} must be a non-empty string.`);
|
|
@@ -18910,6 +18901,24 @@ function checkSpacingBounds(value, name) {
|
|
|
18910
18901
|
);
|
|
18911
18902
|
}
|
|
18912
18903
|
}
|
|
18904
|
+
function checkIndentBounds(value, name) {
|
|
18905
|
+
if (name === "firstLineIndent") {
|
|
18906
|
+
if (value < -MAX_SPACING_POINTS || value > MAX_SPACING_POINTS) {
|
|
18907
|
+
throw new ToolError(
|
|
18908
|
+
`${name} value ${value} is out of range. Valid range: -${MAX_SPACING_POINTS} to ${MAX_SPACING_POINTS} points.`
|
|
18909
|
+
);
|
|
18910
|
+
}
|
|
18911
|
+
} else {
|
|
18912
|
+
if (value < 0) {
|
|
18913
|
+
throw new ToolError(`${name} must be non-negative (in points).`);
|
|
18914
|
+
}
|
|
18915
|
+
if (value > MAX_SPACING_POINTS) {
|
|
18916
|
+
throw new ToolError(
|
|
18917
|
+
`${name} value ${value} exceeds maximum (${MAX_SPACING_POINTS} points = 22 inches).`
|
|
18918
|
+
);
|
|
18919
|
+
}
|
|
18920
|
+
}
|
|
18921
|
+
}
|
|
18913
18922
|
function checkPropertyKeyLength(key) {
|
|
18914
18923
|
if (key.length > MAX_CUSTOM_PROPERTY_KEY_LENGTH) {
|
|
18915
18924
|
throw new ToolError(
|
|
@@ -18972,7 +18981,7 @@ var insertParagraphAtIndex = forwardTool(
|
|
|
18972
18981
|
);
|
|
18973
18982
|
var deleteParagraph = forwardTool(
|
|
18974
18983
|
"word_delete_paragraph",
|
|
18975
|
-
"[Paragraphs] Delete a paragraph by its 0-based index.",
|
|
18984
|
+
"[Paragraphs] Delete a paragraph by its 0-based index. For table cells with multiple paragraphs, extra paragraphs can be removed (the last paragraph in a cell cannot be deleted).",
|
|
18976
18985
|
{
|
|
18977
18986
|
properties: {
|
|
18978
18987
|
index: { type: "number", description: "Paragraph index (0-based)" }
|
|
@@ -19027,14 +19036,21 @@ var setParagraphSpacing = {
|
|
|
19027
19036
|
const spacingFields = [
|
|
19028
19037
|
["lineSpacing", args.lineSpacing],
|
|
19029
19038
|
["spaceBefore", args.spaceBefore],
|
|
19030
|
-
["spaceAfter", args.spaceAfter]
|
|
19039
|
+
["spaceAfter", args.spaceAfter]
|
|
19040
|
+
];
|
|
19041
|
+
for (const [name, value] of spacingFields) {
|
|
19042
|
+
if (value !== void 0 && typeof value === "number") {
|
|
19043
|
+
checkSpacingBounds(value, name);
|
|
19044
|
+
}
|
|
19045
|
+
}
|
|
19046
|
+
const indentFields = [
|
|
19031
19047
|
["firstLineIndent", args.firstLineIndent],
|
|
19032
19048
|
["leftIndent", args.leftIndent],
|
|
19033
19049
|
["rightIndent", args.rightIndent]
|
|
19034
19050
|
];
|
|
19035
|
-
for (const [name, value] of
|
|
19051
|
+
for (const [name, value] of indentFields) {
|
|
19036
19052
|
if (value !== void 0 && typeof value === "number") {
|
|
19037
|
-
|
|
19053
|
+
checkIndentBounds(value, name);
|
|
19038
19054
|
}
|
|
19039
19055
|
}
|
|
19040
19056
|
const result = await bridge2.send("setParagraphSpacing", args);
|
|
@@ -19067,7 +19083,7 @@ var moveParagraph = {
|
|
|
19067
19083
|
throw new ToolError(`toIndex (${toIndex}) is inside the source range [${fromIndex}, ${fromIndex + count - 1}]. Move to a position outside the range.`);
|
|
19068
19084
|
}
|
|
19069
19085
|
const paraCount = await bridge2.send("getParagraphs", {});
|
|
19070
|
-
const total = paraCount.
|
|
19086
|
+
const total = paraCount.total;
|
|
19071
19087
|
checkBounds(fromIndex, total, "fromIndex");
|
|
19072
19088
|
if (fromIndex + count - 1 >= total) throw new ToolError(`fromIndex + count (${fromIndex + count}) exceeds paragraph count (${total}).`);
|
|
19073
19089
|
checkBounds(toIndex, total, "toIndex");
|
|
@@ -19076,6 +19092,15 @@ var moveParagraph = {
|
|
|
19076
19092
|
throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
|
|
19077
19093
|
}
|
|
19078
19094
|
}
|
|
19095
|
+
const lastIdx = total - 1;
|
|
19096
|
+
if (fromIndex + count - 1 === lastIdx) {
|
|
19097
|
+
const lastPara = paraCount.paragraphs.find((p) => p.index === lastIdx);
|
|
19098
|
+
if (lastPara && lastPara.text === "") {
|
|
19099
|
+
throw new ToolError(
|
|
19100
|
+
`Cannot move the last paragraph (index ${lastIdx}) \u2014 Word requires at least one paragraph and will auto-create a replacement, resulting in duplication. Use word_copy_paragraph instead, or exclude the trailing paragraph from the range.`
|
|
19101
|
+
);
|
|
19102
|
+
}
|
|
19103
|
+
}
|
|
19079
19104
|
const adjustedTo = fromIndex < toIndex ? toIndex - count : toIndex;
|
|
19080
19105
|
if (toIndex === fromIndex + count && location === "After") {
|
|
19081
19106
|
return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
|
|
@@ -19123,7 +19148,7 @@ var copyParagraph = {
|
|
|
19123
19148
|
if (!Number.isInteger(count)) throw new ToolError("count must be an integer.");
|
|
19124
19149
|
if (count < 1) throw new ToolError("count must be at least 1");
|
|
19125
19150
|
const paraCount = await bridge2.send("getParagraphs", {});
|
|
19126
|
-
const total = paraCount.
|
|
19151
|
+
const total = paraCount.total;
|
|
19127
19152
|
checkBounds(fromIndex, total, "fromIndex");
|
|
19128
19153
|
if (fromIndex + count - 1 >= total) throw new ToolError(`fromIndex + count (${fromIndex + count}) exceeds paragraph count (${total}).`);
|
|
19129
19154
|
checkBounds(toIndex, total, "toIndex");
|
|
@@ -19168,24 +19193,22 @@ var search = forwardTool(
|
|
|
19168
19193
|
);
|
|
19169
19194
|
var searchAndReplace = {
|
|
19170
19195
|
name: "word_search_and_replace",
|
|
19171
|
-
description: "[Search] Find and replace ALL occurrences. For single-paragraph edits, prefer word_replace_paragraph_text.",
|
|
19196
|
+
description: "[Search] Find and replace ALL occurrences. Supports Word search codes (^p = paragraph mark, ^t = tab). For single-paragraph edits, prefer word_replace_paragraph_text.",
|
|
19172
19197
|
schema: {
|
|
19173
19198
|
properties: {
|
|
19174
19199
|
find: { type: "string" },
|
|
19175
19200
|
replace: { type: "string" },
|
|
19176
19201
|
matchCase: { type: "boolean", description: "Default: false" },
|
|
19177
|
-
matchWholeWord: { type: "boolean" }
|
|
19202
|
+
matchWholeWord: { type: "boolean" },
|
|
19203
|
+
preserveBookmarks: { type: "boolean", description: "Re-create bookmarks on replacement text after replace. Default: false" }
|
|
19178
19204
|
},
|
|
19179
19205
|
required: ["find", "replace"]
|
|
19180
19206
|
},
|
|
19181
19207
|
async handler(args, bridge2) {
|
|
19182
19208
|
const find = args.find;
|
|
19183
|
-
const replace = args.replace;
|
|
19184
19209
|
if (!find || typeof find !== "string" || find.trim() === "") {
|
|
19185
19210
|
throw new ToolError("find string cannot be empty.");
|
|
19186
19211
|
}
|
|
19187
|
-
checkNoSpecialCodes(find, "find");
|
|
19188
|
-
checkNoSpecialCodes(replace, "replace");
|
|
19189
19212
|
const result = await bridge2.send("searchAndReplace", args);
|
|
19190
19213
|
return jsonResult(result);
|
|
19191
19214
|
}
|
|
@@ -19259,7 +19282,7 @@ var formatText = forwardTool(
|
|
|
19259
19282
|
underline: { type: "boolean" },
|
|
19260
19283
|
strikeThrough: { type: "boolean" },
|
|
19261
19284
|
color: { type: "string", description: "Hex color e.g. #FF0000" },
|
|
19262
|
-
highlightColor: { type: "string", description: "Highlight color name
|
|
19285
|
+
highlightColor: { type: "string", description: "Highlight color name (Yellow, Green, Cyan, Magenta, Blue, Red, DarkBlue, DarkCyan, DarkGreen, DarkMagenta, DarkRed, DarkYellow, Gray25, Gray50, Black, White, NoHighlight)" },
|
|
19263
19286
|
size: { type: "number", description: "Font size in points (1-1638)" },
|
|
19264
19287
|
name: { type: "string", description: "Font name" },
|
|
19265
19288
|
matchCase: { type: "boolean", description: "Default: false" }
|
|
@@ -19311,7 +19334,7 @@ var insertTable = forwardTool(
|
|
|
19311
19334
|
data: { type: "array", items: { type: "array", items: { type: "string" } }, description: "Cell values as array of row arrays" },
|
|
19312
19335
|
location: { type: "string", enum: ["Start", "End"] },
|
|
19313
19336
|
style: { type: "string", description: "Table style name" },
|
|
19314
|
-
headerRowCount: { type: "number" }
|
|
19337
|
+
headerRowCount: { type: "number", description: "Number of header rows (default: 0). Set to 1 to mark the first row as a repeating header." }
|
|
19315
19338
|
},
|
|
19316
19339
|
required: ["rows", "cols"]
|
|
19317
19340
|
},
|
|
@@ -20177,6 +20200,11 @@ function createBatchTool(registry, actionMap) {
|
|
|
20177
20200
|
if (operations.length > MAX_BATCH_OPERATIONS) {
|
|
20178
20201
|
throw new ToolError(`maximum ${MAX_BATCH_OPERATIONS} operations per batch`);
|
|
20179
20202
|
}
|
|
20203
|
+
for (const op of operations) {
|
|
20204
|
+
if (op.tool === "word_batch") {
|
|
20205
|
+
throw new ToolError("word_batch cannot be nested inside another batch. Flatten your operations into a single batch call.");
|
|
20206
|
+
}
|
|
20207
|
+
}
|
|
20180
20208
|
const results = [];
|
|
20181
20209
|
let nativeBuf = [];
|
|
20182
20210
|
let stopped = false;
|
|
@@ -20290,6 +20318,20 @@ function buildToolRegistry() {
|
|
|
20290
20318
|
return { tools: allTools, handlers };
|
|
20291
20319
|
}
|
|
20292
20320
|
|
|
20321
|
+
// src/server/mutex.ts
|
|
20322
|
+
function createMutex() {
|
|
20323
|
+
let chain = Promise.resolve();
|
|
20324
|
+
return {
|
|
20325
|
+
run(fn) {
|
|
20326
|
+
const next = chain.then(fn, fn);
|
|
20327
|
+
chain = next.then(noop, noop);
|
|
20328
|
+
return next;
|
|
20329
|
+
}
|
|
20330
|
+
};
|
|
20331
|
+
}
|
|
20332
|
+
function noop() {
|
|
20333
|
+
}
|
|
20334
|
+
|
|
20293
20335
|
// src/server/usage-guide.ts
|
|
20294
20336
|
var usageGuide = `# MCP Word Bridge \u2014 Usage Guide
|
|
20295
20337
|
|
|
@@ -20400,7 +20442,7 @@ After inserting a Table of Contents, heading text appears twice (in TOC and body
|
|
|
20400
20442
|
// package.json
|
|
20401
20443
|
var package_default = {
|
|
20402
20444
|
name: "mcp-word-bridge",
|
|
20403
|
-
version: "4.0
|
|
20445
|
+
version: "4.1.0",
|
|
20404
20446
|
description: "MCP server for live Word document editing via Office Add-in",
|
|
20405
20447
|
main: "dist/server.js",
|
|
20406
20448
|
bin: {
|
|
@@ -20464,6 +20506,7 @@ function createMcpServer(bridge2) {
|
|
|
20464
20506
|
{ capabilities: { tools: {}, resources: {} } }
|
|
20465
20507
|
);
|
|
20466
20508
|
const { tools, handlers } = buildToolRegistry();
|
|
20509
|
+
const toolMutex = createMutex();
|
|
20467
20510
|
server.setRequestHandler(import_types7.ListToolsRequestSchema, async () => ({
|
|
20468
20511
|
tools: tools.map((t) => ({
|
|
20469
20512
|
name: t.name,
|
|
@@ -20476,18 +20519,20 @@ function createMcpServer(bridge2) {
|
|
|
20476
20519
|
}))
|
|
20477
20520
|
}));
|
|
20478
20521
|
server.setRequestHandler(import_types7.CallToolRequestSchema, async (request) => {
|
|
20479
|
-
|
|
20480
|
-
|
|
20481
|
-
|
|
20482
|
-
|
|
20483
|
-
|
|
20484
|
-
|
|
20485
|
-
|
|
20486
|
-
|
|
20487
|
-
|
|
20488
|
-
|
|
20489
|
-
|
|
20490
|
-
|
|
20522
|
+
return toolMutex.run(async () => {
|
|
20523
|
+
const { name, arguments: args } = request.params;
|
|
20524
|
+
const handler = handlers.get(name);
|
|
20525
|
+
if (!handler) {
|
|
20526
|
+
return { content: [{ type: "text", text: "Unknown tool: " + name }], isError: true };
|
|
20527
|
+
}
|
|
20528
|
+
try {
|
|
20529
|
+
const result = await handler(args || {}, bridge2);
|
|
20530
|
+
return result;
|
|
20531
|
+
} catch (e) {
|
|
20532
|
+
const msg = e instanceof ToolError ? e.message : "Error: " + e.message;
|
|
20533
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
20534
|
+
}
|
|
20535
|
+
});
|
|
20491
20536
|
});
|
|
20492
20537
|
server.setRequestHandler(import_types7.ListResourcesRequestSchema, async () => ({
|
|
20493
20538
|
resources: [{
|