@tiwater/office-mcp 0.11.0 → 0.12.0

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
@@ -18,19 +18,21 @@ Shared stdio MCP server for Office document workflows.
18
18
  - `xlsx_validate`
19
19
  - `pptx_inspect`
20
20
  - `pptx_export_json`
21
+ - `pptx_apply_template`
22
+ - `pptx_apply_format`
21
23
 
22
24
  ## Run
23
25
 
24
26
  Install `@tiwater/office-mcp` together with the runtime versions required by
25
27
  the consumer, then run `tiwater-office-mcp` as a stdio MCP server.
26
28
 
27
- Office MCP 0.11 requires these minimum published runtimes on `PATH`:
29
+ Office MCP 0.12 requires these minimum published runtimes on `PATH`:
28
30
 
29
31
  | Command | Package | Minimum version |
30
32
  | --- | --- | --- |
31
33
  | `tiwater-docx` | NuGet `tiwater.docx.cli` | 0.15.0 |
32
34
  | `tiwater-xlsx` | NuGet `tiwater.xlsx.cli` | 0.2.55 |
33
- | `tiwater-pptx` | NuGet `tiwater.pptx.cli` | 0.2.22 |
35
+ | `tiwater-pptx` | NuGet `tiwater.pptx.cli` | 0.3.1 |
34
36
  | `tiwater-convert` | NuGet `tiwater.convert.cli` | 0.9.22 |
35
37
 
36
38
  The server invokes published `tiwater-docx`, `tiwater-xlsx`,
@@ -52,6 +54,21 @@ complete runtime result in a new receipt artifact. It does not interpret
52
54
  scenario knowledge or derive workbook edits. Callers independently inspect and
53
55
  validate the resulting workbook before delivery.
54
56
 
57
+ ## Presentation editing
58
+
59
+ `pptx_apply_template` and `pptx_apply_format` are orthogonal execution
60
+ operations. The first applies a deterministic template plan to a current
61
+ presentation and selected current template. The second applies deterministic
62
+ format operations to a current presentation. Both bind their inputs, created
63
+ output, and complete runtime result by path and content hash. They do not select
64
+ templates, interpret scenario knowledge, or derive business content, slide
65
+ mappings, geometry, coordinates, or formatting decisions.
66
+
67
+ Callers apply a template first, inspect and validate that intermediate result,
68
+ then build and apply formatting against the intermediate presentation. Final
69
+ inspection, independent readback, native rendering, and delivery closure remain
70
+ separate operations.
71
+
55
72
  ## Template migration
56
73
 
57
74
  Template migration separates business choice from document mechanics:
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "tiwater.office.pptx-format-apply-receipt/v1",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["schema", "pass", "input", "operations", "output", "providerResult"],
7
+ "properties": {
8
+ "schema": { "const": "tiwater.office.pptx-format-apply-receipt/v1" },
9
+ "pass": { "type": "boolean" },
10
+ "input": { "$ref": "#/$defs/artifact" },
11
+ "operations": { "$ref": "#/$defs/artifact" },
12
+ "output": { "oneOf": [{ "$ref": "#/$defs/artifact" }, { "type": "null" }] },
13
+ "providerResult": {
14
+ "type": "object",
15
+ "additionalProperties": false,
16
+ "required": ["input", "output", "operationCount", "changedCount", "changes", "issues"],
17
+ "properties": {
18
+ "input": { "type": "string", "minLength": 1 },
19
+ "output": { "type": "string", "minLength": 1 },
20
+ "operationCount": { "type": "integer", "minimum": 0 },
21
+ "changedCount": { "type": "integer", "minimum": 0 },
22
+ "changes": { "type": "array", "items": { "$ref": "#/$defs/change" } },
23
+ "issues": { "type": "array", "items": { "$ref": "#/$defs/issue" } }
24
+ }
25
+ }
26
+ },
27
+ "$defs": {
28
+ "artifact": {
29
+ "type": "object",
30
+ "additionalProperties": false,
31
+ "required": ["path", "sha256", "bytes"],
32
+ "properties": {
33
+ "path": { "type": "string", "minLength": 1 },
34
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
35
+ "bytes": { "type": "integer", "minimum": 0 }
36
+ }
37
+ },
38
+ "change": {
39
+ "type": "object",
40
+ "additionalProperties": false,
41
+ "required": ["slideNumber", "shapeId", "runIndex", "properties"],
42
+ "properties": {
43
+ "slideNumber": { "type": "integer", "minimum": 1 },
44
+ "shapeId": { "type": "integer", "minimum": 0, "maximum": 4294967295 },
45
+ "runIndex": { "type": "integer", "minimum": 0 },
46
+ "properties": { "type": "array", "items": { "type": "string" } }
47
+ }
48
+ },
49
+ "issue": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "required": ["slideNumber", "shapeId", "runIndex", "message"],
53
+ "properties": {
54
+ "slideNumber": { "type": "integer", "minimum": 1 },
55
+ "shapeId": { "type": "integer", "minimum": 0, "maximum": 4294967295 },
56
+ "runIndex": { "type": "integer", "minimum": 0 },
57
+ "message": { "type": "string" }
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,72 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "tiwater.office.pptx-template-apply-receipt/v1",
4
+ "type": "object",
5
+ "additionalProperties": false,
6
+ "required": ["schema", "pass", "input", "template", "plan", "output", "providerResult"],
7
+ "properties": {
8
+ "schema": { "const": "tiwater.office.pptx-template-apply-receipt/v1" },
9
+ "pass": { "type": "boolean" },
10
+ "input": { "$ref": "#/$defs/artifact" },
11
+ "template": { "$ref": "#/$defs/artifact" },
12
+ "plan": { "$ref": "#/$defs/artifact" },
13
+ "output": { "oneOf": [{ "$ref": "#/$defs/artifact" }, { "type": "null" }] },
14
+ "providerResult": {
15
+ "type": "object",
16
+ "additionalProperties": false,
17
+ "required": ["input", "template", "output", "changedSlideCount", "issues", "materializedLayoutShapes", "frozenPlaceholderCount", "removedSystemPlaceholders"],
18
+ "properties": {
19
+ "input": { "type": "string", "minLength": 1 },
20
+ "template": { "type": "string", "minLength": 1 },
21
+ "output": { "type": "string", "minLength": 1 },
22
+ "changedSlideCount": { "type": "integer", "minimum": 0 },
23
+ "issues": { "type": "array", "items": { "$ref": "#/$defs/issue" } },
24
+ "materializedLayoutShapes": { "type": "array", "items": { "$ref": "#/$defs/materializedShape" } },
25
+ "frozenPlaceholderCount": { "type": "integer", "minimum": 0 },
26
+ "removedSystemPlaceholders": { "type": "array", "items": { "$ref": "#/$defs/removedPlaceholder" } }
27
+ }
28
+ }
29
+ },
30
+ "$defs": {
31
+ "artifact": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "required": ["path", "sha256", "bytes"],
35
+ "properties": {
36
+ "path": { "type": "string", "minLength": 1 },
37
+ "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
38
+ "bytes": { "type": "integer", "minimum": 0 }
39
+ }
40
+ },
41
+ "issue": {
42
+ "type": "object",
43
+ "additionalProperties": false,
44
+ "required": ["slideNumber", "message"],
45
+ "properties": {
46
+ "slideNumber": { "type": ["integer", "null"], "minimum": 1 },
47
+ "message": { "type": "string" }
48
+ }
49
+ },
50
+ "materializedShape": {
51
+ "type": "object",
52
+ "additionalProperties": false,
53
+ "required": ["slideNumber", "sourceLayoutPath", "sourceShapeId", "outputShapeId"],
54
+ "properties": {
55
+ "slideNumber": { "type": "integer", "minimum": 1 },
56
+ "sourceLayoutPath": { "type": "string", "minLength": 1 },
57
+ "sourceShapeId": { "type": "integer", "minimum": 0, "maximum": 4294967295 },
58
+ "outputShapeId": { "type": "integer", "minimum": 0, "maximum": 4294967295 }
59
+ }
60
+ },
61
+ "removedPlaceholder": {
62
+ "type": "object",
63
+ "additionalProperties": false,
64
+ "required": ["slideNumber", "shapeId", "placeholderType"],
65
+ "properties": {
66
+ "slideNumber": { "type": "integer", "minimum": 1 },
67
+ "shapeId": { "type": "integer", "minimum": 0, "maximum": 4294967295 },
68
+ "placeholderType": { "type": "string", "minLength": 1 }
69
+ }
70
+ }
71
+ }
72
+ }
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';
@@ -171,6 +170,87 @@ const xlsxApplyOutput = z.object({
171
170
  }).strict(),
172
171
  }).strict();
173
172
 
173
+ const pptxTemplateApplyResult = z.object({
174
+ input: z.string().min(1),
175
+ template: z.string().min(1),
176
+ output: z.string().min(1),
177
+ changedSlideCount: z.number().int().nonnegative(),
178
+ issues: z.array(z.object({
179
+ slideNumber: z.number().int().positive().nullable(),
180
+ message: z.string(),
181
+ }).strict()),
182
+ materializedLayoutShapes: z.array(z.object({
183
+ slideNumber: z.number().int().positive(),
184
+ sourceLayoutPath: z.string().min(1),
185
+ sourceShapeId: z.number().int().nonnegative().max(0xffffffff),
186
+ outputShapeId: z.number().int().nonnegative().max(0xffffffff),
187
+ }).strict()),
188
+ frozenPlaceholderCount: z.number().int().nonnegative(),
189
+ removedSystemPlaceholders: z.array(z.object({
190
+ slideNumber: z.number().int().positive(),
191
+ shapeId: z.number().int().nonnegative().max(0xffffffff),
192
+ placeholderType: z.string().min(1),
193
+ }).strict()),
194
+ }).strict();
195
+ const pptxFormatApplyResult = z.object({
196
+ input: z.string().min(1),
197
+ output: z.string().min(1),
198
+ operationCount: z.number().int().nonnegative(),
199
+ changedCount: z.number().int().nonnegative(),
200
+ changes: z.array(z.object({
201
+ slideNumber: z.number().int().positive(),
202
+ shapeId: z.number().int().nonnegative().max(0xffffffff),
203
+ runIndex: z.number().int().nonnegative(),
204
+ properties: z.array(z.string()),
205
+ }).strict()),
206
+ issues: z.array(z.object({
207
+ slideNumber: z.number().int().positive(),
208
+ shapeId: z.number().int().nonnegative().max(0xffffffff),
209
+ runIndex: z.number().int().nonnegative(),
210
+ message: z.string(),
211
+ }).strict()),
212
+ }).strict();
213
+ const pptxTemplateApplyReceipt = z.object({
214
+ schema: z.literal('tiwater.office.pptx-template-apply-receipt/v1'),
215
+ pass: z.boolean(),
216
+ input: artifact,
217
+ template: artifact,
218
+ plan: artifact,
219
+ output: artifact.nullable(),
220
+ providerResult: pptxTemplateApplyResult,
221
+ }).strict();
222
+ const pptxFormatApplyReceipt = z.object({
223
+ schema: z.literal('tiwater.office.pptx-format-apply-receipt/v1'),
224
+ pass: z.boolean(),
225
+ input: artifact,
226
+ operations: artifact,
227
+ output: artifact.nullable(),
228
+ providerResult: pptxFormatApplyResult,
229
+ }).strict();
230
+ const pptxTemplateApplyOutput = z.object({
231
+ tool: z.literal('pptx_apply_template'),
232
+ runtime: runtimeIdentity,
233
+ receipt: artifact,
234
+ output: artifact.nullable(),
235
+ summary: z.object({
236
+ pass: z.boolean(),
237
+ changedSlideCount: z.number().int().nonnegative(),
238
+ issueCount: z.number().int().nonnegative(),
239
+ }).strict(),
240
+ }).strict();
241
+ const pptxFormatApplyOutput = z.object({
242
+ tool: z.literal('pptx_apply_format'),
243
+ runtime: runtimeIdentity,
244
+ receipt: artifact,
245
+ output: artifact.nullable(),
246
+ summary: z.object({
247
+ pass: z.boolean(),
248
+ operationCount: z.number().int().nonnegative(),
249
+ changedCount: z.number().int().nonnegative(),
250
+ issueCount: z.number().int().nonnegative(),
251
+ }).strict(),
252
+ }).strict();
253
+
174
254
  const renderFileIdentity = z.object({
175
255
  sha256: z.string().regex(/^[a-f0-9]{64}$/),
176
256
  size_bytes: z.number().int().positive(),
@@ -424,6 +504,31 @@ const tools = [
424
504
  outputSchema: artifactOutput('pptx_export_json'),
425
505
  handler: pptxExportJson,
426
506
  },
507
+ {
508
+ name: 'pptx_apply_template',
509
+ description: 'Apply one deterministic PPTX template-application plan to a current presentation. This tool executes the published plan; it does not select a template or derive business content, slide mappings, geometry, or formatting decisions.',
510
+ inputSchema: z.object({
511
+ input: pathInput.describe('Path to the current source PPTX.'),
512
+ template: pathInput.describe('Path to the selected current template PPTX.'),
513
+ plan: pathInput.describe('Path to the deterministic template-application plan artifact.'),
514
+ output: pathInput.describe('New PPTX output path. Existing files are never overwritten.'),
515
+ receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
516
+ }).strict(),
517
+ outputSchema: pptxTemplateApplyOutput,
518
+ handler: pptxApplyTemplate,
519
+ },
520
+ {
521
+ name: 'pptx_apply_format',
522
+ description: 'Apply one deterministic PPTX formatting plan to a current presentation. This tool executes published formatting operations; it does not derive values, coordinates, or business decisions.',
523
+ inputSchema: z.object({
524
+ input: pathInput.describe('Path to the current PPTX.'),
525
+ operations: pathInput.describe('Path to the deterministic formatting operations artifact.'),
526
+ output: pathInput.describe('New PPTX output path. Existing files are never overwritten.'),
527
+ receiptOutput: pathInput.describe('New JSON receipt path. Existing files are never overwritten.'),
528
+ }).strict(),
529
+ outputSchema: pptxFormatApplyOutput,
530
+ handler: pptxApplyFormat,
531
+ },
427
532
  ];
428
533
 
429
534
  function buildServer() {
@@ -1058,7 +1163,7 @@ async function xlsxApply(args) {
1058
1163
 
1059
1164
  async function xlsxValidate(args) {
1060
1165
  const input = requireString(args.input, 'input');
1061
- const result = await runXlsxValidateCandidateChain(['validate', input]);
1166
+ const result = await runJsonCandidateChain(xlsxCandidates, ['validate', input], { allowedExitCodes: [0, 1] });
1062
1167
  return { tool: 'xlsx_validate', runtime: commandRuntime(result), result: result.json };
1063
1168
  }
1064
1169
 
@@ -1082,6 +1187,81 @@ async function pptxExportJson(args) {
1082
1187
  };
1083
1188
  }
1084
1189
 
1190
+ async function pptxApplyTemplate(args) {
1191
+ return pptxApply('pptx_apply_template', args, true);
1192
+ }
1193
+
1194
+ async function pptxApplyFormat(args) {
1195
+ return pptxApply('pptx_apply_format', args, false);
1196
+ }
1197
+
1198
+ async function pptxApply(tool, args, templateMode) {
1199
+ const input = path.resolve(requireString(args.input, 'input'));
1200
+ const template = templateMode ? path.resolve(requireString(args.template, 'template')) : null;
1201
+ const plan = path.resolve(requireString(templateMode ? args.plan : args.operations, templateMode ? 'plan' : 'operations'));
1202
+ const output = path.resolve(requireString(args.output, 'output'));
1203
+ const receiptOutput = path.resolve(requireString(args.receiptOutput, 'receiptOutput'));
1204
+ for (const [label, candidate] of [['input', input], ['output', output], ...(template ? [['template', template]] : [])]) {
1205
+ if (path.extname(candidate).toLowerCase() !== '.pptx') {
1206
+ throw Object.assign(new Error(`${label} must use the .pptx extension`), { code: -32602 });
1207
+ }
1208
+ }
1209
+ await requireNewFile(output, 'output');
1210
+ await requireNewFile(receiptOutput, 'receiptOutput');
1211
+ const inputArtifact = await fileArtifact(input);
1212
+ const templateArtifact = template ? await fileArtifact(template) : null;
1213
+ const planArtifact = await fileArtifact(plan);
1214
+ await mkdir(path.dirname(output), { recursive: true });
1215
+ try {
1216
+ const command = templateMode ? 'apply-template' : 'apply-format-edits';
1217
+ const commandArgs = templateMode
1218
+ ? [command, input, template, plan, output]
1219
+ : [command, input, plan, output];
1220
+ const result = await runJsonCandidateChain(pptxCandidates, commandArgs, { allowedExitCodes: [0, 1] });
1221
+ await requireArtifactUnchanged(inputArtifact, 'PPTX apply input');
1222
+ if (templateArtifact) await requireArtifactUnchanged(templateArtifact, 'PPTX apply template');
1223
+ await requireArtifactUnchanged(planArtifact, 'PPTX apply plan');
1224
+ const providerResult = (templateMode ? pptxTemplateApplyResult : pptxFormatApplyResult).parse(result.json);
1225
+ if (path.resolve(providerResult.input) !== input
1226
+ || path.resolve(providerResult.output) !== output
1227
+ || (templateMode && path.resolve(providerResult.template) !== template)) {
1228
+ throw new Error('PPTX apply receipt is not bound to the current inputs and output');
1229
+ }
1230
+ const pass = providerResult.issues.length === 0;
1231
+ const outputArtifact = pass ? await fileArtifact(output) : null;
1232
+ if (!pass) await rm(output, { force: true });
1233
+ const receiptSchema = templateMode ? pptxTemplateApplyReceipt : pptxFormatApplyReceipt;
1234
+ const receipt = receiptSchema.parse({
1235
+ schema: templateMode
1236
+ ? 'tiwater.office.pptx-template-apply-receipt/v1'
1237
+ : 'tiwater.office.pptx-format-apply-receipt/v1',
1238
+ pass,
1239
+ input: inputArtifact,
1240
+ ...(templateMode ? { template: templateArtifact } : {}),
1241
+ ...(templateMode ? { plan: planArtifact } : { operations: planArtifact }),
1242
+ output: outputArtifact,
1243
+ providerResult,
1244
+ });
1245
+ return {
1246
+ tool,
1247
+ runtime: commandRuntime(result),
1248
+ receipt: await writeJsonArtifact(receiptOutput, receipt),
1249
+ output: outputArtifact,
1250
+ summary: templateMode
1251
+ ? { pass, changedSlideCount: providerResult.changedSlideCount, issueCount: providerResult.issues.length }
1252
+ : { pass, operationCount: providerResult.operationCount, changedCount: providerResult.changedCount, issueCount: providerResult.issues.length },
1253
+ };
1254
+ } catch (error) {
1255
+ await rm(output, { force: true });
1256
+ throw error;
1257
+ }
1258
+ }
1259
+
1260
+ async function requireArtifactUnchanged(expected, label) {
1261
+ const current = await fileArtifact(expected.path);
1262
+ if (!isDeepStrictEqual(current, expected)) throw new Error(`${label} changed during provider execution`);
1263
+ }
1264
+
1085
1265
  async function writeJsonArtifact(output, payload) {
1086
1266
  const fullPath = path.resolve(output);
1087
1267
  await mkdir(path.dirname(fullPath), { recursive: true });
@@ -1125,59 +1305,3 @@ function commandRuntime(result) {
1125
1305
  }
1126
1306
 
1127
1307
  serveStdio(buildServer);
1128
-
1129
- async function runXlsxValidateCandidateChain(args) {
1130
- const errors = [];
1131
- for (const candidate of xlsxCandidates) {
1132
- try {
1133
- const result = await runValidationCommand(candidate, args);
1134
- const text = result.stdout.trim();
1135
- if (!text) return { ...result, json: null };
1136
- try {
1137
- return { ...result, json: JSON.parse(text) };
1138
- } catch {
1139
- if (result.code !== 0) {
1140
- errors.push(`${candidate.command}: validate did not return JSON`);
1141
- continue;
1142
- }
1143
- throw new Error(`Expected JSON output but received: ${text.slice(0, 300)}${text.length > 300 ? '…' : ''}`);
1144
- }
1145
- } catch (error) {
1146
- if (error?.code === 'ENOENT') {
1147
- errors.push(`${candidate.command}: not found`);
1148
- continue;
1149
- }
1150
- throw error;
1151
- }
1152
- }
1153
- throw new Error(`No runnable command candidate succeeded. ${errors.join('; ')}`);
1154
- }
1155
-
1156
- async function runValidationCommand(candidate, args) {
1157
- const env = { ...process.env, ...(candidate.env || {}) };
1158
- const cwd = candidate.cwd || process.cwd();
1159
- const commandArgs = [...(candidate.argsPrefix || []), ...args];
1160
-
1161
- return await new Promise((resolve, reject) => {
1162
- const child = spawn(candidate.command, commandArgs, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
1163
- let stdout = '';
1164
- let stderr = '';
1165
-
1166
- child.stdout.on('data', chunk => {
1167
- stdout += chunk.toString();
1168
- });
1169
-
1170
- child.stderr.on('data', chunk => {
1171
- stderr += chunk.toString();
1172
- });
1173
-
1174
- child.on('error', reject);
1175
- child.on('close', code => {
1176
- if (code === 0 || code === 1) {
1177
- resolve({ code, stdout, stderr, command: candidate.command, args: commandArgs, cwd });
1178
- return;
1179
- }
1180
- reject(new Error(`${candidate.command} ${commandArgs.join(' ')} failed with exit code ${code}\n${stderr || stdout}`));
1181
- });
1182
- });
1183
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiwater/office-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Published MCP server for Tiwater Office document capabilities",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -18,7 +18,8 @@
18
18
  "files": [
19
19
  "_shared/tool-runtime.mjs",
20
20
  "office/index.mjs",
21
- "office/README.md"
21
+ "office/README.md",
22
+ "office/contracts/*.schema.json"
22
23
  ],
23
24
  "scripts": {
24
25
  "test": "node --test office/*.test.mjs"
@@ -29,5 +30,8 @@
29
30
  "dependencies": {
30
31
  "@modelcontextprotocol/server": "2.0.0",
31
32
  "zod": "4.4.3"
33
+ },
34
+ "devDependencies": {
35
+ "ajv": "8.20.0"
32
36
  }
33
37
  }