@tiwater/office-mcp 0.13.0 → 0.14.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/office/README.md CHANGED
@@ -13,7 +13,8 @@ template-migration workflow, customer mapping, or Lucid lifecycle.
13
13
  - XLS/XLSX: convert legacy XLS with ET, inspect/export/fill, validate, and batch
14
14
  one fixed workbook edit action.
15
15
  - PPTX: inspect/export/fill, bind selected masters/layouts, apply text formatting,
16
- and validate OpenXML.
16
+ set exact top-level object geometry, replace existing picture media, and
17
+ validate OpenXML.
17
18
  - Office: render DOC/DOCX/XLS/XLSX/PPT/PPTX to PDF with the corresponding native
18
19
  WPS backend.
19
20
 
@@ -22,6 +23,30 @@ coordinates and values for that action, so they cannot provide an arbitrary
22
23
  operation discriminator or a multi-action plan language. A call may batch
23
24
  multiple changes only when every change has the same action kind.
24
25
 
26
+ Structural worksheet row deletion is exposed as `xlsx_delete_rows`. Each change
27
+ contains only `sheet`, `startRow`, and `count`; unsupported dependent workbook
28
+ structures fail atomically and are reported by the provider receipt.
29
+
30
+ The bounded DOCX object actions are:
31
+
32
+ - `docx_insert_body_range`: copies inclusive direct-body indexes from a bound
33
+ source DOCX before a target direct-body boundary. Source documents are hashed
34
+ into the receipt. Whole sections preserve supported style, numbering, media,
35
+ hyperlink, header, and footer relationships; partial or unsafe sections fail.
36
+ - `docx_replace_drawing_image`: replaces one body drawing's embedded image while
37
+ preserving its drawing geometry. Image inputs are hashed into the receipt.
38
+ - `docx_insert_body_image`: inserts one inline body drawing with explicit EMU
39
+ dimensions. Image inputs are hashed into the receipt.
40
+ - `docx_set_table_row_repeat_as_header`: sets or unsets native repeat-as-header
41
+ state on uniquely addressed direct body, header, or footer table rows. The
42
+ whole same-action batch is validated before mutation; nested, missing,
43
+ ambiguous, duplicate, or invalid targets fail closed.
44
+
45
+ `docx_inspect_tables` preserves the v1 body `Tables` view and additively exposes
46
+ header/footer topology in `StoryTables`. Header/footer tables carry part
47
+ coordinates plus section/reference bindings, and only supported direct-story
48
+ tables expose mutation addresses.
49
+
25
50
  The catalog is intentionally open to new generic document capabilities, but a
26
51
  scenario, template, customer, issue, work item, or model difference does not
27
52
  justify a new tool. Add a tool only for a stable technical responsibility that
package/office/index.mjs CHANGED
@@ -116,6 +116,32 @@ const pptxFormatApplyOutput = z.object({
116
116
  issueCount: z.number().int().nonnegative(),
117
117
  }).strict(),
118
118
  }).strict();
119
+ const pptxObjectIssue = z.object({
120
+ slideNumber: z.number().int(),
121
+ shapeId: z.number().int().nonnegative().max(0xffffffff),
122
+ message: z.string().min(1),
123
+ }).strict();
124
+ const pptxTransform = z.object({
125
+ x: z.number().int(), y: z.number().int(), cx: z.number().int().positive(), cy: z.number().int().positive(),
126
+ }).strict();
127
+ const pptxShapeGeometryResult = z.object({
128
+ input: z.string().min(1), output: z.string().min(1),
129
+ operationCount: z.number().int().nonnegative(), appliedCount: z.number().int().nonnegative(),
130
+ changes: z.array(z.object({
131
+ slideNumber: z.number().int().positive(), shapeId: z.number().int().positive().max(0xffffffff),
132
+ before: pptxTransform, after: pptxTransform,
133
+ }).strict()),
134
+ issues: z.array(pptxObjectIssue),
135
+ }).strict();
136
+ const pptxPictureImageResult = z.object({
137
+ input: z.string().min(1), output: z.string().min(1),
138
+ operationCount: z.number().int().nonnegative(), appliedCount: z.number().int().nonnegative(),
139
+ changes: z.array(z.object({
140
+ slideNumber: z.number().int().positive(), shapeId: z.number().int().positive().max(0xffffffff), image: z.string().min(1),
141
+ beforeSha256: z.string().regex(/^[0-9a-f]{64}$/), afterSha256: z.string().regex(/^[0-9a-f]{64}$/),
142
+ }).strict()),
143
+ issues: z.array(pptxObjectIssue),
144
+ }).strict();
119
145
 
120
146
  const renderFileIdentity = z.object({
121
147
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
@@ -176,9 +202,19 @@ const tableCellInput = z.object({
176
202
  alignment: z.string().optional(),
177
203
  richText: z.array(richTextSegment).optional(),
178
204
  }).strict();
205
+ const tableRowRepeatBase = {
206
+ tableIndex: index,
207
+ rowIndex: index,
208
+ repeatAsHeader: z.boolean(),
209
+ };
210
+ const tableRowRepeatAddress = z.union([
211
+ z.object(tableRowRepeatBase).strict(),
212
+ z.object({ ...tableRowRepeatBase, headerIndex: index }).strict(),
213
+ z.object({ ...tableRowRepeatBase, footerIndex: index }).strict(),
214
+ ]);
179
215
 
180
- function editAction(name, operationType, description, changeSchema) {
181
- return { name, operationType, description, changeSchema, batch: true };
216
+ function editAction(name, operationType, description, changeSchema, options = {}) {
217
+ return { name, operationType, description, changeSchema, batch: true, ...options };
182
218
  }
183
219
 
184
220
  function documentAction(name, operationType, description) {
@@ -192,6 +228,9 @@ const docxEditActions = [
192
228
  editAction('docx_replace_body_text', 'replaceBodyText', 'Replace uniquely matched current body text.', z.object({ findText: pathInput, text: z.string() }).strict()),
193
229
  editAction('docx_delete_body_paragraph', 'deleteBodyParagraph', 'Delete uniquely matched current body paragraphs.', z.object({ findText: pathInput, ...optionalTextMatch }).strict()),
194
230
  editAction('docx_delete_body_drawing_before_paragraph', 'deleteBodyDrawingBeforeParagraph', 'Delete the drawing immediately before a uniquely matched current paragraph.', z.object({ findText: pathInput, ...optionalTextMatch }).strict()),
231
+ editAction('docx_insert_body_range', 'insertBodyRange', 'Insert a bounded direct-body range from a current source DOCX before a current target body boundary, preserving supported styles and relationships.', z.object({ source: pathInput, sourceStartBodyIndex: index, sourceEndBodyIndex: index, targetBodyIndex: index }).strict(), { sourceFields: ['source'] }),
232
+ editAction('docx_replace_drawing_image', 'replaceDrawingImage', 'Replace the image relationship of a current body drawing while preserving its drawing geometry.', z.object({ paragraphIndex: index, drawingIndex: index, image: pathInput }).strict(), { sourceFields: ['image'] }),
233
+ editAction('docx_insert_body_image', 'insertBodyImage', 'Insert an image as a new inline drawing before a current direct-body boundary.', z.object({ targetBodyIndex: index, image: pathInput, widthEmu: z.number().int().positive(), heightEmu: z.number().int().positive(), altText: z.string().optional() }).strict(), { sourceFields: ['image'] }),
195
234
  editAction('docx_delete_body_range', 'deleteBodyRange', 'Delete uniquely bounded current body ranges.', z.object({ findText: pathInput, endFindText: z.string().optional(), matchMode: z.string().optional(), endMatchMode: z.string().optional(), paragraphStyle: z.string().optional(), endParagraphStyle: z.string().optional(), deleteToBodyEnd: z.boolean().optional(), removePrecedingPageBreak: z.boolean().optional() }).strict()),
196
235
  editAction('docx_start_section', 'startSectionBeforeParagraph', 'Start a section before a uniquely matched current paragraph.', z.object({ findText: pathInput, orientation: z.enum(['portrait', 'landscape']) }).strict()),
197
236
  editAction('docx_set_header_paragraph_text', 'replaceHeaderParagraphText', 'Set current header paragraph text.', z.object({ headerIndex: index, paragraphIndex: index, text: z.string() }).strict()),
@@ -218,6 +257,7 @@ const docxEditActions = [
218
257
  editAction('docx_apply_font_policy', 'applyDocumentFontPolicy', 'Apply an explicit font policy to current document text.', z.object({ fontPolicy: z.object({ schema: pathInput, body: z.record(z.string(), z.string()), table: z.record(z.string(), z.string()) }).strict() }).strict()),
219
258
  editAction('docx_set_table_row_height', 'setTableRowHeight', 'Set current body table row height.', z.object({ tableIndex: index, rowIndex: index, height: pathInput, heightRule: z.string().optional() }).strict()),
220
259
  editAction('docx_set_table_row_cant_split', 'setTableRowCantSplit', 'Set current body table row split behavior.', z.object({ tableIndex: index, rowIndex: index, cantSplit: z.boolean() }).strict()),
260
+ editAction('docx_set_table_row_repeat_as_header', 'setTableRowRepeatAsHeader', 'Set or unset repeat-as-header on uniquely addressed current body, header, or footer table rows.', tableRowRepeatAddress),
221
261
  editAction('docx_set_table_row_keep_next', 'setTableRowKeepNext', 'Set keep-next behavior for current body table rows.', z.object({ tableIndex: index, rowIndex: index, keepNext: z.boolean() }).strict()),
222
262
  editAction('docx_set_body_paragraph_keep_next', 'setBodyParagraphKeepNext', 'Set keep-next behavior for current body paragraphs.', z.object({ paragraphIndex: index, keepNext: z.boolean() }).strict()),
223
263
  editAction('docx_set_body_paragraph_keep_lines', 'setBodyParagraphKeepLines', 'Set keep-lines behavior for current body paragraphs.', z.object({ paragraphIndex: index, keepLines: z.boolean() }).strict()),
@@ -240,6 +280,7 @@ const xlsxEditActions = [
240
280
  editAction('xlsx_set_rich_text_cell_value', 'setRichTextCellValue', 'Set current workbook rich-text cell values.', z.object({ sheet: pathInput, cell: pathInput, value: z.string(), bold: z.boolean() }).strict()),
241
281
  editAction('xlsx_set_range_values', 'setRangeValues', 'Set rectangular values in a current workbook.', z.object({ sheet: pathInput, startCell: pathInput, values: z.array(z.array(scalar)), valueType: z.string().optional() }).strict()),
242
282
  editAction('xlsx_insert_rows', 'insertRows', 'Insert rows into a current worksheet.', z.object({ sheet: pathInput, startRow: positiveIndex, count: positiveIndex, preserveHorizontalMergedRanges: z.boolean().optional(), expandAdjacentVerticalMergedRanges: z.boolean().optional() }).strict()),
283
+ editAction('xlsx_delete_rows', 'deleteRows', 'Structurally delete rows from a current worksheet.', z.object({ sheet: pathInput, startRow: positiveIndex, count: positiveIndex }).strict()),
243
284
  editAction('xlsx_copy_row', 'copyRow', 'Copy current worksheet rows.', z.object({ sheet: pathInput, sourceRow: positiveIndex, targetRow: positiveIndex, translateFormulas: z.boolean().optional() }).strict()),
244
285
  editAction('xlsx_expand_section_rows', 'expandSectionRows', 'Expand current worksheet row sections from visible anchors.', z.object({ sheet: pathInput, anchorText: pathInput, exampleRows: positiveIndex, targetRows: positiveIndex, preserveStyle: z.boolean().optional(), preserveFormulas: z.boolean().optional(), preserveMergedRanges: z.boolean().optional() }).strict()),
245
286
  editAction('xlsx_set_print_area', 'setPrintArea', 'Set current worksheet print areas.', z.object({ sheet: pathInput, range: pathInput }).strict()),
@@ -433,6 +474,30 @@ const tools = [
433
474
  outputSchema: pptxFormatApplyOutput,
434
475
  handler: pptxApplyFormat,
435
476
  },
477
+ {
478
+ name: 'pptx_set_shape_geometry',
479
+ description: 'Set exact native EMU bounds for uniquely identified current-slide PPTX objects. One call batches only this fixed geometry action and does not infer repair coordinates.',
480
+ inputSchema: z.object({
481
+ input: pathInput.describe('Path to the current PPTX.'),
482
+ changes: z.array(z.object({ slideNumber: positiveIndex, shapeId: positiveIndex.max(0xffffffff), x: z.number().int(), y: z.number().int(), cx: positiveIndex, cy: positiveIndex }).strict()).min(1),
483
+ output: pathInput.describe('New PPTX output path. Existing files are never overwritten.'),
484
+ receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
485
+ }).strict(),
486
+ outputSchema: fixedEditOutput('pptx_set_shape_geometry'),
487
+ handler: pptxSetShapeGeometry,
488
+ },
489
+ {
490
+ name: 'pptx_replace_picture_image',
491
+ description: 'Replace embedded PNG or JPEG media for uniquely identified current-slide PPTX pictures while preserving the picture object, geometry, crop, and unrelated media. One call batches only this fixed replacement action.',
492
+ inputSchema: z.object({
493
+ input: pathInput.describe('Path to the current PPTX.'),
494
+ changes: z.array(z.object({ slideNumber: positiveIndex, shapeId: positiveIndex.max(0xffffffff), image: pathInput }).strict()).min(1),
495
+ output: pathInput.describe('New PPTX output path. Existing files are never overwritten.'),
496
+ receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
497
+ }).strict(),
498
+ outputSchema: fixedEditOutput('pptx_replace_picture_image'),
499
+ handler: pptxReplacePictureImage,
500
+ },
436
501
  {
437
502
  name: 'pptx_validate',
438
503
  description: 'Validate a current PPTX package against the published OpenXML contract.',
@@ -537,12 +602,24 @@ async function fixedEdit(action, args) {
537
602
  const operations = action.batch
538
603
  ? args.changes.map(change => ({ ...change, type: action.operationType }))
539
604
  : [{ type: action.operationType }];
605
+ const sourcePaths = [...new Set((action.sourceFields ?? []).flatMap(field =>
606
+ (args.changes ?? []).map(change => path.resolve(requireString(change[field], field)))))];
607
+ const sources = await Promise.all(sourcePaths.map(fileArtifact));
540
608
  const candidates = action.name.startsWith('docx_') ? docxCandidates : xlsxCandidates;
541
609
  return withTempJsonFile({ operations }, async operationsPath => {
542
610
  try {
543
611
  const result = await runJsonCandidateChain(candidates, ['edit', input, operationsPath, output], { allowedExitCodes: [0, 1] });
544
- const appliedOperations = Array.isArray(result.json?.appliedOperations) ? result.json.appliedOperations : [];
545
- const pass = appliedOperations.length === operations.length && appliedOperations.every(operation => operation.applied === true);
612
+ const rawAppliedOperations = result.json?.appliedOperations ?? result.json?.AppliedOperations;
613
+ const appliedOperations = Array.isArray(rawAppliedOperations)
614
+ ? rawAppliedOperations.map(operation => ({
615
+ type: operation.type ?? operation.Type,
616
+ applied: operation.applied ?? operation.Applied,
617
+ detail: operation.detail ?? operation.Detail,
618
+ }))
619
+ : [];
620
+ const observedSources = await Promise.all(sourcePaths.map(fileArtifact));
621
+ const sourceBindingStable = isDeepStrictEqual(sources, observedSources);
622
+ const pass = sourceBindingStable && appliedOperations.length === operations.length && appliedOperations.every(operation => operation.applied === true);
546
623
  const outputArtifact = pass ? await fileArtifact(output) : null;
547
624
  if (!pass) await rm(output, { force: true });
548
625
  const receipt = {
@@ -551,6 +628,8 @@ async function fixedEdit(action, args) {
551
628
  operationType: action.operationType,
552
629
  pass,
553
630
  input: inputArtifact,
631
+ ...(sources.length > 0 ? { sources } : {}),
632
+ ...(sources.length > 0 ? { sourceBindingStable } : {}),
554
633
  output: outputArtifact,
555
634
  operationCount: operations.length,
556
635
  appliedOperations,
@@ -726,6 +805,67 @@ async function pptxApplyFormat(args) {
726
805
  return withTempJsonFile({ operations: args.changes }, planPath => pptxApply('pptx_apply_format', args, false, planPath));
727
806
  }
728
807
 
808
+ async function pptxSetShapeGeometry(args) {
809
+ return pptxFixedObjectEdit('pptx_set_shape_geometry', 'set-shape-geometry', args, pptxShapeGeometryResult, args.changes);
810
+ }
811
+
812
+ async function pptxReplacePictureImage(args) {
813
+ const changes = args.changes.map(change => ({ ...change, image: path.resolve(requireString(change.image, 'image')) }));
814
+ return pptxFixedObjectEdit('pptx_replace_picture_image', 'replace-picture-image', args, pptxPictureImageResult, changes, changes.map(change => change.image));
815
+ }
816
+
817
+ async function pptxFixedObjectEdit(tool, command, args, resultSchema, changes, sourcePaths = []) {
818
+ const input = path.resolve(requireString(args.input, 'input'));
819
+ const output = path.resolve(requireString(args.output, 'output'));
820
+ const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
821
+ if (path.extname(input).toLowerCase() !== '.pptx' || path.extname(output).toLowerCase() !== '.pptx')
822
+ throw Object.assign(new Error('PPTX object edits require .pptx input and output paths'), { code: -32602 });
823
+ await requireNewFile(output, 'output');
824
+ await requireNewFile(receiptOutput, 'receiptOutput');
825
+ const inputArtifact = await fileArtifact(input);
826
+ const sourceArtifacts = await Promise.all([...new Set(sourcePaths)].map(fileArtifact));
827
+ return withTempJsonFile({ changes }, async planPath => {
828
+ const requestArtifact = await fileArtifact(planPath);
829
+ try {
830
+ const result = await runJsonCandidateChain(pptxCandidates, [command, input, planPath, output], { allowedExitCodes: [0, 1] });
831
+ await requireArtifactUnchanged(inputArtifact, 'PPTX object edit input');
832
+ await requireArtifactUnchanged(requestArtifact, 'PPTX object edit request');
833
+ for (const source of sourceArtifacts) await requireArtifactUnchanged(source, 'PPTX replacement image');
834
+ const providerResult = resultSchema.parse(result.json);
835
+ if (path.resolve(providerResult.input) !== input || path.resolve(providerResult.output) !== output)
836
+ throw new Error('PPTX object edit receipt is not bound to the current input and output');
837
+ const sourceByPath = new Map(sourceArtifacts.map(source => [source.path, source]));
838
+ const providerMatchesRequest = providerResult.changes.length === changes.length && providerResult.changes.every((change, position) => {
839
+ const requested = changes[position];
840
+ if (change.slideNumber !== requested.slideNumber || change.shapeId !== requested.shapeId) return false;
841
+ if (tool === 'pptx_set_shape_geometry')
842
+ return isDeepStrictEqual(change.after, { x: requested.x, y: requested.y, cx: requested.cx, cy: requested.cy });
843
+ const requestedImage = path.resolve(requested.image);
844
+ return path.resolve(change.image) === requestedImage && change.afterSha256 === sourceByPath.get(requestedImage)?.sha256;
845
+ });
846
+ const pass = providerResult.issues.length === 0
847
+ && providerResult.operationCount === changes.length
848
+ && providerResult.appliedCount === changes.length
849
+ && providerMatchesRequest;
850
+ const outputArtifact = pass ? await fileArtifact(output) : null;
851
+ if (!pass) await rm(output, { force: true });
852
+ const receipt = {
853
+ schema: 'tiwater.office.pptx-fixed-object-edit-receipt/v1', tool, pass,
854
+ input: inputArtifact, requestSha256: requestArtifact.sha256,
855
+ ...(sourceArtifacts.length ? { sourceImages: sourceArtifacts } : {}),
856
+ output: outputArtifact, providerResult,
857
+ };
858
+ return {
859
+ tool, runtime: commandRuntime(result), receipt: await writeJsonArtifact(receiptOutput, receipt), output: outputArtifact,
860
+ summary: { pass, operationCount: providerResult.operationCount, appliedCount: providerResult.appliedCount },
861
+ };
862
+ } catch (error) {
863
+ await rm(output, { force: true });
864
+ throw error;
865
+ }
866
+ });
867
+ }
868
+
729
869
  async function pptxApply(tool, args, templateMode, plan) {
730
870
  const input = path.resolve(requireString(args.input, 'input'));
731
871
  const template = templateMode ? path.resolve(requireString(args.template, 'template')) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.13.0",
3
+ "version": "0.14.2",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",