mcp-word-bridge 4.0.3 → 4.0.5

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
@@ -67,7 +70,6 @@ Single process. The MCP client spawns the server, which starts both the HTTPS br
67
70
  | `word_set_document_properties` | Set metadata fields |
68
71
  | `word_save` | Save document to disk |
69
72
  | `word_clear` | Clear all document body content |
70
- | `word_create_document` | Create and open a new blank document in a new Word window |
71
73
  | `word_get_word_count` | Get word, character, and paragraph counts |
72
74
  | `word_get_styles` | List available styles |
73
75
  | `word_get_coauthors` | Get co-authoring status and active authors |
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) {
@@ -18784,16 +18784,6 @@ var clear = forwardTool(
18784
18784
  { properties: {} },
18785
18785
  "clearDocument"
18786
18786
  );
18787
- var createDocument = forwardTool(
18788
- "word_create_document",
18789
- "[Document] Create and open a new blank document in a new Word window. Optionally provide base64-encoded .docx as template.",
18790
- {
18791
- properties: {
18792
- base64: { type: "string", description: "Optional base64-encoded .docx file to use as template" }
18793
- }
18794
- },
18795
- "createNewDocument"
18796
- );
18797
18787
  var getWordCount = forwardTool(
18798
18788
  "word_get_word_count",
18799
18789
  "[Document] Get word, character, and paragraph counts.",
@@ -18857,7 +18847,6 @@ var documentTools = [
18857
18847
  setDocumentProperties,
18858
18848
  save,
18859
18849
  clear,
18860
- createDocument,
18861
18850
  getWordCount,
18862
18851
  getStyles,
18863
18852
  getCoauthors,
@@ -18874,9 +18863,23 @@ var ToolError = class extends Error {
18874
18863
  };
18875
18864
 
18876
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
+ function checkNonEmpty(value, name) {
18876
+ if (!value || typeof value !== "string" || value.trim() === "") {
18877
+ throw new ToolError(`${name} must be a non-empty string.`);
18878
+ }
18879
+ }
18877
18880
  function checkNonNegative(value, name) {
18878
- if (typeof value !== "number" || value < 0) {
18879
- throw new ToolError(`${name} must be non-negative.`);
18881
+ if (typeof value !== "number" || value < 0 || !Number.isInteger(value)) {
18882
+ throw new ToolError(`${name} must be a non-negative integer.`);
18880
18883
  }
18881
18884
  }
18882
18885
  function checkBounds(index, count, name) {
@@ -18898,6 +18901,22 @@ function checkOccurrence(occurrence, count) {
18898
18901
  }
18899
18902
  return idx;
18900
18903
  }
18904
+ var MAX_SPACING_POINTS = 1584;
18905
+ var MAX_CUSTOM_PROPERTY_KEY_LENGTH = 255;
18906
+ function checkSpacingBounds(value, name) {
18907
+ if (value > MAX_SPACING_POINTS) {
18908
+ throw new ToolError(
18909
+ `${name} value ${value} exceeds maximum (${MAX_SPACING_POINTS} points = 22 inches). Use a value between 0 and ${MAX_SPACING_POINTS}.`
18910
+ );
18911
+ }
18912
+ }
18913
+ function checkPropertyKeyLength(key) {
18914
+ if (key.length > MAX_CUSTOM_PROPERTY_KEY_LENGTH) {
18915
+ throw new ToolError(
18916
+ `key must be ${MAX_CUSTOM_PROPERTY_KEY_LENGTH} characters or fewer (got ${key.length}).`
18917
+ );
18918
+ }
18919
+ }
18901
18920
 
18902
18921
  // src/server/tools/paragraphs.ts
18903
18922
  var getParagraphs = forwardTool(
@@ -18987,10 +19006,10 @@ var setParagraphStyle = forwardTool(
18987
19006
  },
18988
19007
  "setParagraphStyle"
18989
19008
  );
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
- {
19009
+ var setParagraphSpacing = {
19010
+ name: "word_set_paragraph_spacing",
19011
+ description: "[Paragraphs] Set line spacing, before/after spacing, and indentation on a paragraph by index.",
19012
+ schema: {
18994
19013
  properties: {
18995
19014
  index: { type: "number", description: "Paragraph index (0-based)" },
18996
19015
  lineSpacing: { type: "number", description: "Line spacing in points" },
@@ -19002,8 +19021,26 @@ var setParagraphSpacing = forwardTool(
19002
19021
  },
19003
19022
  required: ["index"]
19004
19023
  },
19005
- "setParagraphSpacing"
19006
- );
19024
+ async handler(args, bridge2) {
19025
+ const index = args.index;
19026
+ checkNonNegative(index, "index");
19027
+ const spacingFields = [
19028
+ ["lineSpacing", args.lineSpacing],
19029
+ ["spaceBefore", args.spaceBefore],
19030
+ ["spaceAfter", args.spaceAfter],
19031
+ ["firstLineIndent", args.firstLineIndent],
19032
+ ["leftIndent", args.leftIndent],
19033
+ ["rightIndent", args.rightIndent]
19034
+ ];
19035
+ for (const [name, value] of spacingFields) {
19036
+ if (value !== void 0 && typeof value === "number") {
19037
+ checkSpacingBounds(value, name);
19038
+ }
19039
+ }
19040
+ const result = await bridge2.send("setParagraphSpacing", args);
19041
+ return jsonResult(result);
19042
+ }
19043
+ };
19007
19044
  var moveParagraph = {
19008
19045
  name: "word_move_paragraph",
19009
19046
  description: "[Paragraphs] Move paragraph(s) to another position. Preserves all rich content including footnotes, hyperlinks, formatting, images, and comments.",
@@ -19023,6 +19060,7 @@ var moveParagraph = {
19023
19060
  const location = args.location ?? "After";
19024
19061
  checkNonNegative(fromIndex, "fromIndex");
19025
19062
  checkNonNegative(toIndex, "toIndex");
19063
+ if (!Number.isInteger(count)) throw new ToolError("count must be an integer.");
19026
19064
  if (count < 1) throw new ToolError("count must be at least 1");
19027
19065
  if (fromIndex === toIndex && count === 1) throw new ToolError("fromIndex and toIndex must be different");
19028
19066
  if (toIndex >= fromIndex && toIndex < fromIndex + count) {
@@ -19033,12 +19071,23 @@ var moveParagraph = {
19033
19071
  checkBounds(fromIndex, total, "fromIndex");
19034
19072
  if (fromIndex + count - 1 >= total) throw new ToolError(`fromIndex + count (${fromIndex + count}) exceeds paragraph count (${total}).`);
19035
19073
  checkBounds(toIndex, total, "toIndex");
19074
+ for (const para of paraCount.paragraphs) {
19075
+ if (para.index >= fromIndex && para.index < fromIndex + count && para.inTable) {
19076
+ throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
19077
+ }
19078
+ }
19079
+ const adjustedTo = fromIndex < toIndex ? toIndex - count : toIndex;
19080
+ if (toIndex === fromIndex + count && location === "After") {
19081
+ return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
19082
+ }
19083
+ if (adjustedTo === fromIndex && location === "After") {
19084
+ return jsonResult({ success: true, warning: "No move performed \u2014 destination is equivalent to source position.", moved: null });
19085
+ }
19036
19086
  const ooxmlResult = await bridge2.send("getParaOoxml", { index: fromIndex, count });
19037
19087
  const savedOoxml = ooxmlResult.ooxml;
19038
19088
  for (let i = count - 1; i >= 0; i--) {
19039
19089
  await bridge2.send("deleteParagraph", { index: fromIndex + i });
19040
19090
  }
19041
- const adjustedTo = fromIndex < toIndex ? toIndex - count : toIndex;
19042
19091
  try {
19043
19092
  await bridge2.send("insertOoxmlAtIndex", { ooxml: savedOoxml, index: adjustedTo, location });
19044
19093
  } catch (insertErr) {
@@ -19071,14 +19120,22 @@ 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;
19077
19127
  checkBounds(fromIndex, total, "fromIndex");
19078
19128
  if (fromIndex + count - 1 >= total) throw new ToolError(`fromIndex + count (${fromIndex + count}) exceeds paragraph count (${total}).`);
19079
19129
  checkBounds(toIndex, total, "toIndex");
19130
+ for (const para of paraCount.paragraphs) {
19131
+ if (para.index >= fromIndex && para.index < fromIndex + count && para.inTable) {
19132
+ throw new ToolError(`Paragraph ${para.index} is inside a table cell. Use table-specific tools to modify table content.`);
19133
+ }
19134
+ }
19080
19135
  const ooxmlResult = await bridge2.send("getParaOoxml", { index: fromIndex, count });
19081
- await bridge2.send("insertOoxmlAtIndex", { ooxml: ooxmlResult.ooxml, index: toIndex, location });
19136
+ const effectiveToIndex = toIndex >= fromIndex && toIndex < fromIndex + count ? fromIndex + count - 1 : toIndex;
19137
+ const effectiveLocation = toIndex >= fromIndex && toIndex < fromIndex + count ? "After" : location;
19138
+ await bridge2.send("insertOoxmlAtIndex", { ooxml: ooxmlResult.ooxml, index: effectiveToIndex, location: effectiveLocation });
19082
19139
  return jsonResult({ success: true, copied: { from: fromIndex, count, to: toIndex, location } });
19083
19140
  }
19084
19141
  };
@@ -19098,7 +19155,7 @@ var paragraphTools = [
19098
19155
  // src/server/tools/search.ts
19099
19156
  var search = forwardTool(
19100
19157
  "word_search",
19101
- "[Search] Find text in the document. Returns match count and up to 30 matches. Query must be \u2264255 chars.",
19158
+ '[Search] Find text in the document. Returns match count and up to 30 matches. Query must be \u2264255 chars. Note: Word search codes (^p = paragraph mark, ^t = tab) are interpreted. To search for literal "^", use "^^".',
19102
19159
  {
19103
19160
  properties: {
19104
19161
  query: { type: "string" },
@@ -19109,10 +19166,10 @@ var search = forwardTool(
19109
19166
  },
19110
19167
  "search"
19111
19168
  );
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
- {
19169
+ var searchAndReplace = {
19170
+ name: "word_search_and_replace",
19171
+ description: "[Search] Find and replace ALL occurrences. For single-paragraph edits, prefer word_replace_paragraph_text.",
19172
+ schema: {
19116
19173
  properties: {
19117
19174
  find: { type: "string" },
19118
19175
  replace: { type: "string" },
@@ -19121,8 +19178,18 @@ var searchAndReplace = forwardTool(
19121
19178
  },
19122
19179
  required: ["find", "replace"]
19123
19180
  },
19124
- "searchAndReplace"
19125
- );
19181
+ async handler(args, bridge2) {
19182
+ const find = args.find;
19183
+ const replace = args.replace;
19184
+ if (!find || typeof find !== "string" || find.trim() === "") {
19185
+ throw new ToolError("find string cannot be empty.");
19186
+ }
19187
+ checkNoSpecialCodes(find, "find");
19188
+ checkNoSpecialCodes(replace, "replace");
19189
+ const result = await bridge2.send("searchAndReplace", args);
19190
+ return jsonResult(result);
19191
+ }
19192
+ };
19126
19193
  var insertTextAtMatch = forwardTool(
19127
19194
  "word_insert_text_at_match",
19128
19195
  '[Search] Insert text before or after a search match. Provide "after" OR "before" as the anchor text. Use occurrence for Nth match.',
@@ -19496,7 +19563,7 @@ var commentTools = [
19496
19563
  // src/server/tools/footnotes.ts
19497
19564
  var insertFootnote = forwardTool(
19498
19565
  "word_insert_footnote",
19499
- "[Footnotes] Insert a footnote anchored to a text match.",
19566
+ "[Footnotes] Insert a footnote anchored to a text match. Note: multiple footnotes on the same anchor appear in reverse insertion order (most recent first). Use word_insert_footnote_at_index for explicit placement.",
19500
19567
  {
19501
19568
  properties: {
19502
19569
  anchorText: { type: "string", description: "Text to search for as anchor point" },
@@ -19703,10 +19770,10 @@ var insertContentControl = forwardTool(
19703
19770
  },
19704
19771
  "insertContentControl"
19705
19772
  );
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
- {
19773
+ var setContentControlText = {
19774
+ name: "word_set_content_control_text",
19775
+ description: "[Content Controls] Set text in a content control identified by ID or tag. Does NOT work on CheckBox controls.",
19776
+ schema: {
19710
19777
  properties: {
19711
19778
  id: { type: "number", description: "Content control ID" },
19712
19779
  tag: { type: "string", description: "Content control tag (alternative to ID)" },
@@ -19714,8 +19781,25 @@ var setContentControlText = forwardTool(
19714
19781
  },
19715
19782
  required: ["text"]
19716
19783
  },
19717
- "setContentControlText"
19718
- );
19784
+ async handler(args, bridge2) {
19785
+ const tag = args.tag;
19786
+ const id = args.id;
19787
+ if (!tag && id === void 0) {
19788
+ throw new ToolError('Provide "id" or "tag" to identify the content control. Use word_get_content_controls to list available controls.');
19789
+ }
19790
+ if (tag) {
19791
+ const ccResult = await bridge2.send("getContentControls", {});
19792
+ const matches = ccResult.controls.filter((c) => c.tag === tag);
19793
+ if (matches.length > 1) {
19794
+ throw new ToolError(
19795
+ `Multiple content controls (${matches.length}) share tag "${tag}". Use "id" instead to target a specific control. Matching IDs: ${matches.map((m) => m.id).join(", ")}.`
19796
+ );
19797
+ }
19798
+ }
19799
+ const result = await bridge2.send("setContentControlText", args);
19800
+ return jsonResult(result);
19801
+ }
19802
+ };
19719
19803
  var contentControlTools = [
19720
19804
  getContentControls,
19721
19805
  insertContentControl,
@@ -19930,18 +20014,24 @@ var getCustomProperties = forwardTool(
19930
20014
  { properties: {} },
19931
20015
  "getCustomProperties"
19932
20016
  );
19933
- var setCustomProperty = forwardTool(
19934
- "word_set_custom_property",
19935
- "[Properties] Set a custom document property. Creates or updates the key-value pair.",
19936
- {
20017
+ var setCustomProperty = {
20018
+ name: "word_set_custom_property",
20019
+ description: "[Properties] Set a custom document property. Creates or updates the key-value pair.",
20020
+ schema: {
19937
20021
  properties: {
19938
20022
  key: { type: "string" },
19939
20023
  value: { type: "string" }
19940
20024
  },
19941
20025
  required: ["key", "value"]
19942
20026
  },
19943
- "setCustomProperty"
19944
- );
20027
+ async handler(args, bridge2) {
20028
+ const key = args.key;
20029
+ checkNonEmpty(key, "key");
20030
+ checkPropertyKeyLength(key);
20031
+ const result = await bridge2.send("setCustomProperty", args);
20032
+ return jsonResult(result);
20033
+ }
20034
+ };
19945
20035
  var deleteCustomProperty = forwardTool(
19946
20036
  "word_delete_custom_property",
19947
20037
  "[Properties] Delete a custom document property by key.",
@@ -20310,7 +20400,7 @@ After inserting a Table of Contents, heading text appears twice (in TOC and body
20310
20400
  // package.json
20311
20401
  var package_default = {
20312
20402
  name: "mcp-word-bridge",
20313
- version: "4.0.3",
20403
+ version: "4.0.5",
20314
20404
  description: "MCP server for live Word document editing via Office Add-in",
20315
20405
  main: "dist/server.js",
20316
20406
  bin: {
@@ -20328,7 +20418,14 @@ var package_default = {
20328
20418
  typecheck: "tsc --noEmit",
20329
20419
  prepublishOnly: "npm run build"
20330
20420
  },
20331
- keywords: ["mcp", "word", "office", "add-in", "document", "editing"],
20421
+ keywords: [
20422
+ "mcp",
20423
+ "word",
20424
+ "office",
20425
+ "add-in",
20426
+ "document",
20427
+ "editing"
20428
+ ],
20332
20429
  repository: {
20333
20430
  type: "git",
20334
20431
  url: "https://github.com/likelion/mcp-word-bridge.git"
@@ -20344,6 +20441,7 @@ var package_default = {
20344
20441
  devDependencies: {
20345
20442
  "@types/node": "^22.0.0",
20346
20443
  "@types/ws": "^8.5.10",
20444
+ "@vitest/coverage-v8": "^3.2.6",
20347
20445
  esbuild: "^0.25.0",
20348
20446
  eslint: "^10.4.1",
20349
20447
  typescript: "^5.7.0",
@@ -20366,7 +20464,7 @@ function createMcpServer(bridge2) {
20366
20464
  { capabilities: { tools: {}, resources: {} } }
20367
20465
  );
20368
20466
  const { tools, handlers } = buildToolRegistry();
20369
- server.setRequestHandler(import_types5.ListToolsRequestSchema, async () => ({
20467
+ server.setRequestHandler(import_types7.ListToolsRequestSchema, async () => ({
20370
20468
  tools: tools.map((t) => ({
20371
20469
  name: t.name,
20372
20470
  description: t.description,
@@ -20377,7 +20475,7 @@ function createMcpServer(bridge2) {
20377
20475
  }
20378
20476
  }))
20379
20477
  }));
20380
- server.setRequestHandler(import_types5.CallToolRequestSchema, async (request) => {
20478
+ server.setRequestHandler(import_types7.CallToolRequestSchema, async (request) => {
20381
20479
  const { name, arguments: args } = request.params;
20382
20480
  const handler = handlers.get(name);
20383
20481
  if (!handler) {
@@ -20391,7 +20489,7 @@ function createMcpServer(bridge2) {
20391
20489
  return { content: [{ type: "text", text: msg }], isError: true };
20392
20490
  }
20393
20491
  });
20394
- server.setRequestHandler(import_types5.ListResourcesRequestSchema, async () => ({
20492
+ server.setRequestHandler(import_types7.ListResourcesRequestSchema, async () => ({
20395
20493
  resources: [{
20396
20494
  uri: "word-bridge://usage-guide",
20397
20495
  name: "Word Bridge Usage Guide",
@@ -20399,7 +20497,7 @@ function createMcpServer(bridge2) {
20399
20497
  mimeType: "text/markdown"
20400
20498
  }]
20401
20499
  }));
20402
- server.setRequestHandler(import_types5.ReadResourceRequestSchema, async (request) => {
20500
+ server.setRequestHandler(import_types7.ReadResourceRequestSchema, async (request) => {
20403
20501
  if (request.params.uri === "word-bridge://usage-guide") {
20404
20502
  return { contents: [{ uri: request.params.uri, mimeType: "text/markdown", text: usageGuide }] };
20405
20503
  }