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 +4 -3
- package/dist/server.js +86 -15
- package/dist/server.js.map +2 -2
- package/dist/taskpane-app.js +21 -5
- package/dist/taskpane-app.js.map +2 -2
- package/manifest.xml +1 -1
- package/package.json +1 -1
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 (
|
|
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
|
-
|
|
|
303
|
-
|
|
|
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
|
|
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.
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|