mcp-word-bridge 4.1.2 → 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
@@ -300,6 +300,7 @@ Platform constraints in the Word JavaScript API that cannot be resolved in this
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
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. | |
303
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) |
304
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) |
305
306
 
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 {
@@ -18742,6 +18743,7 @@ function forwardTool(name, description, schema, action, validate) {
18742
18743
  description,
18743
18744
  schema,
18744
18745
  bridgeAction: action,
18746
+ validate,
18745
18747
  async handler(args, bridge2) {
18746
18748
  if (validate) validate(args);
18747
18749
  const result = await bridge2.send(action, args);
@@ -18935,6 +18937,14 @@ function checkHexColor(color, name) {
18935
18937
  throw new ToolError(`${name} must be a valid hex color (e.g. #FF0000).`);
18936
18938
  }
18937
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
+ }
18938
18948
 
18939
18949
  // src/server/tools/paragraphs.ts
18940
18950
  var getParagraphs = forwardTool(
@@ -19123,10 +19133,8 @@ var moveParagraph = {
19123
19133
  }
19124
19134
  }
19125
19135
  const adjustedTo = fromIndex < toIndex ? toIndex - count : toIndex;
19126
- if (toIndex === fromIndex + count && location === "After") {
19127
- return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
19128
- }
19129
- if (adjustedTo === fromIndex && location === "After") {
19136
+ const effectiveInsertPos = location === "Before" ? adjustedTo : adjustedTo + 1;
19137
+ if (effectiveInsertPos === fromIndex) {
19130
19138
  return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
19131
19139
  }
19132
19140
  const ooxmlResult = await bridge2.send("getParaOoxml", { index: fromIndex, count });
@@ -19221,7 +19229,10 @@ var search = forwardTool(
19221
19229
  },
19222
19230
  required: ["query"]
19223
19231
  },
19224
- "search"
19232
+ "search",
19233
+ (args) => {
19234
+ checkNonEmpty(args.query, "query");
19235
+ }
19225
19236
  );
19226
19237
  var searchAndReplace = {
19227
19238
  name: "word_search_and_replace",
@@ -19300,7 +19311,10 @@ var insertLineBreak = forwardTool(
19300
19311
  },
19301
19312
  required: ["anchorText"]
19302
19313
  },
19303
- "insertLineBreak"
19314
+ "insertLineBreak",
19315
+ (args) => {
19316
+ checkNonEmpty(args.anchorText, "anchorText");
19317
+ }
19304
19318
  );
19305
19319
  var searchTools = [
19306
19320
  search,
@@ -19312,7 +19326,6 @@ var searchTools = [
19312
19326
  ];
19313
19327
 
19314
19328
  // src/server/tools/formatting.ts
19315
- var HIGHLIGHT_COLORS = ["Yellow", "Green", "Cyan", "Magenta", "Blue", "Red", "DarkBlue", "DarkCyan", "DarkGreen", "DarkMagenta", "DarkRed", "DarkYellow", "Gray25", "Gray50", "Black", "White", "NoHighlight"];
19316
19329
  function validateFormatText(args) {
19317
19330
  checkNonEmpty(args.text, "text");
19318
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;
@@ -19674,7 +19687,11 @@ var insertFootnote = forwardTool(
19674
19687
  },
19675
19688
  required: ["anchorText", "text"]
19676
19689
  },
19677
- "insertFootnote"
19690
+ "insertFootnote",
19691
+ (args) => {
19692
+ checkNonEmpty(args.anchorText, "anchorText");
19693
+ checkNonEmpty(args.text, "text");
19694
+ }
19678
19695
  );
19679
19696
  var insertFootnoteAtIndex = forwardTool(
19680
19697
  "word_insert_footnote_at_index",
@@ -19686,7 +19703,10 @@ var insertFootnoteAtIndex = forwardTool(
19686
19703
  },
19687
19704
  required: ["paragraphIndex", "text"]
19688
19705
  },
19689
- "insertFootnoteAtIndex"
19706
+ "insertFootnoteAtIndex",
19707
+ (args) => {
19708
+ checkNonEmpty(args.text, "text");
19709
+ }
19690
19710
  );
19691
19711
  var insertEndnote = forwardTool(
19692
19712
  "word_insert_endnote",
@@ -19700,7 +19720,11 @@ var insertEndnote = forwardTool(
19700
19720
  },
19701
19721
  required: ["anchorText", "text"]
19702
19722
  },
19703
- "insertEndnote"
19723
+ "insertEndnote",
19724
+ (args) => {
19725
+ checkNonEmpty(args.anchorText, "anchorText");
19726
+ checkNonEmpty(args.text, "text");
19727
+ }
19704
19728
  );
19705
19729
  var getFootnotes = forwardTool(
19706
19730
  "word_get_footnotes",
@@ -19765,7 +19789,11 @@ var insertBookmark = forwardTool(
19765
19789
  },
19766
19790
  required: ["name", "anchorText"]
19767
19791
  },
19768
- "insertBookmark"
19792
+ "insertBookmark",
19793
+ (args) => {
19794
+ checkNonEmpty(args.name, "name");
19795
+ checkNonEmpty(args.anchorText, "anchorText");
19796
+ }
19769
19797
  );
19770
19798
  var deleteBookmark = forwardTool(
19771
19799
  "word_delete_bookmark",
@@ -19776,7 +19804,10 @@ var deleteBookmark = forwardTool(
19776
19804
  },
19777
19805
  required: ["name"]
19778
19806
  },
19779
- "deleteBookmark"
19807
+ "deleteBookmark",
19808
+ (args) => {
19809
+ checkNonEmpty(args.name, "name");
19810
+ }
19780
19811
  );
19781
19812
  var goToBookmark = forwardTool(
19782
19813
  "word_go_to_bookmark",
@@ -19787,7 +19818,10 @@ var goToBookmark = forwardTool(
19787
19818
  },
19788
19819
  required: ["name"]
19789
19820
  },
19790
- "goToBookmark"
19821
+ "goToBookmark",
19822
+ (args) => {
19823
+ checkNonEmpty(args.name, "name");
19824
+ }
19791
19825
  );
19792
19826
  var getBookmarkText = forwardTool(
19793
19827
  "word_get_bookmark_text",
@@ -19798,7 +19832,10 @@ var getBookmarkText = forwardTool(
19798
19832
  },
19799
19833
  required: ["name"]
19800
19834
  },
19801
- "getBookmarkText"
19835
+ "getBookmarkText",
19836
+ (args) => {
19837
+ checkNonEmpty(args.name, "name");
19838
+ }
19802
19839
  );
19803
19840
  var bookmarkTools = [
19804
19841
  getBookmarks,
@@ -19821,7 +19858,12 @@ var insertHyperlink = forwardTool(
19821
19858
  },
19822
19859
  required: ["anchorText", "url"]
19823
19860
  },
19824
- "insertHyperlink"
19861
+ "insertHyperlink",
19862
+ (args) => {
19863
+ checkNonEmpty(args.anchorText, "anchorText");
19864
+ checkNonEmpty(args.url, "url");
19865
+ checkUrl(args.url);
19866
+ }
19825
19867
  );
19826
19868
  var getHyperlinks = forwardTool(
19827
19869
  "word_get_hyperlinks",
@@ -19840,7 +19882,10 @@ var removeHyperlink = forwardTool(
19840
19882
  },
19841
19883
  required: ["anchorText"]
19842
19884
  },
19843
- "removeHyperlink"
19885
+ "removeHyperlink",
19886
+ (args) => {
19887
+ checkNonEmpty(args.anchorText, "anchorText");
19888
+ }
19844
19889
  );
19845
19890
  var hyperlinkTools = [
19846
19891
  insertHyperlink,
@@ -19869,7 +19914,10 @@ var insertContentControl = forwardTool(
19869
19914
  matchCase: { type: "boolean", description: "Default: false" }
19870
19915
  }
19871
19916
  },
19872
- "insertContentControl"
19917
+ "insertContentControl",
19918
+ (args) => {
19919
+ checkNonEmpty(args.anchorText, "anchorText");
19920
+ }
19873
19921
  );
19874
19922
  var setContentControlText = {
19875
19923
  name: "word_set_content_control_text",
@@ -20259,7 +20307,7 @@ var equationTools = [
20259
20307
  ];
20260
20308
 
20261
20309
  // src/server/tools/batch.ts
20262
- function createBatchTool(registry, actionMap) {
20310
+ function createBatchTool(registry, actionMap, validators) {
20263
20311
  return {
20264
20312
  name: "word_batch",
20265
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).",
@@ -20328,6 +20376,16 @@ function createBatchTool(registry, actionMap) {
20328
20376
  break;
20329
20377
  }
20330
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
+ }
20331
20389
  nativeBuf.push({ tool: op.tool, args: op.args || {}, originalIndex: i });
20332
20390
  } else {
20333
20391
  const ok = await flushNative();
@@ -20387,13 +20445,17 @@ function buildToolRegistry() {
20387
20445
  ];
20388
20446
  const handlers = /* @__PURE__ */ new Map();
20389
20447
  const actionMap = /* @__PURE__ */ new Map();
20448
+ const validators = /* @__PURE__ */ new Map();
20390
20449
  for (const tool of allTools) {
20391
20450
  handlers.set(tool.name, tool.handler);
20392
20451
  if ("bridgeAction" in tool && typeof tool.bridgeAction === "string") {
20393
20452
  actionMap.set(tool.name, tool.bridgeAction);
20394
20453
  }
20454
+ if ("validate" in tool && typeof tool.validate === "function") {
20455
+ validators.set(tool.name, tool.validate);
20456
+ }
20395
20457
  }
20396
- const batchTool = createBatchTool(handlers, actionMap);
20458
+ const batchTool = createBatchTool(handlers, actionMap, validators);
20397
20459
  allTools.push(batchTool);
20398
20460
  handlers.set(batchTool.name, batchTool.handler);
20399
20461
  return { tools: allTools, handlers };
@@ -20534,7 +20596,7 @@ Output always uses canonical form: "Left", "Center", "Right", "Justified".
20534
20596
  // package.json
20535
20597
  var package_default = {
20536
20598
  name: "mcp-word-bridge",
20537
- version: "4.1.2",
20599
+ version: "4.1.3",
20538
20600
  description: "MCP server for live Word document editing via Office Add-in",
20539
20601
  main: "dist/server.js",
20540
20602
  bin: {
@@ -20705,7 +20767,7 @@ function createHttpsServer() {
20705
20767
  };
20706
20768
  const rootDir = import_path.default.join(__dirname, "..");
20707
20769
  return import_https.default.createServer(options, (req, res) => {
20708
- let urlPath = (req.url || "/").split("?")[0];
20770
+ const urlPath = (req.url || "/").split("?")[0];
20709
20771
  let filePath = urlPath === "/" ? "/taskpane.html" : urlPath;
20710
20772
  filePath = import_path.default.resolve(rootDir, "." + filePath);
20711
20773
  if (!filePath.startsWith(rootDir + import_path.default.sep) && filePath !== rootDir) {