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 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 spacingFields) {
19051
+ for (const [name, value] of indentFields) {
19036
19052
  if (value !== void 0 && typeof value === "number") {
19037
- checkSpacingBounds(value, name);
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.count;
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.count;
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 or hex" },
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.5",
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
- const { name, arguments: args } = request.params;
20480
- const handler = handlers.get(name);
20481
- if (!handler) {
20482
- return { content: [{ type: "text", text: "Unknown tool: " + name }], isError: true };
20483
- }
20484
- try {
20485
- const result = await handler(args || {}, bridge2);
20486
- return result;
20487
- } catch (e) {
20488
- const msg = e instanceof ToolError ? e.message : "Error: " + e.message;
20489
- return { content: [{ type: "text", text: msg }], isError: true };
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: [{