mcp-word-bridge 4.1.1 → 4.1.3

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
@@ -266,7 +266,7 @@ npm install
266
266
  npm run build # build server + taskpane
267
267
  npm run dev # watch mode
268
268
  npm run typecheck # TypeScript type checking
269
- npm test # unit tests (67 tests, <500ms)
269
+ npm test # unit tests (272 tests, <600ms)
270
270
  npm run test:live # integration tests (requires Word)
271
271
  ```
272
272
 
@@ -299,8 +299,10 @@ Platform constraints in the Word JavaScript API that cannot be resolved in this
299
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
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
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. | |
302
+ | Mixed Formatting | `word_get_paragraph_by_index` returns `null` for font properties (`bold`, `italic`, `size`, etc.) when the paragraph contains mixed formatting (e.g. partially bold text). This is the Word API's way of indicating "no single value" — treat `null` as "mixed". | [Word.Font docs](https://learn.microsoft.com/en-us/javascript/api/word/word.font?view=word-js-preview) |
303
+ | TOC Search | `body.search()` matches text inside TOC field entries as well as body content. Heading text appears in both locations, so search results return TOC matches first. Use `occurrence` to skip past TOC entries and target the body instance. | |
304
+ | Page Info | `word_get_page_info` requires WordApiDesktop 1.2+ (Word for Windows/Mac desktop). Not available on Word for the web. | [WordApiDesktop 1.2](https://learn.microsoft.com/en-us/javascript/api/requirement-sets/word/word-api-desktop-1-2-requirement-set) |
305
+ | 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. | [Stack Overflow](https://stackoverflow.com/questions/58440921/accessing-the-undo-stack-from-word-javascript-api) |
304
306
 
305
307
  ## License
306
308
 
package/dist/server.js CHANGED
@@ -18646,6 +18646,7 @@ var HEAVY_OPERATIONS = /* @__PURE__ */ new Set([
18646
18646
  "batchExecute"
18647
18647
  ]);
18648
18648
  var MAX_PAYLOAD = 10 * 1024 * 1024;
18649
+ var HIGHLIGHT_COLORS = ["Yellow", "Green", "Cyan", "Magenta", "Blue", "Red", "DarkBlue", "DarkCyan", "DarkGreen", "DarkMagenta", "DarkRed", "DarkYellow", "Gray25", "Gray50", "Black", "White", "NoHighlight"];
18649
18650
 
18650
18651
  // src/server/bridge.ts
18651
18652
  var Bridge = class {
@@ -18722,7 +18723,7 @@ var Bridge = class {
18722
18723
 
18723
18724
  // src/server/mcp.ts
18724
18725
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
18725
- var import_types8 = require("@modelcontextprotocol/sdk/types.js");
18726
+ var import_types9 = require("@modelcontextprotocol/sdk/types.js");
18726
18727
 
18727
18728
  // src/server/types.ts
18728
18729
  var ToolError = class extends Error {
@@ -18736,13 +18737,15 @@ var ToolError = class extends Error {
18736
18737
  function jsonResult(data) {
18737
18738
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
18738
18739
  }
18739
- function forwardTool(name, description, schema, action) {
18740
+ function forwardTool(name, description, schema, action, validate) {
18740
18741
  return {
18741
18742
  name,
18742
18743
  description,
18743
18744
  schema,
18744
18745
  bridgeAction: action,
18746
+ validate,
18745
18747
  async handler(args, bridge2) {
18748
+ if (validate) validate(args);
18746
18749
  const result = await bridge2.send(action, args);
18747
18750
  return jsonResult(result);
18748
18751
  }
@@ -18788,7 +18791,7 @@ var save = forwardTool(
18788
18791
  );
18789
18792
  var clear = forwardTool(
18790
18793
  "word_clear",
18791
- "[Document] Clear all document body content. Does not clear headers/footers or custom properties.",
18794
+ "[Document] Clear all document body content. Does not clear headers/footers or custom properties. In multi-section documents, section breaks are removed and the last section's layout (margins, orientation) is preserved.",
18792
18795
  { properties: {} },
18793
18796
  "clearDocument"
18794
18797
  );
@@ -18929,6 +18932,19 @@ function checkPropertyKeyLength(key) {
18929
18932
  );
18930
18933
  }
18931
18934
  }
18935
+ function checkHexColor(color, name) {
18936
+ if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
18937
+ throw new ToolError(`${name} must be a valid hex color (e.g. #FF0000).`);
18938
+ }
18939
+ }
18940
+ function checkUrl(url) {
18941
+ if (!/^https?:\/\/.+/i.test(url)) {
18942
+ throw new ToolError("URL must be a valid HTTP or HTTPS URL (e.g. https://example.com).");
18943
+ }
18944
+ if (/[<>"{}|\\^`]/.test(url)) {
18945
+ throw new ToolError(`Malformed URL: "${url}". URL contains invalid characters that must be percent-encoded.`);
18946
+ }
18947
+ }
18932
18948
 
18933
18949
  // src/server/tools/paragraphs.ts
18934
18950
  var getParagraphs = forwardTool(
@@ -18944,7 +18960,7 @@ var getParagraphs = forwardTool(
18944
18960
  );
18945
18961
  var getParagraphByIndex = forwardTool(
18946
18962
  "word_get_paragraph_by_index",
18947
- "[Paragraphs] Get full details of a single paragraph including font, spacing, indentation, and outline level.",
18963
+ "[Paragraphs] Get full details of a single paragraph including font, spacing, indentation, and outline level. Font properties return null when the paragraph has mixed formatting (e.g. partially bold).",
18948
18964
  {
18949
18965
  properties: {
18950
18966
  index: { type: "number", description: "Paragraph index (0-based)" }
@@ -19036,6 +19052,12 @@ var setParagraphSpacing = {
19036
19052
  async handler(args, bridge2) {
19037
19053
  const index = args.index;
19038
19054
  checkNonNegative(index, "index");
19055
+ const hasProperty = args.lineSpacing !== void 0 || args.spaceBefore !== void 0 || args.spaceAfter !== void 0 || args.firstLineIndent !== void 0 || args.leftIndent !== void 0 || args.rightIndent !== void 0;
19056
+ if (!hasProperty) {
19057
+ throw new ToolError(
19058
+ "At least one spacing or indent property must be provided (lineSpacing, spaceBefore, spaceAfter, firstLineIndent, leftIndent, rightIndent)."
19059
+ );
19060
+ }
19039
19061
  const spacingFields = [
19040
19062
  ["lineSpacing", args.lineSpacing],
19041
19063
  ["spaceBefore", args.spaceBefore],
@@ -19095,6 +19117,12 @@ var moveParagraph = {
19095
19117
  throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
19096
19118
  }
19097
19119
  }
19120
+ const destPara = paraCount.paragraphs.find((p) => p.index === toIndex);
19121
+ if (destPara?.inTable) {
19122
+ throw new ToolError(
19123
+ `Destination paragraph ${toIndex} is inside a table cell. Moving content here would corrupt table structure. Use table-specific tools or target a paragraph outside the table.`
19124
+ );
19125
+ }
19098
19126
  const lastIdx = total - 1;
19099
19127
  if (fromIndex + count - 1 === lastIdx) {
19100
19128
  const lastPara = paraCount.paragraphs.find((p) => p.index === lastIdx);
@@ -19105,10 +19133,8 @@ var moveParagraph = {
19105
19133
  }
19106
19134
  }
19107
19135
  const adjustedTo = fromIndex < toIndex ? toIndex - count : toIndex;
19108
- if (toIndex === fromIndex + count && location === "After") {
19109
- return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
19110
- }
19111
- if (adjustedTo === fromIndex && location === "After") {
19136
+ const effectiveInsertPos = location === "Before" ? adjustedTo : adjustedTo + 1;
19137
+ if (effectiveInsertPos === fromIndex) {
19112
19138
  return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
19113
19139
  }
19114
19140
  const ooxmlResult = await bridge2.send("getParaOoxml", { index: fromIndex, count });
@@ -19160,6 +19186,12 @@ var copyParagraph = {
19160
19186
  throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
19161
19187
  }
19162
19188
  }
19189
+ const destPara = paraCount.paragraphs.find((p) => p.index === toIndex);
19190
+ if (destPara?.inTable) {
19191
+ throw new ToolError(
19192
+ `Destination paragraph ${toIndex} is inside a table cell. Copying content here would corrupt table structure. Use table-specific tools or target a paragraph outside the table.`
19193
+ );
19194
+ }
19163
19195
  const ooxmlResult = await bridge2.send("getParaOoxml", { index: fromIndex, count });
19164
19196
  const effectiveToIndex = toIndex >= fromIndex && toIndex < fromIndex + count ? fromIndex + count - 1 : toIndex;
19165
19197
  const effectiveLocation = toIndex >= fromIndex && toIndex < fromIndex + count ? "After" : location;
@@ -19197,7 +19229,10 @@ var search = forwardTool(
19197
19229
  },
19198
19230
  required: ["query"]
19199
19231
  },
19200
- "search"
19232
+ "search",
19233
+ (args) => {
19234
+ checkNonEmpty(args.query, "query");
19235
+ }
19201
19236
  );
19202
19237
  var searchAndReplace = {
19203
19238
  name: "word_search_and_replace",
@@ -19228,7 +19263,7 @@ var searchAndReplace = {
19228
19263
  };
19229
19264
  var insertTextAtMatch = forwardTool(
19230
19265
  "word_insert_text_at_match",
19231
- '[Search] Insert text before or after a search match. Supports Word search codes in inserted text: ^p (paragraph mark), ^l (line break), ^t (tab), ^s (non-breaking space), ^m (page break), ^n (column break), ^~ (non-breaking hyphen), ^- (optional hyphen), ^+ (em dash), ^= (en dash), ^^ (literal caret). Provide "after" OR "before" as the anchor text. Use occurrence for Nth match.',
19266
+ '[Search] Insert text before or after a search match. Supports Word search codes in inserted text: ^p (paragraph mark), ^l (line break), ^t (tab), ^s (non-breaking space), ^m (page break), ^n (column break), ^~ (non-breaking hyphen), ^- (optional hyphen), ^+ (em dash), ^= (en dash), ^^ (literal caret). Provide "after" OR "before" as the anchor text. Use occurrence for Nth match. Note: inserting ^p after hyperlinked text may create a paragraph that inherits the hyperlink character style.',
19232
19267
  {
19233
19268
  properties: {
19234
19269
  text: { type: "string", description: "Text to insert" },
@@ -19276,7 +19311,10 @@ var insertLineBreak = forwardTool(
19276
19311
  },
19277
19312
  required: ["anchorText"]
19278
19313
  },
19279
- "insertLineBreak"
19314
+ "insertLineBreak",
19315
+ (args) => {
19316
+ checkNonEmpty(args.anchorText, "anchorText");
19317
+ }
19280
19318
  );
19281
19319
  var searchTools = [
19282
19320
  search,
@@ -19288,6 +19326,33 @@ var searchTools = [
19288
19326
  ];
19289
19327
 
19290
19328
  // src/server/tools/formatting.ts
19329
+ function validateFormatText(args) {
19330
+ checkNonEmpty(args.text, "text");
19331
+ const hasFormatting = args.bold !== void 0 || args.italic !== void 0 || args.underline !== void 0 || args.strikeThrough !== void 0 || args.color !== void 0 || args.highlightColor !== void 0 || args.size !== void 0 || args.name !== void 0;
19332
+ if (!hasFormatting) {
19333
+ throw new ToolError(
19334
+ "At least one formatting property must be specified (bold, italic, underline, strikeThrough, color, highlightColor, size, or name)."
19335
+ );
19336
+ }
19337
+ if (args.size !== void 0) {
19338
+ const size = args.size;
19339
+ if (size <= 0) throw new ToolError("size must be positive (minimum 1 point).");
19340
+ if (size > 1638) throw new ToolError("size must not exceed 1638 points (Word maximum).");
19341
+ if (!Number.isFinite(size)) throw new ToolError("size must be a finite number.");
19342
+ }
19343
+ if (args.color !== void 0) {
19344
+ checkHexColor(args.color, "color");
19345
+ }
19346
+ if (args.highlightColor !== void 0) {
19347
+ const hc = args.highlightColor;
19348
+ const isNamed = HIGHLIGHT_COLORS.some((c) => c.toLowerCase() === hc.toLowerCase());
19349
+ if (!isNamed) {
19350
+ throw new ToolError(
19351
+ `Invalid highlightColor: "${hc}". Valid values: ${HIGHLIGHT_COLORS.join(", ")}.`
19352
+ );
19353
+ }
19354
+ }
19355
+ }
19291
19356
  var formatText = forwardTool(
19292
19357
  "word_format_text",
19293
19358
  "[Formatting] Apply formatting (bold, italic, color, size, font) to a text match. Color must be hex (#FF0000). Size: 1-1638pt.",
@@ -19307,7 +19372,8 @@ var formatText = forwardTool(
19307
19372
  },
19308
19373
  required: ["text"]
19309
19374
  },
19310
- "formatRange"
19375
+ "formatRange",
19376
+ validateFormatText
19311
19377
  );
19312
19378
  var clearFormatting = forwardTool(
19313
19379
  "word_clear_formatting",
@@ -19486,7 +19552,7 @@ var tableTools = [
19486
19552
  // src/server/tools/lists.ts
19487
19553
  var insertList = forwardTool(
19488
19554
  "word_insert_list",
19489
- "[Lists] Insert a bulleted or numbered list from an array of item strings.",
19555
+ "[Lists] Insert a bulleted or numbered list from an array of item strings. Text is inserted literally (Word search codes like ^p or ^t are NOT interpreted).",
19490
19556
  {
19491
19557
  properties: {
19492
19558
  items: { type: "array", items: { type: "string" }, description: "List item strings" },
@@ -19539,7 +19605,11 @@ var addComment = forwardTool(
19539
19605
  },
19540
19606
  required: ["anchorText", "comment"]
19541
19607
  },
19542
- "addComment"
19608
+ "addComment",
19609
+ (args) => {
19610
+ checkNonEmpty(args.anchorText, "anchorText");
19611
+ checkNonEmpty(args.comment, "comment");
19612
+ }
19543
19613
  );
19544
19614
  var getComments = forwardTool(
19545
19615
  "word_get_comments",
@@ -19568,7 +19638,10 @@ var replyToComment = forwardTool(
19568
19638
  },
19569
19639
  required: ["commentId", "text"]
19570
19640
  },
19571
- "replyToComment"
19641
+ "replyToComment",
19642
+ (args) => {
19643
+ checkNonEmpty(args.text, "text");
19644
+ }
19572
19645
  );
19573
19646
  var resolveComment = forwardTool(
19574
19647
  "word_resolve_comment",
@@ -19614,7 +19687,11 @@ var insertFootnote = forwardTool(
19614
19687
  },
19615
19688
  required: ["anchorText", "text"]
19616
19689
  },
19617
- "insertFootnote"
19690
+ "insertFootnote",
19691
+ (args) => {
19692
+ checkNonEmpty(args.anchorText, "anchorText");
19693
+ checkNonEmpty(args.text, "text");
19694
+ }
19618
19695
  );
19619
19696
  var insertFootnoteAtIndex = forwardTool(
19620
19697
  "word_insert_footnote_at_index",
@@ -19626,7 +19703,10 @@ var insertFootnoteAtIndex = forwardTool(
19626
19703
  },
19627
19704
  required: ["paragraphIndex", "text"]
19628
19705
  },
19629
- "insertFootnoteAtIndex"
19706
+ "insertFootnoteAtIndex",
19707
+ (args) => {
19708
+ checkNonEmpty(args.text, "text");
19709
+ }
19630
19710
  );
19631
19711
  var insertEndnote = forwardTool(
19632
19712
  "word_insert_endnote",
@@ -19640,7 +19720,11 @@ var insertEndnote = forwardTool(
19640
19720
  },
19641
19721
  required: ["anchorText", "text"]
19642
19722
  },
19643
- "insertEndnote"
19723
+ "insertEndnote",
19724
+ (args) => {
19725
+ checkNonEmpty(args.anchorText, "anchorText");
19726
+ checkNonEmpty(args.text, "text");
19727
+ }
19644
19728
  );
19645
19729
  var getFootnotes = forwardTool(
19646
19730
  "word_get_footnotes",
@@ -19705,7 +19789,11 @@ var insertBookmark = forwardTool(
19705
19789
  },
19706
19790
  required: ["name", "anchorText"]
19707
19791
  },
19708
- "insertBookmark"
19792
+ "insertBookmark",
19793
+ (args) => {
19794
+ checkNonEmpty(args.name, "name");
19795
+ checkNonEmpty(args.anchorText, "anchorText");
19796
+ }
19709
19797
  );
19710
19798
  var deleteBookmark = forwardTool(
19711
19799
  "word_delete_bookmark",
@@ -19716,7 +19804,10 @@ var deleteBookmark = forwardTool(
19716
19804
  },
19717
19805
  required: ["name"]
19718
19806
  },
19719
- "deleteBookmark"
19807
+ "deleteBookmark",
19808
+ (args) => {
19809
+ checkNonEmpty(args.name, "name");
19810
+ }
19720
19811
  );
19721
19812
  var goToBookmark = forwardTool(
19722
19813
  "word_go_to_bookmark",
@@ -19727,7 +19818,10 @@ var goToBookmark = forwardTool(
19727
19818
  },
19728
19819
  required: ["name"]
19729
19820
  },
19730
- "goToBookmark"
19821
+ "goToBookmark",
19822
+ (args) => {
19823
+ checkNonEmpty(args.name, "name");
19824
+ }
19731
19825
  );
19732
19826
  var getBookmarkText = forwardTool(
19733
19827
  "word_get_bookmark_text",
@@ -19738,7 +19832,10 @@ var getBookmarkText = forwardTool(
19738
19832
  },
19739
19833
  required: ["name"]
19740
19834
  },
19741
- "getBookmarkText"
19835
+ "getBookmarkText",
19836
+ (args) => {
19837
+ checkNonEmpty(args.name, "name");
19838
+ }
19742
19839
  );
19743
19840
  var bookmarkTools = [
19744
19841
  getBookmarks,
@@ -19761,7 +19858,12 @@ var insertHyperlink = forwardTool(
19761
19858
  },
19762
19859
  required: ["anchorText", "url"]
19763
19860
  },
19764
- "insertHyperlink"
19861
+ "insertHyperlink",
19862
+ (args) => {
19863
+ checkNonEmpty(args.anchorText, "anchorText");
19864
+ checkNonEmpty(args.url, "url");
19865
+ checkUrl(args.url);
19866
+ }
19765
19867
  );
19766
19868
  var getHyperlinks = forwardTool(
19767
19869
  "word_get_hyperlinks",
@@ -19780,7 +19882,10 @@ var removeHyperlink = forwardTool(
19780
19882
  },
19781
19883
  required: ["anchorText"]
19782
19884
  },
19783
- "removeHyperlink"
19885
+ "removeHyperlink",
19886
+ (args) => {
19887
+ checkNonEmpty(args.anchorText, "anchorText");
19888
+ }
19784
19889
  );
19785
19890
  var hyperlinkTools = [
19786
19891
  insertHyperlink,
@@ -19809,7 +19914,10 @@ var insertContentControl = forwardTool(
19809
19914
  matchCase: { type: "boolean", description: "Default: false" }
19810
19915
  }
19811
19916
  },
19812
- "insertContentControl"
19917
+ "insertContentControl",
19918
+ (args) => {
19919
+ checkNonEmpty(args.anchorText, "anchorText");
19920
+ }
19813
19921
  );
19814
19922
  var setContentControlText = {
19815
19923
  name: "word_set_content_control_text",
@@ -20199,10 +20307,10 @@ var equationTools = [
20199
20307
  ];
20200
20308
 
20201
20309
  // src/server/tools/batch.ts
20202
- function createBatchTool(registry, actionMap) {
20310
+ function createBatchTool(registry, actionMap, validators) {
20203
20311
  return {
20204
20312
  name: "word_batch",
20205
- description: "[Batch] Execute multiple operations in a single call. Operations execute sequentially \u2014 if one fails, subsequent are skipped.",
20313
+ description: "[Batch] Execute multiple operations in a single call. Operations execute sequentially \u2014 if one fails, subsequent are skipped. Note: paragraph indices are NOT auto-adjusted between operations. Multiple inserts at the same index will produce reversed order (last inserted appears first).",
20206
20314
  schema: {
20207
20315
  properties: {
20208
20316
  operations: {
@@ -20268,6 +20376,16 @@ function createBatchTool(registry, actionMap) {
20268
20376
  break;
20269
20377
  }
20270
20378
  if (actionMap.has(op.tool)) {
20379
+ const validate = validators.get(op.tool);
20380
+ if (validate) {
20381
+ try {
20382
+ validate(op.args || {});
20383
+ } catch (e) {
20384
+ results.push({ index: i, tool: op.tool, success: false, error: e.message });
20385
+ stopped = true;
20386
+ break;
20387
+ }
20388
+ }
20271
20389
  nativeBuf.push({ tool: op.tool, args: op.args || {}, originalIndex: i });
20272
20390
  } else {
20273
20391
  const ok = await flushNative();
@@ -20327,13 +20445,17 @@ function buildToolRegistry() {
20327
20445
  ];
20328
20446
  const handlers = /* @__PURE__ */ new Map();
20329
20447
  const actionMap = /* @__PURE__ */ new Map();
20448
+ const validators = /* @__PURE__ */ new Map();
20330
20449
  for (const tool of allTools) {
20331
20450
  handlers.set(tool.name, tool.handler);
20332
20451
  if ("bridgeAction" in tool && typeof tool.bridgeAction === "string") {
20333
20452
  actionMap.set(tool.name, tool.bridgeAction);
20334
20453
  }
20454
+ if ("validate" in tool && typeof tool.validate === "function") {
20455
+ validators.set(tool.name, tool.validate);
20456
+ }
20335
20457
  }
20336
- const batchTool = createBatchTool(handlers, actionMap);
20458
+ const batchTool = createBatchTool(handlers, actionMap, validators);
20337
20459
  allTools.push(batchTool);
20338
20460
  handlers.set(batchTool.name, batchTool.handler);
20339
20461
  return { tools: allTools, handlers };
@@ -20398,6 +20520,8 @@ Controls a live Word document. All operations execute immediately.
20398
20520
  \`\`\`
20399
20521
  Runs sequentially. Stops on first error. Maximum 50 per batch. Prefer batching over individual calls.
20400
20522
 
20523
+ **Index caveat:** Paragraph indices are NOT auto-adjusted between operations. Multiple inserts at the same index produce reversed order (last inserted appears first). To insert A, B, C in order at position 5, either increment the index or insert in reverse.
20524
+
20401
20525
  ## Search
20402
20526
 
20403
20527
  - Case-insensitive by default. Pass \`matchCase: true\` for exact case.
@@ -20458,12 +20582,21 @@ After inserting a Table of Contents, heading text appears twice (in TOC and body
20458
20582
  6. Use \`word_copy_paragraph\` to duplicate (preserves everything)
20459
20583
  7. Save explicitly after significant changes
20460
20584
  8. Resolve comments rather than deleting (preserves audit trail)
20585
+
20586
+ ## Alignment Values
20587
+
20588
+ Input accepts case-insensitive: "left", "center", "right", "justified".
20589
+ Output always uses canonical form: "Left", "Center", "Right", "Justified".
20590
+
20591
+ ## Mixed Formatting
20592
+
20593
+ \`word_get_paragraph_by_index\` returns \`null\` for font properties when the paragraph has mixed formatting (e.g. partially bold). Treat \`null\` as "mixed" \u2014 use \`word_get_font_info\` with a specific text match for precise values.
20461
20594
  `;
20462
20595
 
20463
20596
  // package.json
20464
20597
  var package_default = {
20465
20598
  name: "mcp-word-bridge",
20466
- version: "4.1.1",
20599
+ version: "4.1.3",
20467
20600
  description: "MCP server for live Word document editing via Office Add-in",
20468
20601
  main: "dist/server.js",
20469
20602
  bin: {
@@ -20528,7 +20661,7 @@ function createMcpServer(bridge2) {
20528
20661
  );
20529
20662
  const { tools, handlers } = buildToolRegistry();
20530
20663
  const toolMutex = createMutex();
20531
- server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
20664
+ server.setRequestHandler(import_types9.ListToolsRequestSchema, async () => ({
20532
20665
  tools: tools.map((t) => ({
20533
20666
  name: t.name,
20534
20667
  description: t.description,
@@ -20539,7 +20672,7 @@ function createMcpServer(bridge2) {
20539
20672
  }
20540
20673
  }))
20541
20674
  }));
20542
- server.setRequestHandler(import_types8.CallToolRequestSchema, async (request) => {
20675
+ server.setRequestHandler(import_types9.CallToolRequestSchema, async (request) => {
20543
20676
  return toolMutex.run(async () => {
20544
20677
  const { name, arguments: args } = request.params;
20545
20678
  const handler = handlers.get(name);
@@ -20555,7 +20688,7 @@ function createMcpServer(bridge2) {
20555
20688
  }
20556
20689
  });
20557
20690
  });
20558
- server.setRequestHandler(import_types8.ListResourcesRequestSchema, async () => ({
20691
+ server.setRequestHandler(import_types9.ListResourcesRequestSchema, async () => ({
20559
20692
  resources: [{
20560
20693
  uri: "word-bridge://usage-guide",
20561
20694
  name: "Word Bridge Usage Guide",
@@ -20563,7 +20696,7 @@ function createMcpServer(bridge2) {
20563
20696
  mimeType: "text/markdown"
20564
20697
  }]
20565
20698
  }));
20566
- server.setRequestHandler(import_types8.ReadResourceRequestSchema, async (request) => {
20699
+ server.setRequestHandler(import_types9.ReadResourceRequestSchema, async (request) => {
20567
20700
  if (request.params.uri === "word-bridge://usage-guide") {
20568
20701
  return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: usageGuide }] };
20569
20702
  }
@@ -20634,7 +20767,7 @@ function createHttpsServer() {
20634
20767
  };
20635
20768
  const rootDir = import_path.default.join(__dirname, "..");
20636
20769
  return import_https.default.createServer(options, (req, res) => {
20637
- let urlPath = (req.url || "/").split("?")[0];
20770
+ const urlPath = (req.url || "/").split("?")[0];
20638
20771
  let filePath = urlPath === "/" ? "/taskpane.html" : urlPath;
20639
20772
  filePath = import_path.default.resolve(rootDir, "." + filePath);
20640
20773
  if (!filePath.startsWith(rootDir + import_path.default.sep) && filePath !== rootDir) {