mcp-word-bridge 4.1.1 → 4.1.2

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,9 @@ 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
+ | 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
+ | 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
305
 
305
306
  ## License
306
307
 
package/dist/server.js CHANGED
@@ -18722,7 +18722,7 @@ var Bridge = class {
18722
18722
 
18723
18723
  // src/server/mcp.ts
18724
18724
  var import_server = require("@modelcontextprotocol/sdk/server/index.js");
18725
- var import_types8 = require("@modelcontextprotocol/sdk/types.js");
18725
+ var import_types9 = require("@modelcontextprotocol/sdk/types.js");
18726
18726
 
18727
18727
  // src/server/types.ts
18728
18728
  var ToolError = class extends Error {
@@ -18736,13 +18736,14 @@ var ToolError = class extends Error {
18736
18736
  function jsonResult(data) {
18737
18737
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
18738
18738
  }
18739
- function forwardTool(name, description, schema, action) {
18739
+ function forwardTool(name, description, schema, action, validate) {
18740
18740
  return {
18741
18741
  name,
18742
18742
  description,
18743
18743
  schema,
18744
18744
  bridgeAction: action,
18745
18745
  async handler(args, bridge2) {
18746
+ if (validate) validate(args);
18746
18747
  const result = await bridge2.send(action, args);
18747
18748
  return jsonResult(result);
18748
18749
  }
@@ -18788,7 +18789,7 @@ var save = forwardTool(
18788
18789
  );
18789
18790
  var clear = forwardTool(
18790
18791
  "word_clear",
18791
- "[Document] Clear all document body content. Does not clear headers/footers or custom properties.",
18792
+ "[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
18793
  { properties: {} },
18793
18794
  "clearDocument"
18794
18795
  );
@@ -18929,6 +18930,11 @@ function checkPropertyKeyLength(key) {
18929
18930
  );
18930
18931
  }
18931
18932
  }
18933
+ function checkHexColor(color, name) {
18934
+ if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
18935
+ throw new ToolError(`${name} must be a valid hex color (e.g. #FF0000).`);
18936
+ }
18937
+ }
18932
18938
 
18933
18939
  // src/server/tools/paragraphs.ts
18934
18940
  var getParagraphs = forwardTool(
@@ -18944,7 +18950,7 @@ var getParagraphs = forwardTool(
18944
18950
  );
18945
18951
  var getParagraphByIndex = forwardTool(
18946
18952
  "word_get_paragraph_by_index",
18947
- "[Paragraphs] Get full details of a single paragraph including font, spacing, indentation, and outline level.",
18953
+ "[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
18954
  {
18949
18955
  properties: {
18950
18956
  index: { type: "number", description: "Paragraph index (0-based)" }
@@ -19036,6 +19042,12 @@ var setParagraphSpacing = {
19036
19042
  async handler(args, bridge2) {
19037
19043
  const index = args.index;
19038
19044
  checkNonNegative(index, "index");
19045
+ 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;
19046
+ if (!hasProperty) {
19047
+ throw new ToolError(
19048
+ "At least one spacing or indent property must be provided (lineSpacing, spaceBefore, spaceAfter, firstLineIndent, leftIndent, rightIndent)."
19049
+ );
19050
+ }
19039
19051
  const spacingFields = [
19040
19052
  ["lineSpacing", args.lineSpacing],
19041
19053
  ["spaceBefore", args.spaceBefore],
@@ -19095,6 +19107,12 @@ var moveParagraph = {
19095
19107
  throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
19096
19108
  }
19097
19109
  }
19110
+ const destPara = paraCount.paragraphs.find((p) => p.index === toIndex);
19111
+ if (destPara?.inTable) {
19112
+ throw new ToolError(
19113
+ `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.`
19114
+ );
19115
+ }
19098
19116
  const lastIdx = total - 1;
19099
19117
  if (fromIndex + count - 1 === lastIdx) {
19100
19118
  const lastPara = paraCount.paragraphs.find((p) => p.index === lastIdx);
@@ -19160,6 +19178,12 @@ var copyParagraph = {
19160
19178
  throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
19161
19179
  }
19162
19180
  }
19181
+ const destPara = paraCount.paragraphs.find((p) => p.index === toIndex);
19182
+ if (destPara?.inTable) {
19183
+ throw new ToolError(
19184
+ `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.`
19185
+ );
19186
+ }
19163
19187
  const ooxmlResult = await bridge2.send("getParaOoxml", { index: fromIndex, count });
19164
19188
  const effectiveToIndex = toIndex >= fromIndex && toIndex < fromIndex + count ? fromIndex + count - 1 : toIndex;
19165
19189
  const effectiveLocation = toIndex >= fromIndex && toIndex < fromIndex + count ? "After" : location;
@@ -19228,7 +19252,7 @@ var searchAndReplace = {
19228
19252
  };
19229
19253
  var insertTextAtMatch = forwardTool(
19230
19254
  "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.',
19255
+ '[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
19256
  {
19233
19257
  properties: {
19234
19258
  text: { type: "string", description: "Text to insert" },
@@ -19288,6 +19312,34 @@ var searchTools = [
19288
19312
  ];
19289
19313
 
19290
19314
  // 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
+ function validateFormatText(args) {
19317
+ checkNonEmpty(args.text, "text");
19318
+ 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;
19319
+ if (!hasFormatting) {
19320
+ throw new ToolError(
19321
+ "At least one formatting property must be specified (bold, italic, underline, strikeThrough, color, highlightColor, size, or name)."
19322
+ );
19323
+ }
19324
+ if (args.size !== void 0) {
19325
+ const size = args.size;
19326
+ if (size <= 0) throw new ToolError("size must be positive (minimum 1 point).");
19327
+ if (size > 1638) throw new ToolError("size must not exceed 1638 points (Word maximum).");
19328
+ if (!Number.isFinite(size)) throw new ToolError("size must be a finite number.");
19329
+ }
19330
+ if (args.color !== void 0) {
19331
+ checkHexColor(args.color, "color");
19332
+ }
19333
+ if (args.highlightColor !== void 0) {
19334
+ const hc = args.highlightColor;
19335
+ const isNamed = HIGHLIGHT_COLORS.some((c) => c.toLowerCase() === hc.toLowerCase());
19336
+ if (!isNamed) {
19337
+ throw new ToolError(
19338
+ `Invalid highlightColor: "${hc}". Valid values: ${HIGHLIGHT_COLORS.join(", ")}.`
19339
+ );
19340
+ }
19341
+ }
19342
+ }
19291
19343
  var formatText = forwardTool(
19292
19344
  "word_format_text",
19293
19345
  "[Formatting] Apply formatting (bold, italic, color, size, font) to a text match. Color must be hex (#FF0000). Size: 1-1638pt.",
@@ -19307,7 +19359,8 @@ var formatText = forwardTool(
19307
19359
  },
19308
19360
  required: ["text"]
19309
19361
  },
19310
- "formatRange"
19362
+ "formatRange",
19363
+ validateFormatText
19311
19364
  );
19312
19365
  var clearFormatting = forwardTool(
19313
19366
  "word_clear_formatting",
@@ -19486,7 +19539,7 @@ var tableTools = [
19486
19539
  // src/server/tools/lists.ts
19487
19540
  var insertList = forwardTool(
19488
19541
  "word_insert_list",
19489
- "[Lists] Insert a bulleted or numbered list from an array of item strings.",
19542
+ "[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
19543
  {
19491
19544
  properties: {
19492
19545
  items: { type: "array", items: { type: "string" }, description: "List item strings" },
@@ -19539,7 +19592,11 @@ var addComment = forwardTool(
19539
19592
  },
19540
19593
  required: ["anchorText", "comment"]
19541
19594
  },
19542
- "addComment"
19595
+ "addComment",
19596
+ (args) => {
19597
+ checkNonEmpty(args.anchorText, "anchorText");
19598
+ checkNonEmpty(args.comment, "comment");
19599
+ }
19543
19600
  );
19544
19601
  var getComments = forwardTool(
19545
19602
  "word_get_comments",
@@ -19568,7 +19625,10 @@ var replyToComment = forwardTool(
19568
19625
  },
19569
19626
  required: ["commentId", "text"]
19570
19627
  },
19571
- "replyToComment"
19628
+ "replyToComment",
19629
+ (args) => {
19630
+ checkNonEmpty(args.text, "text");
19631
+ }
19572
19632
  );
19573
19633
  var resolveComment = forwardTool(
19574
19634
  "word_resolve_comment",
@@ -20202,7 +20262,7 @@ var equationTools = [
20202
20262
  function createBatchTool(registry, actionMap) {
20203
20263
  return {
20204
20264
  name: "word_batch",
20205
- description: "[Batch] Execute multiple operations in a single call. Operations execute sequentially \u2014 if one fails, subsequent are skipped.",
20265
+ 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
20266
  schema: {
20207
20267
  properties: {
20208
20268
  operations: {
@@ -20398,6 +20458,8 @@ Controls a live Word document. All operations execute immediately.
20398
20458
  \`\`\`
20399
20459
  Runs sequentially. Stops on first error. Maximum 50 per batch. Prefer batching over individual calls.
20400
20460
 
20461
+ **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.
20462
+
20401
20463
  ## Search
20402
20464
 
20403
20465
  - Case-insensitive by default. Pass \`matchCase: true\` for exact case.
@@ -20458,12 +20520,21 @@ After inserting a Table of Contents, heading text appears twice (in TOC and body
20458
20520
  6. Use \`word_copy_paragraph\` to duplicate (preserves everything)
20459
20521
  7. Save explicitly after significant changes
20460
20522
  8. Resolve comments rather than deleting (preserves audit trail)
20523
+
20524
+ ## Alignment Values
20525
+
20526
+ Input accepts case-insensitive: "left", "center", "right", "justified".
20527
+ Output always uses canonical form: "Left", "Center", "Right", "Justified".
20528
+
20529
+ ## Mixed Formatting
20530
+
20531
+ \`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
20532
  `;
20462
20533
 
20463
20534
  // package.json
20464
20535
  var package_default = {
20465
20536
  name: "mcp-word-bridge",
20466
- version: "4.1.1",
20537
+ version: "4.1.2",
20467
20538
  description: "MCP server for live Word document editing via Office Add-in",
20468
20539
  main: "dist/server.js",
20469
20540
  bin: {
@@ -20528,7 +20599,7 @@ function createMcpServer(bridge2) {
20528
20599
  );
20529
20600
  const { tools, handlers } = buildToolRegistry();
20530
20601
  const toolMutex = createMutex();
20531
- server.setRequestHandler(import_types8.ListToolsRequestSchema, async () => ({
20602
+ server.setRequestHandler(import_types9.ListToolsRequestSchema, async () => ({
20532
20603
  tools: tools.map((t) => ({
20533
20604
  name: t.name,
20534
20605
  description: t.description,
@@ -20539,7 +20610,7 @@ function createMcpServer(bridge2) {
20539
20610
  }
20540
20611
  }))
20541
20612
  }));
20542
- server.setRequestHandler(import_types8.CallToolRequestSchema, async (request) => {
20613
+ server.setRequestHandler(import_types9.CallToolRequestSchema, async (request) => {
20543
20614
  return toolMutex.run(async () => {
20544
20615
  const { name, arguments: args } = request.params;
20545
20616
  const handler = handlers.get(name);
@@ -20555,7 +20626,7 @@ function createMcpServer(bridge2) {
20555
20626
  }
20556
20627
  });
20557
20628
  });
20558
- server.setRequestHandler(import_types8.ListResourcesRequestSchema, async () => ({
20629
+ server.setRequestHandler(import_types9.ListResourcesRequestSchema, async () => ({
20559
20630
  resources: [{
20560
20631
  uri: "word-bridge://usage-guide",
20561
20632
  name: "Word Bridge Usage Guide",
@@ -20563,7 +20634,7 @@ function createMcpServer(bridge2) {
20563
20634
  mimeType: "text/markdown"
20564
20635
  }]
20565
20636
  }));
20566
- server.setRequestHandler(import_types8.ReadResourceRequestSchema, async (request) => {
20637
+ server.setRequestHandler(import_types9.ReadResourceRequestSchema, async (request) => {
20567
20638
  if (request.params.uri === "word-bridge://usage-guide") {
20568
20639
  return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: usageGuide }] };
20569
20640
  }