mcp-word-bridge 4.0.3 → 4.0.4

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
@@ -1,5 +1,8 @@
1
1
  # MCP Word Bridge
2
2
 
3
+ [![Tests](https://github.com/likelion/mcp-word-bridge/actions/workflows/tests.yml/badge.svg)](https://github.com/likelion/mcp-word-bridge/actions/workflows/tests.yml)
4
+ [![codecov](https://codecov.io/gh/likelion/mcp-word-bridge/branch/main/graph/badge.svg)](https://codecov.io/gh/likelion/mcp-word-bridge)
5
+
3
6
  MCP server for live Word document editing via Office Add-in. Enables programmatic editing of Word documents through the Word JavaScript API, with changes appearing as user edits in co-authoring sessions.
4
7
 
5
8
  ## Quick Start
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_types5 = require("@modelcontextprotocol/sdk/types.js");
18725
+ var import_types7 = require("@modelcontextprotocol/sdk/types.js");
18726
18726
 
18727
18727
  // src/server/tools/helpers.ts
18728
18728
  function jsonResult(data) {
@@ -18874,9 +18874,23 @@ var ToolError = class extends Error {
18874
18874
  };
18875
18875
 
18876
18876
  // src/server/validation.ts
18877
+ 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)/;
18878
+ function checkNoSpecialCodes(text2, paramName) {
18879
+ const match = text2.match(WORD_SPECIAL_CODES);
18880
+ if (match) {
18881
+ throw new ToolError(
18882
+ `${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).`
18883
+ );
18884
+ }
18885
+ }
18886
+ function checkNonEmpty(value, name) {
18887
+ if (!value || typeof value !== "string" || value.trim() === "") {
18888
+ throw new ToolError(`${name} must be a non-empty string.`);
18889
+ }
18890
+ }
18877
18891
  function checkNonNegative(value, name) {
18878
- if (typeof value !== "number" || value < 0) {
18879
- throw new ToolError(`${name} must be non-negative.`);
18892
+ if (typeof value !== "number" || value < 0 || !Number.isInteger(value)) {
18893
+ throw new ToolError(`${name} must be a non-negative integer.`);
18880
18894
  }
18881
18895
  }
18882
18896
  function checkBounds(index, count, name) {
@@ -18898,6 +18912,22 @@ function checkOccurrence(occurrence, count) {
18898
18912
  }
18899
18913
  return idx;
18900
18914
  }
18915
+ var MAX_SPACING_POINTS = 1584;
18916
+ var MAX_CUSTOM_PROPERTY_KEY_LENGTH = 255;
18917
+ function checkSpacingBounds(value, name) {
18918
+ if (value > MAX_SPACING_POINTS) {
18919
+ throw new ToolError(
18920
+ `${name} value ${value} exceeds maximum (${MAX_SPACING_POINTS} points = 22 inches). Use a value between 0 and ${MAX_SPACING_POINTS}.`
18921
+ );
18922
+ }
18923
+ }
18924
+ function checkPropertyKeyLength(key) {
18925
+ if (key.length > MAX_CUSTOM_PROPERTY_KEY_LENGTH) {
18926
+ throw new ToolError(
18927
+ `key must be ${MAX_CUSTOM_PROPERTY_KEY_LENGTH} characters or fewer (got ${key.length}).`
18928
+ );
18929
+ }
18930
+ }
18901
18931
 
18902
18932
  // src/server/tools/paragraphs.ts
18903
18933
  var getParagraphs = forwardTool(
@@ -18987,10 +19017,10 @@ var setParagraphStyle = forwardTool(
18987
19017
  },
18988
19018
  "setParagraphStyle"
18989
19019
  );
18990
- var setParagraphSpacing = forwardTool(
18991
- "word_set_paragraph_spacing",
18992
- "[Paragraphs] Set line spacing, before/after spacing, and indentation on a paragraph by index.",
18993
- {
19020
+ var setParagraphSpacing = {
19021
+ name: "word_set_paragraph_spacing",
19022
+ description: "[Paragraphs] Set line spacing, before/after spacing, and indentation on a paragraph by index.",
19023
+ schema: {
18994
19024
  properties: {
18995
19025
  index: { type: "number", description: "Paragraph index (0-based)" },
18996
19026
  lineSpacing: { type: "number", description: "Line spacing in points" },
@@ -19002,8 +19032,26 @@ var setParagraphSpacing = forwardTool(
19002
19032
  },
19003
19033
  required: ["index"]
19004
19034
  },
19005
- "setParagraphSpacing"
19006
- );
19035
+ async handler(args, bridge2) {
19036
+ const index = args.index;
19037
+ checkNonNegative(index, "index");
19038
+ const spacingFields = [
19039
+ ["lineSpacing", args.lineSpacing],
19040
+ ["spaceBefore", args.spaceBefore],
19041
+ ["spaceAfter", args.spaceAfter],
19042
+ ["firstLineIndent", args.firstLineIndent],
19043
+ ["leftIndent", args.leftIndent],
19044
+ ["rightIndent", args.rightIndent]
19045
+ ];
19046
+ for (const [name, value] of spacingFields) {
19047
+ if (value !== void 0 && typeof value === "number") {
19048
+ checkSpacingBounds(value, name);
19049
+ }
19050
+ }
19051
+ const result = await bridge2.send("setParagraphSpacing", args);
19052
+ return jsonResult(result);
19053
+ }
19054
+ };
19007
19055
  var moveParagraph = {
19008
19056
  name: "word_move_paragraph",
19009
19057
  description: "[Paragraphs] Move paragraph(s) to another position. Preserves all rich content including footnotes, hyperlinks, formatting, images, and comments.",
@@ -19023,6 +19071,7 @@ var moveParagraph = {
19023
19071
  const location = args.location ?? "After";
19024
19072
  checkNonNegative(fromIndex, "fromIndex");
19025
19073
  checkNonNegative(toIndex, "toIndex");
19074
+ if (!Number.isInteger(count)) throw new ToolError("count must be an integer.");
19026
19075
  if (count < 1) throw new ToolError("count must be at least 1");
19027
19076
  if (fromIndex === toIndex && count === 1) throw new ToolError("fromIndex and toIndex must be different");
19028
19077
  if (toIndex >= fromIndex && toIndex < fromIndex + count) {
@@ -19071,6 +19120,7 @@ var copyParagraph = {
19071
19120
  const location = args.location ?? "After";
19072
19121
  checkNonNegative(fromIndex, "fromIndex");
19073
19122
  checkNonNegative(toIndex, "toIndex");
19123
+ if (!Number.isInteger(count)) throw new ToolError("count must be an integer.");
19074
19124
  if (count < 1) throw new ToolError("count must be at least 1");
19075
19125
  const paraCount = await bridge2.send("getParagraphs", {});
19076
19126
  const total = paraCount.count;
@@ -19109,10 +19159,10 @@ var search = forwardTool(
19109
19159
  },
19110
19160
  "search"
19111
19161
  );
19112
- var searchAndReplace = forwardTool(
19113
- "word_search_and_replace",
19114
- "[Search] Find and replace ALL occurrences. For single-paragraph edits, prefer word_replace_paragraph_text.",
19115
- {
19162
+ var searchAndReplace = {
19163
+ name: "word_search_and_replace",
19164
+ description: "[Search] Find and replace ALL occurrences. For single-paragraph edits, prefer word_replace_paragraph_text.",
19165
+ schema: {
19116
19166
  properties: {
19117
19167
  find: { type: "string" },
19118
19168
  replace: { type: "string" },
@@ -19121,8 +19171,18 @@ var searchAndReplace = forwardTool(
19121
19171
  },
19122
19172
  required: ["find", "replace"]
19123
19173
  },
19124
- "searchAndReplace"
19125
- );
19174
+ async handler(args, bridge2) {
19175
+ const find = args.find;
19176
+ const replace = args.replace;
19177
+ if (!find || typeof find !== "string" || find.trim() === "") {
19178
+ throw new ToolError("find string cannot be empty.");
19179
+ }
19180
+ checkNoSpecialCodes(find, "find");
19181
+ checkNoSpecialCodes(replace, "replace");
19182
+ const result = await bridge2.send("searchAndReplace", args);
19183
+ return jsonResult(result);
19184
+ }
19185
+ };
19126
19186
  var insertTextAtMatch = forwardTool(
19127
19187
  "word_insert_text_at_match",
19128
19188
  '[Search] Insert text before or after a search match. Provide "after" OR "before" as the anchor text. Use occurrence for Nth match.',
@@ -19703,10 +19763,10 @@ var insertContentControl = forwardTool(
19703
19763
  },
19704
19764
  "insertContentControl"
19705
19765
  );
19706
- var setContentControlText = forwardTool(
19707
- "word_set_content_control_text",
19708
- "[Content Controls] Set text in a content control identified by ID or tag. Does NOT work on CheckBox controls.",
19709
- {
19766
+ var setContentControlText = {
19767
+ name: "word_set_content_control_text",
19768
+ description: "[Content Controls] Set text in a content control identified by ID or tag. Does NOT work on CheckBox controls.",
19769
+ schema: {
19710
19770
  properties: {
19711
19771
  id: { type: "number", description: "Content control ID" },
19712
19772
  tag: { type: "string", description: "Content control tag (alternative to ID)" },
@@ -19714,8 +19774,25 @@ var setContentControlText = forwardTool(
19714
19774
  },
19715
19775
  required: ["text"]
19716
19776
  },
19717
- "setContentControlText"
19718
- );
19777
+ async handler(args, bridge2) {
19778
+ const tag = args.tag;
19779
+ const id = args.id;
19780
+ if (!tag && id === void 0) {
19781
+ throw new ToolError('Provide "id" or "tag" to identify the content control. Use word_get_content_controls to list available controls.');
19782
+ }
19783
+ if (tag) {
19784
+ const ccResult = await bridge2.send("getContentControls", {});
19785
+ const matches = ccResult.controls.filter((c) => c.tag === tag);
19786
+ if (matches.length > 1) {
19787
+ throw new ToolError(
19788
+ `Multiple content controls (${matches.length}) share tag "${tag}". Use "id" instead to target a specific control. Matching IDs: ${matches.map((m) => m.id).join(", ")}.`
19789
+ );
19790
+ }
19791
+ }
19792
+ const result = await bridge2.send("setContentControlText", args);
19793
+ return jsonResult(result);
19794
+ }
19795
+ };
19719
19796
  var contentControlTools = [
19720
19797
  getContentControls,
19721
19798
  insertContentControl,
@@ -19930,18 +20007,24 @@ var getCustomProperties = forwardTool(
19930
20007
  { properties: {} },
19931
20008
  "getCustomProperties"
19932
20009
  );
19933
- var setCustomProperty = forwardTool(
19934
- "word_set_custom_property",
19935
- "[Properties] Set a custom document property. Creates or updates the key-value pair.",
19936
- {
20010
+ var setCustomProperty = {
20011
+ name: "word_set_custom_property",
20012
+ description: "[Properties] Set a custom document property. Creates or updates the key-value pair.",
20013
+ schema: {
19937
20014
  properties: {
19938
20015
  key: { type: "string" },
19939
20016
  value: { type: "string" }
19940
20017
  },
19941
20018
  required: ["key", "value"]
19942
20019
  },
19943
- "setCustomProperty"
19944
- );
20020
+ async handler(args, bridge2) {
20021
+ const key = args.key;
20022
+ checkNonEmpty(key, "key");
20023
+ checkPropertyKeyLength(key);
20024
+ const result = await bridge2.send("setCustomProperty", args);
20025
+ return jsonResult(result);
20026
+ }
20027
+ };
19945
20028
  var deleteCustomProperty = forwardTool(
19946
20029
  "word_delete_custom_property",
19947
20030
  "[Properties] Delete a custom document property by key.",
@@ -20310,7 +20393,7 @@ After inserting a Table of Contents, heading text appears twice (in TOC and body
20310
20393
  // package.json
20311
20394
  var package_default = {
20312
20395
  name: "mcp-word-bridge",
20313
- version: "4.0.3",
20396
+ version: "4.0.4",
20314
20397
  description: "MCP server for live Word document editing via Office Add-in",
20315
20398
  main: "dist/server.js",
20316
20399
  bin: {
@@ -20328,7 +20411,14 @@ var package_default = {
20328
20411
  typecheck: "tsc --noEmit",
20329
20412
  prepublishOnly: "npm run build"
20330
20413
  },
20331
- keywords: ["mcp", "word", "office", "add-in", "document", "editing"],
20414
+ keywords: [
20415
+ "mcp",
20416
+ "word",
20417
+ "office",
20418
+ "add-in",
20419
+ "document",
20420
+ "editing"
20421
+ ],
20332
20422
  repository: {
20333
20423
  type: "git",
20334
20424
  url: "https://github.com/likelion/mcp-word-bridge.git"
@@ -20344,6 +20434,7 @@ var package_default = {
20344
20434
  devDependencies: {
20345
20435
  "@types/node": "^22.0.0",
20346
20436
  "@types/ws": "^8.5.10",
20437
+ "@vitest/coverage-v8": "^3.2.6",
20347
20438
  esbuild: "^0.25.0",
20348
20439
  eslint: "^10.4.1",
20349
20440
  typescript: "^5.7.0",
@@ -20366,7 +20457,7 @@ function createMcpServer(bridge2) {
20366
20457
  { capabilities: { tools: {}, resources: {} } }
20367
20458
  );
20368
20459
  const { tools, handlers } = buildToolRegistry();
20369
- server.setRequestHandler(import_types5.ListToolsRequestSchema, async () => ({
20460
+ server.setRequestHandler(import_types7.ListToolsRequestSchema, async () => ({
20370
20461
  tools: tools.map((t) => ({
20371
20462
  name: t.name,
20372
20463
  description: t.description,
@@ -20377,7 +20468,7 @@ function createMcpServer(bridge2) {
20377
20468
  }
20378
20469
  }))
20379
20470
  }));
20380
- server.setRequestHandler(import_types5.CallToolRequestSchema, async (request) => {
20471
+ server.setRequestHandler(import_types7.CallToolRequestSchema, async (request) => {
20381
20472
  const { name, arguments: args } = request.params;
20382
20473
  const handler = handlers.get(name);
20383
20474
  if (!handler) {
@@ -20391,7 +20482,7 @@ function createMcpServer(bridge2) {
20391
20482
  return { content: [{ type: "text", text: msg }], isError: true };
20392
20483
  }
20393
20484
  });
20394
- server.setRequestHandler(import_types5.ListResourcesRequestSchema, async () => ({
20485
+ server.setRequestHandler(import_types7.ListResourcesRequestSchema, async () => ({
20395
20486
  resources: [{
20396
20487
  uri: "word-bridge://usage-guide",
20397
20488
  name: "Word Bridge Usage Guide",
@@ -20399,7 +20490,7 @@ function createMcpServer(bridge2) {
20399
20490
  mimeType: "text/markdown"
20400
20491
  }]
20401
20492
  }));
20402
- server.setRequestHandler(import_types5.ReadResourceRequestSchema, async (request) => {
20493
+ server.setRequestHandler(import_types7.ReadResourceRequestSchema, async (request) => {
20403
20494
  if (request.params.uri === "word-bridge://usage-guide") {
20404
20495
  return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: usageGuide }] };
20405
20496
  }