@tiwater/office-mcp 0.10.1 → 0.11.1

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
@@ -14,6 +14,7 @@ Shared stdio MCP server for Office document workflows.
14
14
  - `office_render_pdf`
15
15
  - `xlsx_inspect`
16
16
  - `xlsx_export_json`
17
+ - `xlsx_apply`
17
18
  - `xlsx_validate`
18
19
  - `pptx_inspect`
19
20
  - `pptx_export_json`
@@ -23,7 +24,7 @@ Shared stdio MCP server for Office document workflows.
23
24
  Install `@tiwater/office-mcp` together with the runtime versions required by
24
25
  the consumer, then run `tiwater-office-mcp` as a stdio MCP server.
25
26
 
26
- Office MCP 0.10 requires these minimum published runtimes on `PATH`:
27
+ Office MCP 0.11 requires these minimum published runtimes on `PATH`:
27
28
 
28
29
  | Command | Package | Minimum version |
29
30
  | --- | --- | --- |
@@ -41,6 +42,16 @@ tool arguments and structured results before they cross the protocol boundary.
41
42
  Large observations and exports are written to a caller-selected new JSON
42
43
  artifact. MCP returns only the artifact path, hash, and byte count.
43
44
 
45
+ ## Workbook editing
46
+
47
+ `xlsx_apply` executes one existing `tiwater.xlsx-edit/v1` artifact against a
48
+ current `.xlsx` workbook. The caller's deterministic builder owns all values,
49
+ coordinates, and operation selection. Office MCP binds the current input,
50
+ operations artifact, and created output by path and content hash and records the
51
+ complete runtime result in a new receipt artifact. It does not interpret
52
+ scenario knowledge or derive workbook edits. Callers independently inspect and
53
+ validate the resulting workbook before delivery.
54
+
44
55
  ## Template migration
45
56
 
46
57
  Template migration separates business choice from document mechanics:
package/office/index.mjs CHANGED
@@ -3,7 +3,6 @@ import { createHash } from 'node:crypto';
3
3
  import { createReadStream } from 'node:fs';
4
4
  import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
5
5
  import path from 'node:path';
6
- import { spawn } from 'node:child_process';
7
6
  import { isDeepStrictEqual } from 'node:util';
8
7
  import { McpServer } from '@modelcontextprotocol/server';
9
8
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
@@ -138,6 +137,39 @@ const artifact = z.object({
138
137
  bytes: z.number().int().nonnegative(),
139
138
  }).strict();
140
139
 
140
+ const xlsxAppliedOperation = z.object({
141
+ type: z.string().min(1),
142
+ applied: z.boolean(),
143
+ detail: z.string(),
144
+ sheet: z.string().nullable().optional(),
145
+ changedRange: z.string().nullable().optional(),
146
+ warnings: z.array(z.string()).nullable().optional(),
147
+ }).strict();
148
+ const xlsxEditResult = z.object({
149
+ input: z.string().min(1),
150
+ output: z.string().min(1),
151
+ appliedOperations: z.array(xlsxAppliedOperation),
152
+ }).strict();
153
+ const xlsxApplyReceipt = z.object({
154
+ schema: z.literal('tiwater.office.xlsx-apply-receipt/v1'),
155
+ pass: z.boolean(),
156
+ input: artifact,
157
+ operations: artifact,
158
+ output: artifact.nullable(),
159
+ appliedOperations: z.array(xlsxAppliedOperation),
160
+ }).strict();
161
+ const xlsxApplyOutput = z.object({
162
+ tool: z.literal('xlsx_apply'),
163
+ runtime: runtimeIdentity,
164
+ receipt: artifact,
165
+ output: artifact.nullable(),
166
+ summary: z.object({
167
+ pass: z.boolean(),
168
+ operationCount: z.number().int().nonnegative(),
169
+ appliedCount: z.number().int().nonnegative(),
170
+ }).strict(),
171
+ }).strict();
172
+
141
173
  const renderFileIdentity = z.object({
142
174
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
143
175
  size_bytes: z.number().int().positive(),
@@ -358,6 +390,18 @@ const tools = [
358
390
  outputSchema: artifactOutput('xlsx_export_json'),
359
391
  handler: xlsxExportJson,
360
392
  },
393
+ {
394
+ name: 'xlsx_apply',
395
+ description: 'Apply one deterministic XLSX operations artifact to a current workbook. This tool executes published workbook edits; it does not derive values, coordinates, or business decisions.',
396
+ inputSchema: z.object({
397
+ input: pathInput.describe('Path to the current XLSX workbook.'),
398
+ operations: pathInput.describe('Path to the deterministic tiwater.xlsx-edit/v1 operations artifact.'),
399
+ output: pathInput.describe('New XLSX output path. Existing files are never overwritten.'),
400
+ receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
401
+ }).strict(),
402
+ outputSchema: xlsxApplyOutput,
403
+ handler: xlsxApply,
404
+ },
361
405
  {
362
406
  name: 'xlsx_validate',
363
407
  description: 'Validate an XLSX workbook package and return Open XML validation evidence.',
@@ -961,9 +1005,59 @@ async function xlsxExportJson(args) {
961
1005
  };
962
1006
  }
963
1007
 
1008
+ async function xlsxApply(args) {
1009
+ const input = path.resolve(requireString(args.input, 'input'));
1010
+ const operations = path.resolve(requireString(args.operations, 'operations'));
1011
+ const output = path.resolve(requireString(args.output, 'output'));
1012
+ const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
1013
+ if (path.extname(input).toLowerCase() !== '.xlsx' || path.extname(output).toLowerCase() !== '.xlsx') {
1014
+ throw Object.assign(new Error('XLSX apply input and output must use the .xlsx extension'), { code: -32602 });
1015
+ }
1016
+ await requireNewFile(output, 'output');
1017
+ await requireNewFile(receiptOutput, 'receiptOutput');
1018
+ const inputArtifact = await fileArtifact(input);
1019
+ const operationsArtifact = await fileArtifact(operations);
1020
+ await mkdir(path.dirname(output), { recursive: true });
1021
+ try {
1022
+ const result = await runJsonCandidateChain(
1023
+ xlsxCandidates,
1024
+ ['edit', input, operations, output],
1025
+ { allowedExitCodes: [0, 1] });
1026
+ const edit = xlsxEditResult.parse(result.json);
1027
+ if (path.resolve(edit.input) !== input || path.resolve(edit.output) !== output) {
1028
+ throw new Error('XLSX edit receipt is not bound to the current input and output');
1029
+ }
1030
+ const pass = edit.appliedOperations.every(operation => operation.applied);
1031
+ const outputArtifact = pass ? await fileArtifact(output) : null;
1032
+ if (!pass) await rm(output, { force: true });
1033
+ const receipt = xlsxApplyReceipt.parse({
1034
+ schema: 'tiwater.office.xlsx-apply-receipt/v1',
1035
+ pass,
1036
+ input: inputArtifact,
1037
+ operations: operationsArtifact,
1038
+ output: outputArtifact,
1039
+ appliedOperations: edit.appliedOperations,
1040
+ });
1041
+ return {
1042
+ tool: 'xlsx_apply',
1043
+ runtime: commandRuntime(result),
1044
+ receipt: await writeJsonArtifact(receiptOutput, receipt),
1045
+ output: outputArtifact,
1046
+ summary: {
1047
+ pass,
1048
+ operationCount: edit.appliedOperations.length,
1049
+ appliedCount: edit.appliedOperations.filter(operation => operation.applied).length,
1050
+ },
1051
+ };
1052
+ } catch (error) {
1053
+ await rm(output, { force: true });
1054
+ throw error;
1055
+ }
1056
+ }
1057
+
964
1058
  async function xlsxValidate(args) {
965
1059
  const input = requireString(args.input, 'input');
966
- const result = await runXlsxValidateCandidateChain(['validate', input]);
1060
+ const result = await runJsonCandidateChain(xlsxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
967
1061
  return { tool: 'xlsx_validate', runtime: commandRuntime(result), result: result.json };
968
1062
  }
969
1063
 
@@ -1030,59 +1124,3 @@ function commandRuntime(result) {
1030
1124
  }
1031
1125
 
1032
1126
  serveStdio(buildServer);
1033
-
1034
- async function runXlsxValidateCandidateChain(args) {
1035
- const errors = [];
1036
- for (const candidate of xlsxCandidates) {
1037
- try {
1038
- const result = await runValidationCommand(candidate, args);
1039
- const text = result.stdout.trim();
1040
- if (!text) return { ...result, json: null };
1041
- try {
1042
- return { ...result, json: JSON.parse(text) };
1043
- } catch {
1044
- if (result.code !== 0) {
1045
- errors.push(`${candidate.command}: validate did not return JSON`);
1046
- continue;
1047
- }
1048
- throw new Error(`Expected JSON output but received: ${text.slice(0, 300)}${text.length > 300 ? '…' : ''}`);
1049
- }
1050
- } catch (error) {
1051
- if (error?.code === 'ENOENT') {
1052
- errors.push(`${candidate.command}: not found`);
1053
- continue;
1054
- }
1055
- throw error;
1056
- }
1057
- }
1058
- throw new Error(`No runnable command candidate succeeded. ${errors.join('; ')}`);
1059
- }
1060
-
1061
- async function runValidationCommand(candidate, args) {
1062
- const env = { ...process.env, ...(candidate.env || {}) };
1063
- const cwd = candidate.cwd || process.cwd();
1064
- const commandArgs = [...(candidate.argsPrefix || []), ...args];
1065
-
1066
- return await new Promise((resolve, reject) => {
1067
- const child = spawn(candidate.command, commandArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
1068
- let stdout = '';
1069
- let stderr = '';
1070
-
1071
- child.stdout.on('data', chunk => {
1072
- stdout += chunk.toString();
1073
- });
1074
-
1075
- child.stderr.on('data', chunk => {
1076
- stderr += chunk.toString();
1077
- });
1078
-
1079
- child.on('error', reject);
1080
- child.on('close', code => {
1081
- if (code === 0 || code === 1) {
1082
- resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs, cwd });
1083
- return;
1084
- }
1085
- reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
1086
- });
1087
- });
1088
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.10.1",
3
+ "version": "0.11.1",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",